bug(exec): request_user_input waits forever in a headless run — no responder exists and the disable switch removes the bound
- Dominant language
- Rust
- Stars
- 41k
- Forks
- 3.6k
- Avg merge
- 13h 59m
- Merged PRs (30d)
- 299
Description
## Description
A headless `codewhale exec` run can block indefinitely on `request_user_input`, with no error and nothing in the run's own output that says it is waiting for a human. The tool is offered to the model in headless mode, the engine emits the request and then waits for a response that a client has to submit — but a one-shot exec has no responder. The operator's own `[tools] user_input_timeout_seconds = 0` ("wait without timeout", the disable switch added by #6003) then removes the last bound, leaving cancellation and the turn wall clock (`turn_wall_clock_secs`, default 3600 s, configurable up to 24 h) as the only stops.
So on a nightly run with a 24 h turn budget, a model that decides to clarify something parks the run for the rest of the night.
## Steps to reproduce
1. Set `[tools] user_input_timeout_seconds = 0` (a documented, deliberately supported setting).
2. Run `codewhale exec --auto ""`.
3. The run emits the request and waits; nothing can submit an answer, so it ends only on cancellation or when the turn wall clock expires.
Code path (read, not reproduced live — see the caveat below):
- `crates/tui/src/core/engine/approval.rs:227` sends `Event::UserInputRequired`;
- `:235` resolves the wait as `self.config.user_input_timeout.unwrap_or(USER_INPUT_TIMEOUT)`, with `USER_INPUT_TIMEOUT = 300 s` at `:15`;
- `:245` — when the configured value is zero, the receive is awaited with **no timeout**, so the `ToolError::Timeout` branch at `:275-280` can never run;
- the tool is in the catalog unconditionally (`crates/tui/src/core/engine/tool_setup.rs:116`), with no interactive-boundary gate.
## Expected behavior
A run without an interactive boundary should resolve `request_user_input` fail-closed instead of waiting:
- answer immediately with a typed error the model can act on, or
- ignore the "disable the timeout" value where nothing can answer it — `0` is meaningful in the TUI (a human will answer eventually) and meaningless in `exec`.
If the project would rather keep the tool unavailable than fail, the same fix could drop `request_user_input` from worker and/or headless session surfaces — the child role prompt already tells workers never to ask ("You are a background sub-agent: every instruction comes from the orchestrating agent, not a human. Never address the end user or ask them questions", `crates/tui/src/tools/subagent/mod.rs:11195`).
## Actual behavior
The wait is unbounded, and the state is not even observable from the run's output: `crates/tui/src/exec_agent.rs` has no `ExecStreamEvent` variant and no handler for `Event::UserInputRequired`, so a stream-json consumer sees nothing while the run sits on the request.
That is an asymmetry with every other stop in the runtime:
- a child gets a wall clock — 1800 s default (`crates/tui/src/tools/subagent/mod.rs:271`);
- one tool call is capped at 300 s (`crates/tui/src/config/subagent_limits.rs:42`);
- a child with no manager-visible progress is auto-cancelled after 300 s (`:45`, log line at `subagent/mod.rs:7995`);
- `agent action=wait` is clamped to 1–120 s (`subagent/mod.rs:10327-10331`);
- only the input wait has no bound of its own.
Cheap operator-side workarounds exist, and they are all per-run flags rather than product behaviour:
- `codewhale exec --disallowed-tools request_user_input` — the session deny list is merged into every child's (`subagent/mod.rs:10514`) and an ancestor denial cannot be dropped (`docs/SUBAGENTS.md:104`), so the whole run loses the ability to ask;
- or a separate headless config carrying a finite `user_input_timeout_seconds`.
## Impact
For unattended runs the failure is silent: the process is alive, the transcript looks fine, and the only bound is a wall clock measured in hours. In this pipeline (`codewhale exec --auto --hooks`, one call per role, nightly cycles) the mitigation above is being wired in; the class is not specific to it.
Related: #6003 made this timeout configurable *and disableable* — that is where `0` comes from; this report is the headless half of that decision. #6013 (goal gates, `needs_input` statuses) touches the same "a worker needs input" surface.
## Environment
- OS: Arch Linux
- codewhale version: 0.9.13 (dev)
- Install method: source build
- Terminal app: tmux (`TERM=tmux-256color`)
- Shell: fish
- Run shape: `codewhale exec --auto --hooks …`, `[tools] user_input_timeout_seconds = 0`, `[tui] turn_wall_clock_secs = 86400`
## Logs, screenshots, or recordings
Code references were read at upstream `main` = `9cdfa92bc`. Caveat: I did not reproduce the hang live — that means waiting out the wall clock. The diagnosis is the code path above plus the documented meaning of `0`. The control for a live reproduction would be the same run with `--disallowed-tools request_user_input`.
Contributor guide
Research direction
Start with crates/tui/src/core/engine/approval.rs at the UserInputRequired event and timeout resolution, then compare the tool catalog in crates/tui/src/core/engine/tool_setup.rs with the headless path in crates/tui/src/exec_agent.rs. Trace how exec handles the request and its output events, using the cited configuration and subagent limits for context. Done means a headless request cannot wait indefinitely and its outcome is visible or actionable to the model and operator.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- backend, cli
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100