Skip to content

UI tool-hang watchdog kills a turn waiting on request_user_input after 600 s #6872

Closed

Description

@7jrxt42BxFZo4iAnN4CX

UI tool-hang watchdog kills a turn waiting on request_user_input after 600 s

Not a duplicate of:

This issue: the turn itself is killed by the UI tool-hang watchdog while the
request_user_input modal 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

  1. Run codewhale (TUI) in a project.
  2. Trigger any conversation where the model will call request_user_input
    (e.g. ask it a multi-choice question).
  3. Do not answer for ≥ 10 minutes (e.g. step away, read something long).
  4. Watch: the turn is killed by the UI watchdog. The modal is gone, the turn shows
    an error.
  5. 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.log
  • 20261004T002402.010Z-turn-stall-ui.log
  • 20261003T215330.603Z-turn-stall-ui.log
  • 20261003T215154.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 or 0
    → 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

  1. added
    needs-triageNew external report awaiting maintainer triage; repro, logs and version output help
    on Oct 6, 2026
  2. added 3 commits that reference this issue on Oct 6, 2026
    9752aa4
    fa4709e
    b0fb5f8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-triageNew external report awaiting maintainer triage; repro, logs and version output help

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions