Browse Source

G-163 proposed: unix tty exit-path output drain (shared-description O_NONBLOCK tail loss)

New goal (user-approved wording): punkshell exit paths on unix ttys restore
blocking on the shared tty file description and drain pending stdout/stderr
(bounded best-effort) before teardown - closing the tclIO behaviour where
stdin's non-blocking read windows make stdout non-blocking underneath,
'flush' silently no-ops once a background flush is scheduled, and TclInExit
discards EAGAIN'd buffers (measured 2026-08-03: 161/5000 lines survive exit
on a throttled WSL pty; ~498KB pending after a 'successful' flush). Guard
homes in punk::console, invoked after the G-038 eof-restart decision point;
acceptance includes complete delivery under a slow reader, bounded exit under
a dead reader, unchanged windows/piped behaviour, ARCHITECTURE.md exit-section
documentation, and a unix-pty-gated pin. Related: G-038, G-013, G-011, G-090
(+ archived G-036 lineage).

goals_lint clean; overlap survey recorded in Notes.

Claude-Session: https://claude.ai/code/session_01QgaxV27VZkmEec7oNbEVFc
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 6 days ago
parent
commit
a9c5c9228e
  1. 4
      GOALS.md
  2. 69
      goals/G-163-unix-exit-output-drain.md

4
GOALS.md

@ -412,3 +412,7 @@ Detail: goals/G-161-tcltestrun-multiline-banner.md
Scope: src/make.tcl (bake stamp writer, bakelist report, digest/cache helpers); src/modules/punk/buildinfo-999999.0a1.0.tm (surface new stamp fields); src/vfs/ (payload trees as digest inputs); src/runtime/ (runtime artifacts and mapvfs.toml as consumed inputs); src/tests/shell/testsuites/punkexe/ (freshness pin)
Detail: goals/G-162-bakelist-kit-freshness.md
### G-163 [proposed] unix tty exit-path output drain: no tail loss from shared-description O_NONBLOCK
Scope: src/modules/punk/repl-999999.0a1.0.tm (exit/eof done paths, reader-window blocking-restore discipline); src/modules/punk/console-999999.0a1.0.tm (drain helper, -blocking 0 window sites); src/lib/app-punkshell/punkshell.tcl (final-exit seam); ARCHITECTURE.md (Process exit and shutdown section); src/tests/shell/testsuites/punkexe/ (unix-pty-gated drain pin)
Detail: goals/G-163-unix-exit-output-drain.md

69
goals/G-163-unix-exit-output-drain.md

