{"record":{"id":"d0947360294a3cf9","repo":"xai-org/grok-build","slug":"terminal-writer-stopped","errorCode":null,"errorMessage":"terminal writer stopped","messagePattern":"terminal writer stopped","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/codegen/xai-grok-pager/src/app/event_loop.rs","lineNumber":2411,"sourceCode":"            // Leader disconnect: the bridge fires cancel when the IPC channel closes\n            // Without this arm the loop would hang because AppView holds the client-side tx, keeping acp_rx open\n            _ = connection_cancel.cancelled() => {\n                break;\n            }\n\n            // Graceful-quit request from the signal handler\n            // Kept high in the biased order so a SIGTERM quit isn't starved by an ACP firehose\n            _ = quit_notify.notified() => {\n                let effs = dispatch::dispatch(Action::Quit, &mut app);\n                let _ = process_effects(effs, &mut tasks, &mut app, &progress_tx);\n                break;\n            }\n\n            writer_event = writer_event_rx.recv() => {\n                let Some(writer_event) = writer_event else {\n                    app.finish_startup(xai_grok_telemetry::startup::StartupOutcome::Error);\n                    flush_pending_stall(&mut stall_rollup);\n                    return Err(anyhow::anyhow!(\"terminal writer stopped\"));\n                };\n                let sequence = match writer_event_sequence(writer_event)\n                    .context(\"terminal output failed\")\n                {\n                    Ok(sequence) => sequence,\n                    Err(e) => {\n                        app.finish_startup(xai_grok_telemetry::startup::StartupOutcome::Error);\n                        flush_pending_stall(&mut stall_rollup);\n                        return Err(e);\n                    }\n                };\n                presenter.acknowledge(sequence);\n            }\n\n            // Biased order: cancellation/quit, writer acks/failures, ACP, task/progress results, updates, input, and render/poll timers\n            // All of them precede the deliberately-last voice STT arm (see its note below)\n\n            // Gated on empty terminal input","sourceCodeStart":2393,"sourceCodeEnd":2429,"githubUrl":"https://github.com/xai-org/grok-build/blob/bc7f02eddd3d84085849dc19ed216f11c23b0571/crates/codegen/xai-grok-pager/src/app/event_loop.rs#L2393-L2429","documentation":"The event loop multiplexes the terminal writer's event channel via tokio::select. When writer_event_rx.recv() returns None, the writer task has terminated (channel closed) while the app is still running, so the loop records a startup error outcome, flushes stall telemetry, and aborts with this anyhow error.","triggerScenarios":"The terminal writer task exits early — it panics, returns Err on an unwritable/lost stdout (e.g. output pipe closed by the terminal or pager teardown), or is aborted — dropping its sender so recv() yields None in the select branch.","commonSituations":"Running the TUI with stdout redirected into a pipe that the consumer closes; writer task crash due to an I/O error on a detached/SSH-disconnected terminal; a bug causing writer shutdown before the event loop exits.","solutions":["Re-run with logging/RUST_BACKTRACE=1 to find why the writer task exited (panic or I/O error) first","Check that stdout is a valid, writable terminal or pipe and the parent process has not closed it","Verify terminal size/emulator handles the writer's output (try a plain xterm or TERM=xterm-256color)","Inspect writer task code for early returns/aborts on non-fatal I/O errors and make them retry or degrade gracefully"],"exampleFix":"// before: writer exits on transient write error\nwriter.send(line).await?;\n// after: tolerate transient failures instead of closing the channel\nif writer.send(line).await.is_err() {\n    tracing::warn!(\"terminal write failed; backing off\");\n    tokio::time::sleep(Duration::from_millis(50)).await;\n}","handlingStrategy":"try-catch","validationCode":"// before launching, ensure stdout is writable\nuse std::io::Write;\nfn stdout_writable() -> bool {\n    let mut out = std::io::stdout();\n    out.flush().is_ok()\n}","typeGuard":null,"tryCatchPattern":"match run().await {\n    Err(e) if e.to_string().contains(\"terminal writer stopped\") => {\n        eprintln!(\"TUI writer died; re-run in a persistent terminal: {e:#}\");\n    }\n    Err(e) => eprintln!(\"fatal: {e:#}\"),\n    Ok(_) => {}\n}","preventionTips":["Run the TUI attached to a real terminal, not a short-lived pipe","Enable RUST_BACKTRACE and tracing to capture writer-task panics early","Avoid killing/closing the parent shell or SSH session while the pager runs","Monitor writer-task health and restart it instead of letting the channel close"],"tags":["async","terminal","channel-closed","tui"],"backgroundTag":"channel-closed-early","analyzedSha":"bc7f02eddd3d84085849dc19ed216f11c23b0571","analyzedAt":"2026-08-31T04:59:42.031Z","schemaVersion":2},"datasetVersion":"2026-08-31T09:17:48.483Z"}