Hmbown / Hmbown/Codewhale

bug(exec): request_user_input waits forever in a headless run — no responder exists and the disable switch removes the bound

Open
#6,236 2 comments 0 reactions 0 assignees View on GitHub
bug
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.