0.10.0 follow-up: user-input deadline under the heartbeat, terminal routes off the runtime workers, sleep inhibitor as a tokio child - #6371
Conversation
…rtbeat `await_user_input` rebuilt `timeout(wait, recv())` on every pass through its `select!`, and the #6184 heartbeat wins that select every 60 s. Each win dropped the pending timeout and the next pass started a fresh one, so with the default 300 s timeout an unanswered prompt never expired and the turn stayed parked. The wait now takes one `Instant` deadline before the loop and selects on `timeout_at`, so heartbeats announce the park without extending it; `user_input_timeout_seconds = 0` still waits indefinitely. The regression test uses the 50 ms test heartbeat against a 200 ms timeout and fails on the previous code (its 3 s outer guard trips instead of the configured timeout). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv
…dvertise them only where they answer
The four `/v1/terminal/{name}/*` handlers locked the session mutex and drove
the PTY inline on a Tokio worker. The agent's own tool holds that lock across
a whole command, and a 64 KiB `input` write to a child that stopped reading
blocks until the kernel buffer drains, so a single client request could park
a runtime worker (#6149). Every session touch now runs through one
`with_session` helper on the blocking pool, matching the tool side.
`runtime/info` advertised `terminal_*` from `cfg!(unix)`, but the routes are
gated on `all(unix, not(ohos))` and the OpenHarmony build answers 501. The
flags now use the routes' own gate, and the capability test asserts it.
Docs: the `input` encoding default is `base64` (exact bytes), as the handler
and its round-trip test already say; the 501 platforms name OpenHarmony
beside Windows; a cursor past `total` is answered from `total`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv
`read_since` echoed a cursor past `total` back as `next_cursor`, so a client that continued from it skipped every byte the stream produced before it reached that position, permanently. The start position now clamps to `total`: a future cursor reads nothing, is not a gap, and hands back the head so the next read continues from what actually exists. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv
`SleepGuard` spawned `caffeinate`/`systemd-inhibit` with `std::process` and its `Drop` ran `kill` + `wait` inline, all from inside `Engine::run_turn` on a Tokio worker — the pattern the blocking-call convention (#6149) rules out, and one the budget script does not count because it only matches `thread::sleep` and `std::fs`. The child is now a `tokio::process` one spawned with `kill_on_drop`: dropping the guard still sends the release signal synchronously, and the runtime reaps the process instead of a blocking `wait`. `hold` therefore requires a runtime context, which its only caller already is; the tests run on one and poll for the reap. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv
Without a Drop impl the guard's child field is written and never read outside the test helper, so every non-test build rejects it under -D warnings (the ohos check and the mobile smoke on c6b52ff). Dropping the child in Drop is the release itself — kill_on_drop sends the signal there — so the field's purpose is code rather than a lint waiver. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv
|
Claude finished @Hmbown's task in 1m 29s —— View job Review of PR #6371 (merged)
Note: this repo checkout is a shallow, single-commit snapshot of the already-merged head, so I reviewed the final state of the six changed files ( Findings: none. All six fixes check out against the code as-is:
One pre-existing (not introduced by this PR) note worth flagging separately if anyone revisits No provider/model-prefix routing code, no new tests needed beyond what's already added (the regression test for the timeout bug is the one that matters and is documented as failing without the fix), and no reuse/duplication issues — |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Summary
The review bots that woke when #6370 was marked ready posted eight threads on the merged head. Each one was verified against main
056eb55before touching anything; these are the six that hold, in the order they matter for 0.10.0.await_user_inputrebuilttimeout(wait, recv())on every pass through itsselect!, and the Engine silently freezes mid-run: user messages are persisted but never answered; no error, no log line, no crash entry #6184 heartbeat wins that select every 60 s, dropping the pending timeout each time. With the default 300 s an unanswered prompt parked the turn forever. The wait now takes one absoluteInstantdeadline and selects ontimeout_at;user_input_timeout_seconds = 0still waits indefinitely. Regression testuser_input_deadline_is_not_reset_by_the_wait_heartbeat(50 ms test heartbeat vs a 200 ms timeout) fails on the previous code.SleepGuardspawnedcaffeinate/systemd-inhibitwithstd::processand itsDroprankill+waitinline fromEngine::run_turn, which the blocking-call convention (Blocking calls in async paths: audit thread::sleep and std::fs in tool paths, pick one convention #6149) rules out and the budget script does not count. It is now atokio::processchild spawned withkill_on_drop:Dropdrops the child, which sends the release signal synchronously, and the runtime reaps the process.holdrequires a runtime context, which its only caller already is./v1/terminal/{name}/*handlers locked the session mutex and wrote/resized/killed on a Tokio worker; a 64 KiBinputwrite to a shell that stopped reading blocks until the kernel buffer drains. Every session touch now goes through onewith_sessionhelper on the blocking pool, matching the tool side.runtime/info'sterminal_*flags usedcfg!(unix)while the routes are gated onall(unix, not(ohos)); the flags now use the routes' gate and the capability test asserts it.read_sincereturned a future cursor asnext_cursor, so a client continuing from it skipped every byte produced before the stream reached that position. The start now clamps tototal. This is a deliberate contract change from feat(runtime-api): terminal byte stream (#34), stream resume + idempotent submit (#76), pet agent-count pin (#12) #6361's "honest about a cursor ahead" wording: a future cursor still reads nothing and is not a gap, but it hands back the head.inputencoding default isbase64, as the handler and its round-trip test already say; the 501 platforms name OpenHarmony beside Windows; the clamp above is stated.Not changed: the changelog note (CONTRIBUTING reserves changelog edits for the merge-time receipts pass; #6370 was that pass and is already on main). No changelog hunks in this PR for the same reason.
No-Issue: post-merge review-bot findings on #6370, verified against main; no tracker issue exists for them (the timeout regression touches #6184's heartbeat but does not close #6184).
Testing
Head
50b4dec(the first head,c6b52ff, failed every non-test lib build onfield child is never readonce the oldDropwas gone; the explicitDropcommit fixes that andcargo check -p codewhale-tui --lib --lockedis clean locally: exit 0, zero warnings).CI on
50b4dec, all green: Lint ✓, Test (ubuntu-latest) ✓, Test (macos-latest) ✓, Test (windows-latest) ✓, cargo check (aarch64-unknown-linux-ohos) ✓, Mobile runtime smoke ✓, Safety gate ✓, npm wrapper smoke ✓, Version drift ✓, Integrations ✓, VS Code ✓, link ✓, CodeQL (rust/python/js/actions) ✓, GitGuardian ✓.Local, tui lib test binary built from this tree (
cargo rustc -p codewhale-tui --lib --profile test --all-features --locked, sealed HOME, proxy unset):core::engine::approval::tests::user_input_deadline_is_not_reset_by_the_wait_heartbeat—test result: ok. 1 passed; 0 failed(0.22 s). Without the fix (the twoawait_user_inputhunks reverted, test kept, rebuilt):test result: FAILED. 0 passed; 1 failed(3.23 s) —panicked … a bounded user-input wait must end at its own deadline: Elapsed(()), i.e. the 200 ms timeout never fired and the 3 s outer guard tripped. File restored; tree clean.core::engine::approval::tests::— 8 passed (includes the Engine silently freezes mid-run: user messages are persisted but never answered; no error, no log line, no crash entry #6184 parked-approval announcement test).sleep_guard::— 2 passed (on the binary carrying the explicitDrop).tools::terminal_session::tests::— 14 passed (bounded replay incl. the new past-the-head clamp, session-level read, resize-reaches-kernel, …).runtime_api::tests::terminal_*— 2 passed (terminal_routes_serve_output_input_resize_and_kill_over_httpagainst a real PTY through the blocking-pool helper; unknown session 404).runtime_api::tests::runtime_info_advertises_terminal_capabilities— 1 passed.runtime_api::tests::thread_event*— 1 passed.Tip re-verification of the Linux MCP startup on this tree's debug TUI (sha
5d00b589e1ec): raw-pty SIGPIPE probe with a required MCP server that exits immediately, 12 launches — 0 SIGPIPE deaths, 12/12 reached the launch card. Cucumber--all-features(55 tests)launch_card_pty::group:test result: ok. 10 passed; 0 failed; 6 ignored(113.8 s).cargo fmt --all -- --checkcargo clippy --workspace --all-targets --all-features --locked(warning-free under the CI allow list) — CI Lint ✓ on50b4deccargo test --workspace --all-features --locked— CI Test ubuntu ✓ / macos ✓ / windows ✓ on50b4decChecklist
with_sessionreplaceslock_sessionin the same module; nothing else is new)🤖 Generated with Claude Code
https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv