Repository navigation
UI tool-hang watchdog kills a turn waiting on request_user_input after 600 s #6872
Description
UI tool-hang watchdog kills a turn waiting on request_user_input after 600 s
Not a duplicate of:
- Make user-input / approval wait timeout configurable (and disableable) #6003 (closed) — makes the user-input timeout configurable; fixed in 0.9.14+.
- v0.9.14: unanswered user-input prompts stick in pending_user_inputs forever when their turn dies without the terminal settle #6275 (closed) —
pending_user_inputsentries leak when a turn dies; UI renders a stale sheet.- Goal turns stop at 1,000 steps while ordinary turns are uncapped; [goal] max_steps = 0 is not unlimited #6512 (open) — budgets disagree across hosts (ACP 50, goals 1000, wall clock, 5-min user-input).
This issue: the turn itself is killed by the UI tool-hang watchdog while the
request_user_inputmodal is open. The user answers, but the turn is already dead — they
must type "continue" to get the model going again.
What happened?
The model calls request_user_input and the question modal appears. While the user is
reading/typing an answer (≥ 10 minutes), the UI tool-hang watchdog fires: it sees a
"running tool with no progress" for 600 s and calls recover_stalled_runtime_turn, which
ends the Engine's turn. The modal is dismissed, the turn is gone, and the user's answer
lands in a dead turn. The user sees a tool error (paraphrased as "tool issue
request_user_input") and must type "continue" to re-trigger the model.
This happens even when [tools] user_input_timeout_seconds = 0 (indefinite wait) is set,
because the UI watchdog is separate from the engine's user-input timeout and does not
know that a request_user_input wait is a parked human wait, not a hung tool.
Steps to reproduce
- Run
codewhale(TUI) in a project. - Trigger any conversation where the model will call
request_user_input
(e.g. ask it a multi-choice question). - Do not answer for ≥ 10 minutes (e.g. step away, read something long).
- Watch: the turn is killed by the UI watchdog. The modal is gone, the turn shows
an error. - Type "continue" — the model resumes, but only because a new user message started
a new turn, not because the original turn survived.
Version
codewhale 0.10.1 (dev)
Where did it happen?
- TUI (terminal interface)
Operating system
- Linux
How did you get Codewhale?
- Built from a git checkout
Provider and model
openrouter / stealth/space-bunny-alpha (does not matter — any provider/model)
Terminal and shell
tmux + bash
Logs, screenshots, anything else
Ground truth: ~/.codewhale/crashes/ records
Multiple turn-stall records from the last several days, all with the same signature:
Kind: turn-stall
Source: ui
Phase: while a tool ran with no progress
No progress for: 600s
Phase bound: 600s
Examples:
20261005T015343.118Z-turn-stall-ui.log20261004T002402.010Z-turn-stall-ui.log20261003T215330.603Z-turn-stall-ui.log20261003T215154.067Z-turn-stall-ui.log
All with Provider request: openrouter / stealth/space-bunny-alpha.
Caveat (honesty): the phase string "while a tool ran with no progress" is shared by
any running tool, including exec_shell — these records prove the UI watchdog fired on a
600 s no-progress tool wait, but they do not by themselves disambiguate that the tool was
request_user_input specifically. The user-visible symptom (answer lands in a dead turn,
"continue" needed) is what ties it to request_user_input.
Root cause (by code reading, not by live repro)
The UI watchdog in crates/tui/src/tui/ui/session_state.rs:375-383 checks:
if app.is_loading
&& ... && !app.is_compacting
&& active_turn_has_running_tool(app)
&& let Some(last_activity) = app.turn_last_activity_at.or(app.turn_started_at)
&& now.saturating_duration_since(last_activity) > TOOL_HANG_WATCHDOG_TIMEOUT // 600 s, ui.rs:209
{
record_ui_stall(...); // session_state.rs:384
recover_stalled_runtime_turn(...); // session_state.rs:389 — ends the Engine turn
}active_turn_has_running_tool (tui/ui.rs:734) returns true for a request_user_input
in flight: the tool cell is created with status: ToolStatus::Running
(tool_routing.rs:293) and stays Running until the tool result arrives, i.e. until the
user answers. turn_last_activity_at is refreshed on engine events (session_state.rs:663);
during the human wait the engine sends a status event once (approval.rs:428) and then goes
quiet, so the 600 s of silence is exactly what the watchdog counts. The watchdog has no
exception for "this tool is waiting on a human".
The engine side is correct:
core/engine/approval.rs:404-406—turn_wall_clock.begin_human_wait()/
end_human_wait()excludes the human wait from the wall clock.core/engine/turn_heartbeat.rs— a human approval is declared parked
(bound = None) and never reported by the engine's own stall watchdog.core/engine/approval.rs:414-421—[tools] user_input_timeout_seconds: absent or0
→None→ "waits until the person answers or cancels" (the Make user-input / approval wait timeout configurable (and disableable) #6003 resolution).
The UI watchdog is a separate mechanism (session_state.rs) that reads
app.turn_last_activity_at and does not consult the engine's parked state. It
treats a 10-minute human wait identically to a hung tool.
Suggested fix
In session_state.rs, the tool-hang watchdog condition should exclude turns whose
only running tool is request_user_input (or more generally, any tool the engine
has marked as parked/human-wait). The engine already publishes this via
EngineHandle (turn_heartbeat); the UI should read it instead of inferring
liveness from turn_last_activity_at.
Config note
[tools] user_input_timeout_seconds is not set in my config (no [tools] table).
Per config.rs, absent → None → indefinite wait. This is the intended behavior
and is working. The bug is that the UI watchdog operates independently of this
setting.
Codex 0.10.1 completion packet, 2026-10-06 UTC
Owner: current Engine release lead in worktrees/cw-ts-extensions, wave/0.10.1-next, integration PR #6846. Existing source commits 9752aa46 and fa4709ef repair indefinite question liveness, exact-ID retirement, bounded answer delivery with Engine acknowledgement, durable answer races and failed-delivery restoration. Focused local proof at those commits: 90 Rust tests passed, 0 failed; this is not final integrated CI/provider proof.
The adjacent audit confirmed the same false-stall class for direct approvals/elevation and hidden current-session child requests. Claimed follow-up paths: crates/tui/src/tui/ui/{session_state.rs,apply.rs,handlers.rs,event_loop.rs,remote_control_bridge.rs,tests.rs}, crates/tui/src/tui/ui.rs, crates/tui/src/tui/approval/elevation.rs, and crates/tui/src/tui/views/mod.rs. Repairs reuse existing modal/request authority, retire exact tool IDs before presentation filters, preserve live child requests at parent terminal boundaries, and grant resume activity only after successful answer delivery. Combined contributor build and focused tests are in progress. Fresh exact-head Linux/macOS/Windows/safety/web CI and real DeepSeek terminal acceptance remain required.
Separate remaining finding: finite approval expiry currently advances only the top view; hidden child reopening restarts that UI deadline. This is covered by the existing approval program / #6101 and must not be claimed fixed by the indefinite-wait repair. No new duplicate issue was filed.
2026-10-06 current integration and real terminal evidence
Qualified Engine ed5e3f1 (G6), PR6846, retains exact-ID retirement, Engine acknowledgement, bounded delivery and live human-wait authority. Finite approval expiry remains Engine-owned (b0fb5f8); hidden/reopened prompts cannot restart its deadline or accept after expiry, while zero remains indefinite. Focused approval28/0 and liveness19/0 remain their original local source receipts.
Exact3907-blob optimized G6 build/atomic install passed; all four aliases report0.10.1(ed5e3f1), valid local ad-hoc signatures. Actual DeepSeek-v4-flash original question in session569ad44f-ce06-43a7-83b7-70aa70cbd788 stayed pending703.889719s request→result, including634.677676s visibly unanswered. Original request124e6832→result with matching parent/call/execution ID; same turn completed G6_WAIT_Aurora without Continue. Eight-part answer folded/restored with full durable body/journal/tool count unchanged. Exact cancellation, help80x24→110x34, full process restart, unchanged startup journal and recall without supplying Aurora passed; both exits0/no panic. Actual24-line paste bytes exact in active prompt, no dispatch until separate Enter. Receipt: artifacts/takeover-0101-20261005/deepseek-G6-human-wait/acceptance-final.json.
Current coding actual+independent Node6/0/exit0, reviewed write/command approval, optional command denial/no marker/no retry/unchanged fixtures passed. Installed token/control HTTP42/0 also passed. Current four-language PRCodeQL SUCCESS/0open current-ref findings; reviews resolved. Full37531610868 SUCCESS: Linux18982/0/34skip,macOS18985/0/33skip,Windows18279/0/27skip;docs3/0/8ignored per OS. Explicitshared-twice37531708395 SUCCESS/all20;nonpublishingG6RC37531712159 SUCCESS/all15/7targets/34assets/38contracts. Original PR6846 merged to Main ad333fd with exactly the G6 tree and all11 original contributor heads. Marked Done after qualified Main landing. Final Main RC, ordinary-user Windows clipboard, signing/publication/deployment remain separate.
Activity
fix(release): keep pending questions alive and repair CI fixtures
fix(runtime): acknowledge human answers and retire ended question waits
Integrate reviewed 0.10.1 contributor work and harden parked turns