Skip to main content

FLUSHER_DRAIN_TIMEOUT

Constant FLUSHER_DRAIN_TIMEOUT 

Source
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.