pub(super) const FLUSHER_DRAIN_TIMEOUT: Duration;Expand description
Bound on how long the wait task waits for the coalescing flusher to
drain trailing output after the direct child has already been reaped
(codex P1 on PR #3206, third round). Normally resolves almost
immediately: the read loop observes real PTY EOF once the direct child
exits and drops pty_tx, closing the flusher’s channel. But EOF depends
on every process holding the PTY slave fd closing it, not just the
direct child — a background descendant that inherited the fd and
ignores SIGHUP can keep it open indefinitely, in which case the flusher
would never see its channel close and its JoinHandle would never
resolve on its own. An unbounded await here would then hang child
reaping’s own downstream cleanup (STATUS_DONE, run_lock release)
forever, for a case that must never block them. The original 10s mirrored
persistent/process_waiter.rs’s stdout-reader bound for the same
descendant-held-descriptor scenario: generous enough that normal
flushing (one already-queued drain plus a write, milliseconds) never trips
it, but a hard ceiling so a genuinely stuck descendant can’t hang pane
teardown. Unlike persistent/process_waiter.rs’s bound, expiry does NOT abort the
flusher (reagentx P1 on PR #3206, same round): there, one combined async
reader task both reads and processes, so aborting it genuinely stops the
read. Here reading (a separate spawn_blocking doing a raw, blocking,
un-cancellable OS read()) and flushing are two different tasks —
aborting only the flusher can’t reclaim the read loop’s thread either
way, and would additionally make every later blocking_send find the
receiver gone and silently drop it, permanently losing any further
output from a still-live descendant. Letting the JoinHandle simply
drop on timeout detaches the flusher (and transitively its read loop)
to keep running independently in the background instead, so nothing
produced after teardown gives up waiting is silently lost.
1s, reduced from 10s (2026-09-15,
docs/specs/SPEC_TERM_EXIT_RESPAWN_LOOP_2026_09_15.md §12). The
reasoning above is unchanged and still correct — what changed is a
measurement of how often the ceiling is actually hit. On Windows this
bound is reached on essentially every ordinary shell exit, not as the
rare stuck-descendant case it was sized for: ConPTY’s own console-host
process routinely outlives the direct child and holds the PTY open, so
the read loop never sees EOF and the flusher never resolves on its own.
Because STATUS_DONE is published only after this wait, every exit
left the pane visibly inert for the full ceiling — the user-reported
“terminal hangs when I type exit,” reproduced in srv logs as a flat 10s
gap between process exited and any further activity.
Waiting longer buys nothing in that case (the flusher cannot finish until an EOF that may never come — which is exactly why expiry detaches rather than aborts), while costing the full ceiling of dead UI time on every single exit. 1s keeps ~50x headroom over the legitimate case this bound exists for (a final flush is bounded by one drain of what is already queued, plus one FileStore write and broadcast) and cuts the pathological case’s user-visible cost by 10x. Nothing else about the behavior changes: expiry still detaches the flusher and its read loop to keep running in the background, so late output is still never lost.