@ -0,0 +1,69 @@
# G-163 unix tty exit-path output drain: no tail loss from shared-description O_NONBLOCK
Status: proposed
Scope: src/modules/punk/repl-999999.0a1.0.tm (exit/eof done paths, reader-window blocking-restore discipline); src/modules/punk/console-999999.0a1.0.tm (drain helper, -blocking 0 window sites); src/lib/app-punkshell/punkshell.tcl (final-exit seam); ARCHITECTURE.md (Process exit and shutdown section); src/tests/shell/testsuites/punkexe/ (unix-pty-gated drain pin)
Goal: punkshell sessions on unix ttys do not silently lose queued stdout/stderr output at exit: the exit paths restore blocking on the shared tty file description (undoing the O_NONBLOCK that stdin's non-blocking read windows set underneath Tcl's per-channel view) and drain pending channel output with a bounded best-effort wait before teardown - closing the tclIO behaviour where a large emission near exit is silently truncated (background-flush-scheduled makes 'flush' a no-op and TclInExit discards EAGAIN'd buffers).
Acceptance: on a linux/WSL pty with a rate-limited reader, a punkshell kit session that emits a numbered-line block larger than the pty buffer and immediately exits delivers the block complete (all markers present in the captured stream), where the same harness with the guard absent demonstrates tail loss (pre-fix baseline recorded in this file); in that scenario the exit path reaches 'chan pending output' 0 on stdout and stderr before teardown; with a fully-stopped reader the session still exits within the guard's declared bounded timeout (best-effort drain, never a hang); existing windows exit/quit behaviour and the piped-eof handling path are unchanged (relevant punkexe pins stay green); the mechanism and guard are documented in ARCHITECTURE.md's Process exit and shutdown section; a unix-pty-gated pin covers the complete-delivery and bounded-timeout cases (constraint-skipped where no unix pty is available).
## Context
On a unix tty, fd 0/1/2 typically share one open file description, so the repl's and
punk::console's read-window 'chan configure stdin -blocking 0' sets O_NONBLOCK on
stdout/stderr underneath Tcl's per-channel view (stdout still reports -blocking 1).
tclIO.c then behaves as follows (verified 2026-08-03 against the TEMP_REFERENCE/tcl9
checkout; the code comments acknowledge the dup'd-tty case): a write hitting EAGAIN
queues with BG_FLUSH_SCHEDULED; from then on synchronous 'flush' returns success
without writing (drain needs event-loop writable events); and during exit the EAGAIN
path does not reschedule - queued output is discarded at finalization.
Measured (stock tclsh 8.6.14, WSL pty, throttled reader, 2026-08-03): blocking control
delivered 5000/5000 lines; with stdin -blocking 0, /proc/self/fdinfo/1 flags went
02 -> 04002, 'flush' claimed success with ~498KB pending ('chan pending output'), and
exit delivered 161/5000; a 6s event-loop grace still lost the final ~22KB. The window
flickers with every repl read cycle, so the exposure is a large emission while a
window is open followed by prompt exit (scripted sessions, error exits, quit right
after a big table). WSL's slow console pipeline is the amplifier; windows console
channels do not share flags this way and are unaffected. Each thread's std channel
instance has its own queue, so worker threads that stop servicing their event loop
strand queued output the same way (the thread dimension - G-090 territory).
Full investigation record: session memory 2026-08-03 (punkshell-unix-repl-output-loss);
distinct from the deterministic tee_to_pipe clear defect fixed as shellfilter 0.2.5.
## Approach
- A drain guard on the true-exit path: restore stdin -blocking 1 first (Tcl's fcntl
clears O_NONBLOCK on the shared description, so queued data drains with blocking
writes on the next writable event), then service events until 'chan pending output'
is 0 on stdout/stderr or the bounded timeout expires.
- Home for the helper: punk::console (owns raw/mode arbitration and the window sites);
invoked from the repl/app final-exit seam - after the G-038 eof-restart decision
point, so a restarting session never pays or interferes with the drain.
- The pin drives a kit on a pty with a rate-limited consumer (the 2026-08-03 harness
pattern: script(1) + throttled reader) and a stopped consumer for the timeout case.
## Alternatives considered
- Dedicated /dev/tty output channel (own file description, immune by construction) -
deferred: larger surface change to output routing; intersects G-011's err-channel
design and would be better decided there.
- Do nothing until raw-default lands - rejected: G-013 widens the non-blocking window
substantially, increasing exposure; the guard should exist first.
## Notes
- Related: G-038 (piped-to-interactive restart) - shares the exit/eof seam; the drain
applies only to true exit and must leave the eof-restart handover untouched.
- Related: G-013 (raw mode as default input mode) - raw mode keeps stdin non-blocking
for much longer, widening this hazard's window; this guard is a prerequisite-grade
hardening for that goal.
- Related: G-011 (per-console err channel) - stderr shares the tty description today;
the deferred dedicated-channel alternative belongs to that design space.
- Related: G-090 (shellfilter/shellthread lifecycle audit) - worker threads stranding
their per-thread channel queues at finalize is the thread dimension of this hazard;
G-036 (achieved - see goals/archive/G-036-tcl9-udp-console-worker-wedge.md) is the
adjacent exit-wedge lineage.
- Overlap survey 2026-08-03: goals_xref paths repl + console modules + ARCHITECTURE.md;
mechanism-relevant intersections named above; the remaining repl/console-scoped
proposals (G-002/G-003/G-008/G-009/G-010/G-014/G-042/G-043/G-044) overlap only by
module and are judged non-overlapping for this goal's mechanism.
Loading…
Cancel
Save