Skip to content

0.10.0 release readiness: green CI, #6362 stack fixes, water cadence, Extensions trust review, SIGPIPE-safe MCP startup - #6370

Merged
Hmbown merged 23 commits into
mainfrom
claude/trusting-shannon-sgusdh
Sep 21, 2026
Merged

Hmbown merged 23 commits into
mainfrom
claude/trusting-shannon-sgusdh

Conversation

@Hmbown

@Hmbown Hmbown commented Sep 20, 2026

Copy link
Copy Markdown
Owner

Summary

Release-readiness pass for 0.10.0 on top of main 9b34ab5, whose CI was red on Lint (stale README translations) and on Test macOS/Windows. Final head 752bae3 is green on every leg of the PR run, on an exact-head workflow_dispatch of ci.yml (heavy gates forced), on the OpenHarmony check, and on the exact-head release-candidate run that builds all seven artifact targets.

  • README media: caption and 18 translations resynced and restamped (Lint's README check passes).
  • Native fish poses in widget tests: a test-only counter recognizes both the ASCII and the braille school (fixes four widget tests, makes three "no fish" assertions non-vacuous).
  • bug(tests): configured_model_api_tests overflow the test thread stack and abort the tui lib suite #6362: ConfigFile.base boxed; runtime_store_binding_survives_launch_snapshot_and_resume split into boxed phases on a pinned 2 MiB thread; explicit 2 MiB guard tests. No stack ceilings were raised.
  • Water cadence: the animation tick follows the content-driven draw tier, and the event loop aims its poll at the next tick instead of quantizing to the 48 ms idle poll.
  • Extensions: the plugin trust review stacks on the panel and the row refreshes after the digest is confirmed.
  • feat(runtime-api): terminal byte stream (#34), stream resume + idempotent submit (#76), pet agent-count pin (#12) #6361 merged as itself (owner branch work/0.9.14-core-unblocks, ten commits with their authorship): terminal byte-stream routes, Last-Event-ID resume and idempotent-replay reporting, pet agent-count pin, turn-scoped idle-sleep guard, Engine silently freezes mid-run: user messages are persisted but never answered; no error, no log line, no crash entry #6184 approval/user-input heartbeat. Its three conflicts were generated/measured files and resolve to main's versions. If this PR is squash-merged, feat(runtime-api): terminal byte stream (#34), stream resume + idempotent submit (#76), pet agent-count pin (#12) #6361 needs to be closed by hand with that credit.
  • SIGPIPE kills the session when a stdio peer exits first (root cause of the Linux "blank frames, exited 1" startup): the TUI inherited the dispatcher's SIGPIPE=SIG_DFL, so a stdio MCP server that exits before initialize is written killed the whole process on that write — terminal left in raw mode, nothing on stderr, empty runtime log, and the PTY harness folded the signal death into exit code 1. Reproduced with a raw pty and waitpid: 6 of 12 launches died of SIGPIPE at ~0.4 s. run_tui now ignores SIGPIPE (children still get SIG_DFL from std before exec; non-TUI subcommands keep SIG_DFL). 0 of 12 after the fix. The QA harness prints the killing signal beside the mapped exit code.
  • Non-Unix and Android builds of feat(runtime-api): terminal byte stream (#34), stream resume + idempotent submit (#76), pet agent-count pin (#12) #6361's modules: the 501 terminal stubs are pub(crate), the Unix-only constants and base64 import carry the handler gate, the request structs expect unread fields on ohos/Windows, and sleep_guard's spawn/Command/Stdio carry the macOS/Linux gate of their only callers (Android is Unix but a no-op). sandbox_details is listed as an action row in every_settings_row_reaches_a_store.
  • release-candidate.yml: its web check now uses a full-history checkout, because check:facts derives each model's addedAt from git history (web.yml/ci.yml already pinned depth 0). This had been broken on main since the 2026-09-19 facts-script changes; the last green candidate run predates them.
  • Dead-code budget locked to the measured 174; changelog entries and regenerated derived changelogs.

Closes #6362

Testing

CI on the final head 752bae3 (pull_request run 35515776090): Lint ✓, Test (ubuntu-latest) ✓, Test (macos-latest) ✓, Test (windows-latest) ✓, cargo check (aarch64-unknown-linux-ohos) ✓, Safety gate ✓, Mobile runtime smoke ✓, npm wrapper smoke ✓, Version drift ✓, Integrations ✓, VS Code extension ✓, Workflow lint ✓, android/portable/recorder ✓, CodeQL ✓, GitGuardian ✓, link ✓. Exact-head dispatch of ci.yml with expected_sha=752bae3… (run 35515781290, forces the heavy/workflow/mobile/action gates): success. Exact-head release-candidate run 90 (35515779945): success — source resolve ✓, web surface ✓, artifacts linux-x64 ✓, linux-arm64 ✓, android-arm64 ✓, macos-x64 ✓, macos-arm64 ✓, windows-arm64 ✓, windows-x64 ✓ (21 workflow artifacts, 7-day retention; no tag, release or registry write). Earlier heads: 904314e failed Test ×3 (every_settings_row_reaches_a_store) and ohos (E0364/E0603); 8811455 failed ohos on dead code; 2ca42e9/d5cfae1 failed the release-candidate web check and android build on the two pre-existing defects fixed above.

Local (rustc 1.98.1, Linux x86_64, 4 cores, 14.3 GB cgroup):

  • cargo fmt --all -- --check clean; dead-code budget 174/174; blocking-call budget within budget; scripts/release/check-versions.sh: workspace=0.10.0, npm=0.10.0, npm-binary=0.10.0, lockfile in sync.
  • Rebuilt tui test binary (tree 2ca42e9): every_settings_row_reaches_a_store 1 passed (failed on 904314e); sleep_guard 2 passed; runtime_api::terminal 4 passed; profile_document_parses_within_the_default_test_thread_stack 1 passed; runtime_store_binding_survives_launch_snapshot_and_resume 1 passed; earlier on the branch: tui::widgets::tests:: 161, ambient_life::tests:: 39, config::tests:: 474, views::extensions::tests:: 17, tui::ui::tests:: 777 passed.
  • Raw-pty SIGPIPE probe (python pty.fork + waitpid, /usr/bin/false as a required MCP server): pre-fix binary 6/12 launches killed by SIGPIPE; post-fix 0/12, 12/12 reached the launch card.
  • Real-PTY cucumber suites on the rebuilt debug TUI (all-features, sealed HOME, loopback fixtures, no provider calls): launch_card_pty group 10 passed / 0 failed (twice, old and new harness); launch_mcp_summary_opens_manager_by_click_and_keyboard 3/3 runs; the six opt-in evidence tests (underwater motion, every theme, whale reveal, website capture, settings, populated home) 6 passed in 174.8 s; pre-fix binary also passed work_bar_keys, config_theme_nav, active_composer_pointer, search_text, screen_mode_inline (2), contextual_tips (2), automations_editor (2).
  • Visual evidence: 238 cell captures across 12×40, 16×60, 24×80, 32×100, 40×140 (home, home-mcp, populated home, conversation, confirm, fleet, mcp, models, plugins, providers, settings, statusline, no-color, 20 themes); rendered PNGs inspected for the launch card (whale mark, Underwater default, failed-MCP row), Config, Extensions, route picker, Provider, Status line, Fleet, restored conversation, light themes; a 48-frame / 11.5 s underwater motion GIF from the frame stream.
  • Release build in this container (cargo build --release --locked -j 2 -p codewhale-cli -p codewhale-tui, tree 2ca42e9, 27m30s): codewhale 0.10.0 (dev) sha256 f2b94b88f9fcc2b738fe1f40491e1b8629723a8956877b204bd4431f52a9c769, codewhale-tui 0.10.0 (dev) sha256 f245094a763e6c1643d7d219caa084fa06d4049d0ad33960dd7cb6c1ce54860d. 752bae3 differs from 2ca42e9 only by the workflow file and an Android-only cfg gate, so x86_64-linux compilation is unchanged; the CI-built candidate artifacts for the exact head are the ones on run 90.

Not verified here: native-terminal look (renders are headless-Chromium cell renders), Kitty /pet (#6155), Computer Use on macOS (#5856), any paid-provider turn, #6184 reproduction.

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets --all-features --locked — CI Lint ✓ on 752bae3
  • cargo test --workspace --all-features --locked — CI Test ubuntu/macos/windows ✓ on 752bae3

Checklist

🤖 Generated with Claude Code

https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv

CodeWhale Bot and others added 18 commits September 18, 2026 19:25
App-side issue #12 reports the pet showing only single-agent state and asks
whether the owner emits `activity["parallel"]` at all. It does — the count is
derived JS-side from `agent:`-prefixed spans. The chain, traced end to end
before writing anything:

- the engine emits `Event::AgentSpawned/Progress/Complete`, and
  `tui/ui/event_loop.rs:1946` gates them on the owning session before calling
  `pet_watch::observe`;
- `metadata()` (`pet_watch/mod.rs:207`) allowlists all three variants and
  forwards `event`, `id` and `worker_status`;
- the JS worker dispatches `agent_spawned` into `start(`agent:${id('id')}`)`
  (`pet_watch/pet-native.js:1796`) and counts `agent:`-keyed spans into
  `parallel` (`pet-native.js:1675`);
- `Scene.activity.parallel` is what the caption renders as "· ×N"
  (`pet_watch/mod.rs:469`).

So there is no producer to write: adding one would be a second authority for
a count that already flows. The JS half is already covered by
`pet/tests/pet-engine.test.mjs` (`assert.equal(frame.activity.parallel,3)`,
gated by `.github/workflows/pet.yml`). What no test covered was the Rust half
of the agent path — `metadata()` is tested for tool, thinking and message
events only — and that is the half that fails silently: a trimmed allowlist
or a dropped id zeroes the count with no error and no log line.

Verification:

- `scripts/dev-test.sh crates/tui/src/tui/pet_watch/mod.rs agent_events_forward`
  -> `Summary [0.028s] 1 test run: 1 passed, 12891 skipped`
- Proven to catch the failure it names, not just to pass: deleting
  `| Event::AgentSpawned { .. }` from the allowlist fails it with
  `panicked at crates/tui/src/tui/pet_watch/mod.rs:761:10: agent spawns are
  observed` -> `0 passed; 1 failed`. The allowlist was restored; `git diff`
  holds only this test.
- `scripts/dev-test.sh crates/tui/src/tui/pet_watch/mod.rs pet_watch`
  -> `Summary [0.980s] 17 tests run: 17 passed, 12875 skipped`
- `cargo fmt --all -- --check` exit 0
- `cargo clippy -p codewhale-tui --all-targets --locked -- -D warnings -A clippy::uninlined_format_args -A clippy::too_many_arguments -A clippy::unnecessary_map_or` exit 0

The app-side half of #12 — rendering, and whether the visualization itself
changes beyond the caption — stays in codewhale-app; this pins the contract
it consumes.
App-side issue #34 asks Core for "authenticated byte input/output, resize,
exit and bounded replay", and is explicit that the inspected stateful terminal
path is Unix-gated and that Core work must be linked before those paths are
claimed. Recon of crates/tui/src/tools/terminal_session.rs found the owner had
none of the four: no resize (the 24x120 PtySize was fixed at openpty and the
master was dropped after the reader/writer clones were taken), no kill, and a
512 KiB ring whose only reader was the consuming tool-result cursor.

This lands the primitives that contract needs, in the file's existing
free-function style:

- the pty master is retained on the session, so `resize_session` reaches the
  kernel's window size;
- `OutputChunk` + `OutputBuffer::read_since` read from an *absolute* cursor
  without consuming, so two readers replay the same bytes and a repeated read
  is idempotent. A cursor the ring has moved past sets `gap` instead of
  silently answering from the middle of the stream — report, not repair;
- `session_exit_status` polls the child (None = still running) and
  `kill_session` terminates it, so a dead shell stops looking alive;
- `read_session_since` clamps one response to READ_LIMIT (64 KiB) over the
  bounded ring;
- `take_output` now uses that same cursor arithmetic instead of its own copy of
  it, behaviour unchanged (its existing tests cover it).

Known limitation, recorded on `read_session_since`: nothing re-reads dropped
bytes from disk — the durable record is identity and lifecycle, never output —
and no route consumes these yet. The Engine byte-stream route is the next
slice; this is the owner work it needs.

Verification (macOS aarch64, this worktree):

- `scripts/dev-test.sh crates/tui/src/tools/terminal_session.rs terminal_session`
  -> `Summary [2.329s] 15 tests run: 15 passed, 12881 skipped`, including:
  * `resize_reaches_the_kernel_and_the_live_shell` — asserts the kernel's own
    `get_size`, then that the live shell reports `40 100` through `stty size`;
  * `session_read_since_is_non_consuming_and_clamped` — a repeat read at the
    same cursor returns identical bytes; a >64 KiB stream clamps to READ_LIMIT;
  * `bounded_replay_is_absolute_non_consuming_and_reports_its_gap` — past a
    wrapped ring the chunk reports `oldest_cursor` 32 and `gap` true;
  * `killed_shell_reports_an_exit_status`.
- `cargo fmt --all -- --check` exit 0
- `python3 scripts/check-dead-code-budget.py` -> `PASS: 185 attributes, exactly at budget.`

Not claimed: Windows. This path is `#[cfg(unix)]` end to end and every test
here is `cfg(all(test, unix))`; ConPTY qualification is its own slice, as the
ticket itself says.
The terminal owner gained byte replay, resize and exit in the previous commit;
this is the consumer that makes them reachable — and, per the compiler, the
consumer that makes them live code rather than scaffolding. `/v1` auth is the
route layer's bearer token; nothing here re-implements or bypasses it.

- `GET /v1/terminal/{name}/output` — the resumable byte stream, wire-shaped
  like the jobs stream on purpose (`cursor` / `max_bytes` / `format` in,
  `offset` / `next_cursor` / `total` / `dropped` out) so two byte streams in
  one product do not speak two dialects. Reads never consume: several clients
  can hold independent cursors, and polling never steals output from the
  agent's own consuming read.
- `POST /v1/terminal/{name}/input` — bytes into the live session, `text` or
  `base64`, bounded per frame. Input stays attributable by route: this is the
  client's writer, `terminal_send` is the agent's.
- `POST /v1/terminal/{name}/resize` — the window the child draws for.
- `POST /v1/terminal/{name}/kill` — end the shell. The exit itself is read
  from the stream (`running` / `exit_code`), not from the acknowledgement.
- Routes attach to shells the Engine already owns and never create one: a name
  with no live session is `404`. An HTTP request must not be able to conjure a
  shell the Engine does not know about.
- `RuntimeCapabilities` gains `terminal_stream`, `terminal_input`,
  `terminal_resize` and `terminal_kill`, set from `cfg!(unix)` so the Windows
  build advertises `false` for all four. A client gates its pane on the flag
  instead of discovering the gap from a failed request; the Windows routes
  answer `501` (the owner is Unix-only end to end) so "this build cannot do
  terminals" is distinguishable from "that session is gone".
- `docs/RUNTIME_API.md` documents the family in the GPUI section, next to the
  jobs stream, including the four stated limitations: no `wait_ms` long poll,
  no scrollback recovery, live sessions only (a restarted Engine reports no
  session rather than pretending to reattach), and no runtime-sdk wrapper yet.

Verification (macOS aarch64, this worktree):

- `scripts/dev-test.sh crates/tui/src/runtime_api/tests.rs terminal_routes_serve`
  -> `Summary [0.208s] 1 test run: 1 passed` — the full seam over HTTP against
  a real PTY: input through the route executes in the Engine's own session, the
  shell's output comes back through the route with an absolute cursor, a repeat
  read at the same cursor returns identical bytes, resize is confirmed by the
  shell's own `stty size` reporting `40 100` (so a handler that only stored the
  numbers would fail this), and kill is observed as `running: false`.
- `... tests.rs terminal_output_for_an_unknown` -> `1 test run: 1 passed`
  (404 for an unknown session and for an over-long name).
- `... tests.rs terminal_capabilities` -> `1 test run: 1 passed`.
- `... crates/tui/src/tools/terminal_session.rs terminal` -> `360 tests run:
  360 passed, 12543 skipped`.
- `... crates/protocol/src/runtime/mod.rs runtime::` -> `10 tests run: 10 passed`.
- `cargo clippy -p codewhale-tui -p codewhale-protocol --all-targets --locked -- -D warnings ...` exit 0. This is the gate that failed before the route
  existed: the previous commit's primitives were dead code without a consumer.
- `cargo fmt --all -- --check` exit 0;
  `check-blocking-calls-budget.py` -> `601 sites across 177 files, within budget`;
  `check-dead-code-budget.py` -> `PASS: 185 attributes, exactly at budget`.

Not verified: Windows. The owner is `#[cfg(unix)]`, the routes answer 501 there,
and ConPTY qualification remains its own slice — the ticket says as much.
Reviewing my own tests before CI spent a Windows cycle on them: two of the
three new ones would have failed the required `Test (windows-latest)` job,
because on Windows these routes answer `501` and every terminal capability is
`false` by design.

- `runtime_info_advertises_terminal_capabilities` now asserts
  `cfg!(unix)` rather than `true`, which also makes it a real assertion on
  Windows: the flag must not claim a capability the build cannot serve.
- `terminal_output_for_an_unknown_session_is_not_found_and_creates_nothing`
  is `#[cfg(unix)]`: the 404-not-501 distinction only exists where the routes
  serve bytes.
- The five request helpers (`chunk_encoding`, `encode_bytes`, `decode_bytes`,
  `bounded_max_bytes`, `bounded_dimension`) are `#[cfg(unix)]` too — they are
  reached only by the Unix handlers, and leaving them ungated would have made
  them dead code on Windows under `-D warnings`.

Verification: `cargo check -p codewhale-protocol --target
x86_64-pc-windows-msvc --locked` exit 0, so the capability fields are
portable. A full `codewhale-tui` check for Windows cannot run from macOS —
`ring`'s build script needs a Windows C toolchain — so the Windows leg of
this branch remains CI's to prove, and the Windows compile of `terminal.rs`
is the one thing here I could not verify locally.
…76)

The app-side ticket asks for resumable event streaming with sequence
acknowledgements. The durable half already existed — per-thread `seq`, a JSONL
event log, `since_seq` replay with a bounded tail — but nothing on the wire let
a browser-style client use it: every SSE frame was written without an `id:`,
so an `EventSource` had nothing to resume from, and the route only read the
query cursor.

- Journal frames now carry their durable `seq` as the SSE event id (the three
  yield sites in `replay_live_thread_events`). Ids ride journal frames only:
  the `stream.progress` frames are transport progress, not events, and giving
  them an id would invite a client to resume from a point it never received.
- `stream_thread_events` reads `Last-Event-ID` and uses it as the cursor when
  no explicit `since_seq` was asked for. An explicit query cursor wins, so a
  deliberate replay-from-zero is never silently overridden by a stale header.
- `last_event_id` accepts only a decimal sequence number. An opaque id from a
  proxy or an older client starts the stream from the durable head instead of
  failing to open it — a refused stream looks like an outage to a reconnecting
  client.
- `RuntimeCapabilities` gains `event_stream_resume`, so the app can gate its
  reconnect controls on the capability rather than discovering it from a
  missing id.

Not in this commit, deliberately: the idempotent-submission half of #76. The
`operation_key` mechanism already exists with a lookup route; what is missing
is surfacing a replay as a replay on `POST /v1/threads/{id}/turns` (it answers
`201` either way today) and having app-server mint the key. That is its own
slice, and this one is already verifiable on its own.

Verification (macOS aarch64, this worktree):

- `scripts/dev-test.sh crates/tui/src/runtime_api/tests.rs thread_event_frames_carry`
  -> `Summary [0.231s] 1 test run: 1 passed` — the first frame's `id:` equals
  its payload `seq`; a reconnect with only `Last-Event-ID` lands on the next
  durable event; `?since_seq=0` with a stale header still replays from zero.
- `scripts/dev-test.sh crates/tui/src/runtime_api/tests.rs last_event_id_accepts`
  -> `1 test run: 1 passed` (absent, padded, and opaque ids).
- The two tests this route already had still pass unchanged:
  `events_endpoint_respects_since_seq_cursor` and
  `event_handoff_replays_and_dedupes_interaction_prompts_without_a_gap`
  (`1 test run: 1 passed` each).
- `scripts/dev-test.sh crates/tui/src/runtime_api/tests.rs event`
  -> `170 tests run: 170 passed, 12735 skipped`.
- `... crates/protocol/src/runtime/mod.rs runtime::` -> `10 tests run: 10 passed`.
- `cargo clippy -p codewhale-tui -p codewhale-protocol --all-targets --locked -- -D warnings -A clippy::uninlined_format_args -A clippy::too_many_arguments -A clippy::unnecessary_map_or` exit 0 · `cargo fmt --all -- --check` exit 0
- `check-blocking-calls-budget.py` PASS · `check-dead-code-budget.py` PASS
…ion (#76)

The second half of #76: an ambiguous submit must be resolvable by operation
lookup. The durable machinery already existed — `operation_key` is validated,
fingerprinted and bound, and a replay returns the original turn — but the
answer was indistinguishable from a fresh admission: `POST
/v1/threads/{id}/turns` answered `201` either way, so a client that retried
after a dropped response could not tell whether it had created a second turn
or been handed the one it already had.

- The admission path now reports the disposition. `start_turn_with_source`
  returns `(TurnRecord, bool)`; both replay returns (the pre-claim lookup and
  the recheck under the claim lock) report `true`, the tail reports `false`.
  `start_turn` and `start_turn_from_stored_images` keep their existing
  signatures, and `start_turn_reporting_replay` exposes the pair, so the 94
  existing `start_turn` callers are untouched.
- The route answers `200 { ..., idempotent_replay: true }` for a replay and
  keeps `201` for a new admission, following the Agent Mail precedent. The
  flag is omitted on a fresh admission, so every response an existing client
  already parses is byte-identical.

Not in this commit: app-server minting an `operation_key` for its own
submissions. That is the client half, it lands in the app lane, and the
capability it needs (`turn_operation_idempotency`, `turn_operation_lookup`)
is already advertised.

Verification (macOS aarch64, this worktree):

- `scripts/dev-test.sh crates/tui/src/runtime_api/tests.rs turn_endpoint_operation_key`
  -> `Summary [0.259s] 1 test run: 1 passed`. That test already existed and
  asserted the old `201` for a replay; it now pins the stronger contract —
  `200`, `idempotent_replay: true`, the original turn id, `409` on a changed
  request with the same key, exactly one `SendMessage`, and exactly one turn.
  It also asserts a fresh admission carries no flag.
- The paths the signature change touches:
  `turn_operation` -> `5 tests run: 5 passed`;
  `start_turn` -> `5 passed`;
  `agent_mail` -> `6 passed`;
  `thread_goal` -> `5 passed`;
  `steer` -> `33 passed` (12900 skipped in each filter).
- `cargo clippy -p codewhale-tui -p codewhale-protocol --all-targets --locked -- -D warnings -A clippy::uninlined_format_args -A clippy::too_many_arguments -A clippy::unnecessary_map_or` exit 0 · `cargo fmt --all -- --check` exit 0
- `check-blocking-calls-budget.py` PASS · `check-dead-code-budget.py` PASS
…ight

Issue #2990 ("Active turn dies ... when the computer sleeps") was fixed in
v0.8.57 by detecting the suspend on wake and re-issuing the request
(`core::engine::streaming::sleep_gap_detected`). That survives a suspend; it
does not stop one. An unattended machine still idles into sleep mid-turn, and
a turn that outlives the idle timer is lost work with no error line.

This holds the platform's idle-sleep assertion for exactly as long as a turn:

- macOS: `caffeinate -i`
- Linux: `systemd-inhibit --what=idle --why="Codewhale turn in flight"
  --mode=block sleep infinity`
- other Unix: no inhibitor this module knows
- Windows: not implemented, deliberately. `SetThreadExecutionState` is
  thread-affine — the release has to happen on the thread that set it, which a
  guard travelling with a turn cannot promise. An untested holder that might
  never release would keep a laptop awake forever, which is worse than the
  problem this solves.

Release is `Drop`, and no guard is ever cached: a leaked inhibitor is worse
than the sleep it prevents. The guard rides the existing
`terminal_chrome_enabled` gate — the same one that already decides host-facing
chrome — so an interactive TUI turn holds it while headless hosts (`exec`,
app-server, CI) never do. No new config key; a dedicated `[tui]` opt-out is
stated as not-implemented in the module and in docs.

What it does not do, recorded next to the behaviour in
`docs/ENVIRONMENTS.md`: it does not defeat an explicit `sleep` / `pmset
sleepnow`, a closed lid, or a low battery, and it cannot run while the host is
suspended.

Verification (macOS aarch64, this worktree):

- The mechanism at the OS level, which is the part my code depends on:
  `pmset -g assertions` reports `PreventUserIdleSystemSleep` 0 -> 1 while a
  `caffeinate -i` is held. (The host also carries an unrelated
  `caffeinate -s -w 4908` from the user's own `deeprich` supervisor, which is
  why no sleep events appear in `pmset -g log`; that process is not ours.)
- `scripts/dev-test.sh crates/tui/src/sleep_guard.rs sleep_guard`
  -> `Summary [0.030s] 2 tests run: 2 passed`: the inhibitor is alive while the
  guard lives and gone after the drop — asserted through `kill(pid, 0)`, so the
  test cannot perturb the process it measures — and two guards own two
  independent processes, so the first drop releases only its own.
- `turn_loop` -> `67 tests run: 67 passed`; `engine::tests::turn`
  -> `20 tests run: 20 passed`.
- `cargo clippy -p codewhale-tui --all-targets --locked -- -D warnings -A clippy::uninlined_format_args -A clippy::too_many_arguments -A clippy::unnecessary_map_or` exit 0 · `cargo fmt --all -- --check` exit 0
- `check-blocking-calls-budget.py` PASS · `check-dead-code-budget.py` PASS

Not verified: Windows (no implementation) and the interactive TUI end to end —
the guard is exercised at its own boundary, and a real turn needs a provider.
…g list

`main` is red on `Lint` and `Test (macos-latest)`. Neither is visible in a PR
rollup: `check-runtime-contract-budget` is advisory on pull requests and fatal
on push, and the macOS job that fails is not one of the three required checks.
So every PR since looked green while main carried both.

**Lint — an unrecorded identity change.** The checker refuses identity drift by
design, and `--update` cannot paper over it (`compare` raises before the update
path runs), so this is the explicit maintainer edit the file's own header asks
for. Two changes moved it:

- `execute_tools` enters every catalog outside Plan (`tool_catalog.rs:350`,
  from code-mode Phase 1 `e23ce514c`), so the Act and Operate full tool names
  and identity digests move with it.
- The `agent` tool advertises its `cwd` parameter (the subagents `cwd` move),
  growing the shared active surface by 256 schema bytes / 64 estimated tokens
  everywhere that tool appears — including Plan full, which is why that surface
  grew without gaining a tool.

The `_comment` history records both, measured from the release train, and the
14 raised ceilings are the measured values (0 decreased; 55 metrics exactly at
budget afterwards).

**Test (macos-latest) — a real race, now proven fixed.** `threads_running_lists_active_turns_and_clears_on_settle`
forces a synthetic settle into the durable store, but the engine still owns
that record and can persist its own status afterwards — the listing is read
from the store, so a later engine write puts the turn back in flight and the
single read after the write fails. That is not a product defect: the engine is
entitled to finish its turn. The assertion now polls until the settle wins
(deadline `ci_scaled(5s)`, so a genuinely stuck turn still fails), which is how
the rest of this suite already handles async settling.

Verification (macOS aarch64, this worktree):

- The race is reproduced deterministically, not inferred: with the record
  flipped back to `InProgress` after the test's write and settled 300ms later,
  the previous single read fails with the CI shape verbatim —
  `left: Array [Object {..., "active_turns": Array [Object {"turn_id": ...,
  "status": String("in_progress")}]}]`, `right: Array []` — while the polling
  version passes the identical injection (0.549s: it waited for the settle).
  The injection was reverted; the commit holds only the fix.
- `scripts/dev-test.sh crates/tui/src/runtime_api/tests.rs threads_running_lists_active_turns`
  -> `Summary [0.224s] 1 test run: 1 passed`
- `python3 scripts/check-runtime-contract-budget.py`
  -> `[runtime-contract-budget] PASS: all 55 metrics are exactly at budget.`
- `cargo clippy -p codewhale-tui --all-targets --locked -- -D warnings -A clippy::uninlined_format_args -A clippy::too_many_arguments -A clippy::unnecessary_map_or` exit 0 · `cargo fmt --all -- --check` exit 0

Not verified: the macOS job itself, which only CI can run; the proof here is
that the injected CI failure shape no longer fails.
…ly (#6184)

Issue #6184 is an engine that "silently freezes mid-run: user messages are
persisted but never answered; no error, no log line, no crash entry". Recon for
the instrumented hunt named the prime suspect: the tool-approval wait in
`core/engine/approval.rs` had no engine-side deadline, the turn wall clock is
*paused* across it so no budget ever fires, the approval card only expires when
a view is top-of-stack, and the wait logged nothing — so a turn parked there is
indistinguishable from a working one until the user gives up.

Both waits now carry a heartbeat:

- `await_tool_approval` and `await_user_input` tick every `WAIT_HEARTBEAT`
  (60s; 50ms under `cfg(test)` so the real path is observable without waiting a
  minute) and log a `tracing::warn!` naming the tool and the elapsed time.
- The first heartbeat also sends `Event::Status`, so the user sees "Still
  waiting for tool approval on `<tool>` after Ns" once rather than a frozen
  screen. Later heartbeats keep the log trail without refilling the transcript.
- The message comes from one `wait_announcement` helper, so the log line and
  the status event cannot drift apart.

The user-input wait matters most in the case #6003 already allows:
`user_input_timeout_seconds = 0` means wait indefinitely, and nothing bounded
or reported that wait at all.

Verification (macOS aarch64, this worktree):

- `scripts/dev-test.sh crates/tui/src/core/engine/approval.rs a_parked_approval_announces`
  -> `Summary [0.189s] 1 test run: 1 passed` — a new test drives the real fixture
  to the approval gate, answers nothing, and asserts the announcement names both
  the wait and the tool.
- Proven to catch the absence of the feature, not just to pass: disabling the
  `Event::Status` send makes that test fail after its 5s deadline
  (`FAIL [5.147s] ... panicked at approval.rs:554`). The mutation was reverted.
- `... approval` (crate-wide) -> `283 tests run: 283 passed`; `turn_loop`
  -> `67 tests run: 67 passed` — the added event does not disturb the existing
  approval and turn-loop assertions.
- `cargo clippy -p codewhale-tui --all-targets --locked -- -D warnings -A clippy::uninlined_format_args -A clippy::too_many_arguments -A clippy::unnecessary_map_or` exit 0 · `cargo fmt --all -- --check` exit 0
- `check-blocking-calls-budget.py` PASS · `check-dead-code-budget.py` PASS ·
  `check-runtime-contract-budget.py` PASS

Still open from the same recon (next slices, not this commit): the event-channel
send that can pend when the UI stops draining, the shell-permit wait, and the
steer queue that nothing drains while the engine is parked.
…bility

`cargo check (aarch64-unknown-linux-ohos)` failed on this branch with
`error[E0432]: unresolved import crate::tools::terminal_session`
(runtime_api/terminal.rs:37). The cause is a cfg mismatch I introduced: the
owner module is gated `#[cfg(not(target_env = "ohos"))]` in `tools/mod.rs`
while its functions are `#[cfg(unix)]` — and ohos *is* unix, so "the owner's
functions exist" is `unix AND not-ohos`, not `unix`.

- Every item that drives the owner is now gated
  `#[cfg(all(unix, not(target_env = "ohos")))]`.
- The 501 stubs cover `any(not(unix), target_env = "ohos")`, so ohos gets the
  honest "this build cannot do terminals" answer instead of a resolution error,
  and the route registration in `runtime_api.rs` keeps resolving.

Verification (macOS aarch64, this worktree):

- `cargo check -p codewhale-tui --all-targets --locked` exit 0
- `./scripts/release/check-ohos-deps.sh`
  -> `OHOS dependency graph OK for codewhale-tui on aarch64-unknown-linux-ohos.`
  (plus the linker-wrapper and rquickjs feature edges), exit 0
- `scripts/dev-test.sh crates/tui/src/runtime_api/terminal.rs terminal`
  -> `360 tests run: 360 passed, 12548 skipped`
- `cargo fmt --all -- --check` exit 0 · clippy `--all-targets -D warnings` exit 0

Not verified locally: the ohos *build* itself. `cargo check --target
aarch64-unknown-linux-ohos` cannot run from macOS — `ring` and
`libsqlite3-sys` need a cross C toolchain — and simulating the cfg with
`RUSTFLAGS='--cfg target_env="ohos"'` fails inside `libc`, which keys off that
cfg. CI's ohos job is the receipt for this fix.
…tions

The English README already showed the d7a9a1c 0.10.0 development capture
but still captioned it as a v0.9.12 build, and all 18 translations kept the
old 171acee image and caption, so the README translation lint failed on
main. Point every locale at the same capture, describe it as the v0.10.0
development build it is, and restamp each translation with the current
README.md hash.

Validation: python3 scripts/check-readme-translations.py -> 18 translations
in sync (sha256:29c349b6f2b4); bash scripts/check-readme-locales.sh PASS;
./scripts/release/check-versions.sh --range-audit-advisory -> Version state
OK.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv
ad20493 replaced the ASCII fish school with cached braille dot poses, but
five widget tests still looked for `><>` / `<><` bodies: the launch-water
test failed on macOS and Windows CI ("got 0"), three siblings failed the
same way, and the "no fish" assertions passed vacuously. Add a test-only
counter in ambient_life that recognizes both silhouette families (the
ASCII bodies of CODEWHALE_ASCII_SAFE=1 and every native pose) and assert
through it. The native poses carry no eye, so the eyed-lead check is gone
with the eye.

Validation (rustc 1.98.1, default 2 MiB test stack):
tui::widgets::tests:: -> test result: ok. 161 passed; 0 failed
ambient_life::tests:: -> test result: ok. 39 passed; 0 failed

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv
…iB stack (#6362)

Two distinct overshoots, measured with gdb frame attribution on the
default libtest thread rather than papered over with RUST_MIN_STACK:

- `ConfigFile { base: Config }` carried the multi-kilobyte Config by value
  through `toml::de`'s flatten visitor and `apply_profile`. Box the base.
  The three `configured_model_api_tests` members named in #6362 now pass
  on a 2 MiB stack, and a new guard parses a profile document on an
  explicit 2 MiB thread so CI's 16 MiB export cannot mask a regression.
- `runtime_store_binding_survives_launch_snapshot_and_resume` was one
  async body whose poll frame alone held 1.14 MiB of debug temporaries
  (boxed App returns, Config clones, two snapshots, task-manager futures)
  on top of 0.39 MiB of pinned state and ~0.43 MiB for App construction.
  Split it into phases built and boxed through a new `boxed_phase`
  helper (inline `async {}` phases still left 806 KiB of never-reused
  full-size future slots on the outer frame) so each phase's temporaries
  die with its own poll frame, and run it through a new
  `block_on_default_test_stack` helper that pins the 2 MiB budget instead
  of inheriting CI's.

CI keeps its RUST_MIN_STACK for the rest of the suite; these two tests
are the ones that now enforce the default budget on the paths reported.

Validation (rustc 1.98.1):
RUST_MIN_STACK=2097152 config::tests:: -> ok. 474 passed; 0 failed
RUST_MIN_STACK=2097152 declared_model_posts_do_not_preserve_alias_at_wrong_endpoint -> ok. 1 passed
RUST_MIN_STACK=2097152 declared_model_posts_preserve_exact_identity_after_reload -> ok. 1 passed
RUST_MIN_STACK=2097152 explicit_runtime_selections_migrate_legacy_memory_into_config_once -> ok. 1 passed
runtime_store_binding:: (the test pins its own 2 MiB thread) -> ok. 11 passed; 0 failed
RUST_MIN_STACK=2097152 runtime_store_binding_survives_launch_snapshot_and_resume -> ok. 1 passed
Before the split, the same test aborted at 2 MiB ("has overflowed its
stack") and passed at 3 MiB.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv
…t tick

The event loop asked for ocean frames every 80 ms while the frame limiter,
on the Atmosphere tier, only drew every 120 ms, so idle water spent one
wake in three requesting a frame the limiter then held. Separately, nothing
scheduled the poll for the next animation deadline: the tick only ran when
the 48 ms idle poll happened to return, which quantized an 80 ms cadence to
96 ms and a 120 ms one to 144 ms.

Read the content-driven cadence tier once per frame and feed it to both
the tick and the limiter: with only ambience moving the tick lands exactly
on the atmosphere interval; while streaming or typing the authored 80 ms
ocean cadence rides inside the interactive cap as before. Arm the existing
FrameRequester with `request_at` for the next tick so the poll wakes on
time, and consume a stale request once motion stops so an orphaned
deadline cannot pin the poll at zero. Ghostty, constrained (tmux/SSH),
reduced-motion and still behaviour are unchanged, as is the six-second
idle settle.

Validation (rustc 1.98.1): underwater_motion_keeps_its_smoother_cadence_
during_live_status, ghostty_caps_underwater_motion_without_slowing_
interaction, underwater_motion_ticks_only_for_visible_unobscured_owners
-> 1 passed each; display_refresh:: 13 passed; motion:: 7 passed;
frame_requester 3 passed; tui::ui::tests:: 776 passed, 1 failed
(mcp_login_stalled_discovery: passes with the container's HTTPS_PROXY
unset; loopback routed through the sandbox proxy, not a product change).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv
Inspecting an unreviewed plugin from Extensions closed the panel before
opening the exact-content review, so confirming the digest left the
person in the transcript with no view of the row they had just trusted.
The review now stacks on the panel (InPlace, like the other in-place rows),
and when a command confirmed from a stacked review lands back on the
Extensions panel the host re-reads the inventory, so the row reports
"trusted" and offers Enable instead of the stale "not reviewed".

Trust itself is unchanged: the same reviewed `/plugin trust <name> <digest>`
command, the same fail-closed token, no new mutation path.

Validation (rustc 1.98.1): views::extensions::tests:: 17 passed;
"extensions" filter 23 passed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv
scripts/check-dead-code-budget.py reported 174 attributes against a
budget of 185 and asked for the ratchet. Lock it in.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv
Add the Extensions trust-review flow and the water cadence change under
Changed, and the #6362 stack fixes under Fixed; regenerate the embedded
crates/tui/CHANGELOG.md (scripts/sync-changelog.sh) and the website's
changelog.generated.ts (web/scripts/derive-changelog.mjs).

Validation: npm --prefix web test -> 51 files, 471 tests passed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv
…branch

Brings the owner's Core unblocks in as themselves: the Engine terminal
byte-stream routes (output/input/resize/kill, Unix-only, 501 elsewhere and
on ohos), Last-Event-ID stream resume plus idempotent-replay reporting on
turn submission, the pet agent-count contract pin, the turn-scoped host
idle-sleep guard, and the #6184 approval/user-input wait heartbeat.

Conflicts (3, all generated or measured files) resolved to main's newer
versions, which the branch's own commits never intended to change:
docs/public-surface-facts.json and web/lib/facts.generated.ts (0.10.0
capture facts), and scripts/runtime-contract-budget.json (main's 2026-09-19
Linux lock-in already carries the agent cwd / execute_tools ceilings the
branch measured; every metric value is identical, only the _comment
history differed).

Gates on the merged tree: cargo fmt --check clean; blocking-call budget
603 sites within budget; dead-code budget 174 at budget; OHOS dependency
graph OK. Rust tests for the merged code run in the next verification
pass on this branch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv
… as an action

Three failures on the merge head 904314e, one per CI leg:

- OpenHarmony `cargo check` and Windows: the `platform` stubs that answer
  the terminal routes with 501 were `pub(super)` inside their own module,
  so the module-level `pub(super) use` re-export was wider than the items
  (E0364/E0603 at every `terminal::terminal_*` route). They are
  `pub(crate)` now, and the `base64::Engine` import only exists on the
  Unix build that encodes bytes.
- Windows: `sleep_guard` imported `Child`, `Command` and `Stdio` on a
  platform where every user of them is `#[cfg(unix)]`, which
  `-D warnings` turns into an error. The import carries the same gate.
- macOS/Ubuntu: `sandbox_details` is an action row (opens `/status`),
  like `mcp_open` and `plugins_open`, so `every_settings_row_reaches_a_store`
  lists it with the other rows that `settings.toml` does not take.

Validation: `cargo fmt --all -- --check` clean; dead-code and
blocking-call budgets unchanged. All three failures are judged by CI on
this head; the ohos and Windows toolchains are not available here, and
the local run of the settings test is recorded on the PR.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv
…ts first

The dispatcher resets SIGPIPE to SIG_DFL so `codewhale doctor | head`
exits quietly (#4030). The interactive session inherited that, and it
writes to pipes whose far end it does not own: stdio MCP servers, shell
tools, hooks, LSP. A stdio MCP server that exits before `initialize` is
written kills the whole TUI on that write, with the terminal left in raw
mode, nothing on stderr, and an empty runtime log. That is the Linux
"blank frames, exited 1" startup: the PTY harness folds a signal death
into exit code 1.

Reproduced on the launch-card fixture (`/usr/bin/false` as a required
MCP server) with a raw pty and `waitpid`: 6 of 12 launches died of
SIGPIPE (13) about 0.4 s in, before the first frame; under strace the
child was slow enough that the write won and every launch drew.

`run_tui` now sets SIGPIPE to SIG_IGN once the terminal checks pass, so
the failed write returns `EPIPE` and the MCP client reports the server
as failed like any other transport error. Children are unaffected: the
standard library resets SIGPIPE to SIG_DFL before exec, so `| head`
inside a shell tool still terminates the way a shell expects. Non-TUI
subcommands keep SIG_DFL.

The QA PTY harness now prints the killing signal beside the mapped exit
code (`observed_exit=Some(1) signal=Some("Broken pipe")`), so the next
signal death does not read as a deliberate exit.

Validation: the raw-pty probe above is the reproduction (6 of 12
launches killed by SIGPIPE on the unfixed binary, sha db798d8b). The
post-fix probe and the `launch_mcp_summary_*` / `workbench_*_visual_evidence`
PTY runs on the rebuilt binary are recorded on the PR.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv
…uilds

The OpenHarmony check on 8811455 got past the re-export and stopped on
dead code: the four size constants are read only by the Unix handlers,
and the three request bodies are deserialized by the 501 stubs but never
read there, which `-D warnings` rejects on ohos and Windows.

The constants carry the same Unix gate as the handlers. The request
structs stay on every platform, since they are the route contract the
stubs answer, and expect their fields to be unread on the non-Unix
build; `expect` rather than `allow` so a future stub that does read
them fails the build instead of leaving a stale attribute (and it stays
outside the dead-code budget).

Validation: `cargo fmt --all -- --check` clean; dead-code budget
unchanged at 174. The ohos and Windows legs are judged by CI on this
head; neither toolchain is available here.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv
…derived from

The exact-head release-candidate run on 2ca42e9 failed in "Verify exact
candidate web surface": `check:facts` reported the committed
`facts.generated.ts` as stale with every model's `addedAt` collapsed to
2026-09-20. The web job checked out at the default depth 1, and
`web/scripts/facts-lib.mjs` derives `addedAt` from the commit on which
each model id first appeared in the declaration paths, so a shallow
checkout can only ever answer "today". web.yml and ci.yml already pin
`fetch-depth: 0` for exactly this reason; the last green candidate run
(run 87, 2026-09-14) predates the 2026-09-19 facts-script changes, so
this has been broken on main since then and is not specific to this
branch.

Validation: workflow YAML parses; the same checkout setting is what the
green web.yml check job uses. Judged by re-dispatching
release-candidate.yml on this head.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv
…ixes

The release-candidate android-arm64 build on d5cfae1 failed with
`function spawn is never used`: Android is Unix but neither macOS nor
Linux, so `start_inhibitor` is the no-op arm and nothing calls `spawn`,
which `-D warnings` rejects. The helper and the `Command`/`Stdio`
imports now carry the same macOS/Linux gate as their only callers;
`Child` stays Unix-wide for the field and the Drop. Present on main
since 919d602 (2026-09-19); the last green candidate run predates it.

Validation: `cargo fmt --all -- --check` clean; Linux compilation is
unchanged. Judged by re-dispatching release-candidate.yml on this head.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv
@Hmbown
Hmbown marked this pull request as ready for review September 21, 2026 00:01
Copilot AI lite review requested due to automatic review settings September 21, 2026 00:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 21, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-21T00:03:36.530861Z 752bae3 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Hmbown
Hmbown merged commit 056eb55 into main Sep 21, 2026
67 of 69 checks passed
@Hmbown
Hmbown deleted the claude/trusting-shannon-sgusdh branch September 21, 2026 00:02
@claude

claude Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 5m 16s —— View job


Claude's review of #6370

Todo list

  • Gather context (PR is already merged into main; pulled the full diff via gh pr diff 6370)
  • Understand the request (code review per repo custom instructions)
  • Review correctness, provider/model/route safety, reuse/simplification, tests, security
  • Post findings

Summary

Reviewed the full diff (51 files, +2241/-330) against the priority order in this repo's review instructions. This is a well-scoped, well-tested release-readiness PR — most changes (README resync, the #6362 Box<Config> fix, the cadence-tier dedup in event_loop.rs/motion.rs, the SSE Last-Event-ID resume, the terminal HTTP routes, the #6184 approval heartbeat) are correct, have matching tests, and document their own known limitations per the repo's convention. One finding worth fixing:

1 finding — posted inline

  • crates/tui/src/sleep_guard.rs (Drop impl, lines 70-81, + hold()/spawn() at 49 and 112-121): SleepGuard's process spawn (Command::spawn()) and its Drop's child.kill() + child.wait() run as plain std::process calls, but the guard lives inside Engine::run_turn (crates/tui/src/core/engine/turn_loop.rs:696-699), an async fn on the Tokio runtime — so both execute inline on a Tokio worker thread on every turn start/end. This is the exact pattern this repo's own "Blocking-call convention (Blocking calls in async paths: audit thread::sleep and std::fs in tool paths, pick one convention #6149)" in AGENTS.md calls out, and the codebase already has spawn_blocking_supervised (crates/tui/src/utils.rs) for it. scripts/check-blocking-calls-budget.py's regex only matches thread::sleep/std::fs::*, not std::process::*, so this didn't trip the automated ratchet. Real-world impact is likely small (kill() sends SIGKILL, so wait() returns almost immediately), but it's worth wrapping in spawn_blocking_supervised to match the stated contract. Fix this →

Good choices worth noting

  • crates/tui/src/tui/ui/event_loop.rs/motion.rs: the cadence-tier computation is now read once and shared between the animation tick and the draw limiter, removing a duplicate cadence_tier_from_signals call and keeping the two in agreement by construction.
  • crates/tui/src/runtime_api.rs: Last-Event-ID vs. since_seq precedence (explicit query cursor wins) is well-tested (thread_event_frames_carry_the_id_a_reconnect_resumes_from, last_event_id_accepts_only_decimal_cursors), and an unparseable id degrades to "start from durable head" rather than a hard failure.
  • crates/tui/src/runtime_api.rs idempotent_replay: skip_serializing_if keeps the response byte-identical for clients that never send an operation_key, and both the added and pre-existing call sites (start_thread_turn, compact_thread) were updated consistently.
  • crates/tui/src/test_support.rs (boxed_phase, block_on_default_test_stack): a reused helper rather than a one-off fix for the #6362 stack-overflow regression test, and it pins the default 2 MiB stack explicitly so CI's larger RUST_MIN_STACK can't mask a recurrence.
  • No provider/model-prefix routing logic was touched in this diff — the EPIC v0.8.65 EPIC: Separate provider facts, model facts, offerings, and route resolution #2608 invariant doesn't apply here.

No provider/model/route-safety issues, no correctness issues in the terminal HTTP routes' bounds-checking (bounded_max_bytes, bounded_dimension, TERMINAL_INPUT_MAX_BYTES), and no dead code or duplicated logic beyond the one item above.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 7 potential issues.

Devin Review

Comment on lines +282 to +290
_ = heartbeat.tick() => {
// An indefinite wait (`user_input_timeout_seconds = 0`) is
// the case that needs this most: nothing else bounds it.
let waited = started.elapsed();
let message = wait_announcement("user input", tool_id, waited);
tracing::warn!(tool_id, waited_secs = waited.as_secs(), "{message}");
if !announced {
announced = true;
let _ = self.tx_event.send(Event::Status { message }).await;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Heartbeats disable user-input timeouts

For waits above 60 seconds, heartbeat.tick() wins first and cancels the in-flight timeout future. Each loop rebuilds timeout(wait, recv()), so unanswered prompts never expire and turns remain parked.

Learn more

tokio::select! drops every losing branch when a winning branch completes. The default user-input timeout is 300 seconds, while the production heartbeat fires every 60 seconds. Therefore, each heartbeat drops the current 300-second timeout and the next loop iteration starts another 300-second timeout. This repeats forever unless input, cancellation, or channel closure arrives.

Example: With the default 300-second timeout, heartbeats fire around 60, 120, 180, and 240 seconds. Each heartbeat resets the timeout countdown, so the expected timeout at 300 seconds never occurs.

Recommended fix: Create one absolute deadline or pinned Sleep before the loop and select on that stable future. Keep the receive future separate so heartbeat ticks do not reset the timeout budget.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +599 to +602
terminal_stream: cfg!(unix),
terminal_input: cfg!(unix),
terminal_resize: cfg!(unix),
terminal_kill: cfg!(unix),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 OHOS falsely advertises terminal support

On OHOS, cfg!(unix) advertises every terminal capability although each route uses the 501 stubs. Clients expose terminal controls that can never work.

Suggested change
terminal_stream: cfg!(unix),
terminal_input: cfg!(unix),
terminal_resize: cfg!(unix),
terminal_kill: cfg!(unix),
terminal_stream: cfg!(all(unix, not(target_env = "ohos"))),
terminal_input: cfg!(all(unix, not(target_env = "ohos"))),
terminal_resize: cfg!(all(unix, not(target_env = "ohos"))),
terminal_kill: cfg!(all(unix, not(target_env = "ohos"))),

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +176 to +183
let start = cursor.max(dropped);
let skip = usize::try_from(start - dropped).unwrap_or(usize::MAX);
let take = max_bytes.min(self.bytes.len().saturating_sub(skip));
let bytes = self.bytes.iter().skip(skip).take(take).copied().collect();
OutputChunk {
bytes,
offset: start,
next_cursor: start + take as u64,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Future cursors permanently skip output

When cursor exceeds total, read_since returns that future value as next_cursor. Subsequent output below that cursor stays invisible, so clients can permanently skip terminal bytes.

Suggested change
let start = cursor.max(dropped);
let skip = usize::try_from(start - dropped).unwrap_or(usize::MAX);
let take = max_bytes.min(self.bytes.len().saturating_sub(skip));
let bytes = self.bytes.iter().skip(skip).take(take).copied().collect();
OutputChunk {
bytes,
offset: start,
next_cursor: start + take as u64,
let start = cursor.max(dropped).min(self.total);
let skip = usize::try_from(start - dropped).unwrap_or(usize::MAX);
let take = max_bytes.min(self.bytes.len().saturating_sub(skip));
let bytes = self.bytes.iter().skip(skip).take(take).copied().collect();
OutputChunk {
bytes,
offset: start,
next_cursor: start + take as u64,

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +260 to +261
let guard = lock_session(&session)?;
terminal_session::write_bytes(&guard, &bytes).map_err(ApiError::internal)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Terminal input blocks Tokio workers

terminal_input writes up to 64 KiB synchronously while holding a std::sync::Mutex on an async worker. A shell that stops reading can block Runtime API progress.

Learn more

PTY writes use std::io::Write, and the session uses std::sync::Mutex. Both can block the Tokio runtime thread. A PTY's input buffer is bounded, so writing a full request to a child that is not reading can wait indefinitely or until the PTY fails. The repository's blocking-call contract requires synchronous I/O reached from async handlers to run under spawn_blocking.

Example: A terminal runs a foreground process that never reads stdin. A client posts 64 KiB to the input route. Once the PTY buffer fills, write_all blocks the Axum worker instead of yielding.

Recommended fix: Move session lookup locking and write_bytes into tokio::task::spawn_blocking, returning the byte count or mapped error after the blocking task completes. Apply the same boundary to synchronous resize, kill, and exit-status operations in this module.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +114 to +120
Command::new(program)
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.ok()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Sleep inhibitor blocks turn startup

SleepGuard::hold runs Command::spawn synchronously when an interactive turn starts. Slow process creation stalls the engine runtime before model work begins.

Learn more

The sleep guard is created synchronously from run_turn, which runs on Tokio. Command::spawn performs blocking process setup, and dropping the guard later also calls blocking kill and wait. The repository contract forbids std::process operations inline on async call chains.

Example: On a loaded Linux host, starting systemd-inhibit stalls during process creation. The turn occupies its Tokio worker before sending the model request, delaying unrelated engine work on that runtime.

Recommended fix: Make inhibitor acquisition and release asynchronous through tokio::process, or move the complete child lifecycle to a dedicated blocking task controlled by a channel. Preserve RAII semantics without running spawn, kill, or wait on the engine worker.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread CHANGELOG.md
Comment on lines +152 to +162
- Extensions keeps the exact-content plugin review on the panel: confirming
a bundle's digest re-reads the inventory, so the row you just reviewed
reports its new trust state and offers Enable instead of leaving you in
the transcript with a stale "not reviewed" row.
- Underwater motion ticks at the cadence the frame limiter actually draws
(the atmosphere interval while only the water moves, the authored 80 ms
ocean cadence inside the interactive cap while a turn streams), and the
event loop wakes exactly for the next tick instead of on the next idle
poll. Idle water no longer requests frames it cannot draw or quantizes its
cadence to the poll interval; reduced motion, Ghostty, tmux and the
six-second idle settle are unchanged.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Branch carries prohibited changelog edits

CONTRIBUTING.md reserves both changelogs for merge-time updates on main. Remove these branch hunks to avoid conflicts with concurrent work.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread docs/RUNTIME_API.md
Comment on lines +1233 to +1234
- `POST /v1/terminal/{name}/input` — `{ "data", "encoding"? }`, UTF-8 text by
default or `base64` for exact bytes → `{ "name", "written" }`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Terminal input default is inconsistent

The documentation says omitted encoding means text, while terminal_input defaults it to base64. Align the contract before clients adopt it.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +70 to +81
#[cfg(unix)]
impl Drop for SleepGuard {
fn drop(&mut self) {
let Some(child) = self.child.as_mut() else {
return;
};
// Killing the inhibitor is what releases the assertion; reaping it
// keeps a zombie out of the process table.
let _ = child.kill();
let _ = child.wait();
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking std::process calls run inline on the Tokio runtime.

SleepGuard::hold() (line 49, via start_inhibitorspawn at 112-121) calls std::process::Command::spawn() directly, and this Drop impl calls child.kill() + child.wait() synchronously. The guard is created and dropped inside Engine::run_turn (crates/tui/src/core/engine/turn_loop.rs:696-699), which is an async fn running on the Tokio runtime — so both the process spawn and the blocking reap execute inline on a Tokio worker thread on every turn start/end.

This is exactly the pattern the repo's own Blocking-call convention (#6149) in AGENTS.md calls out: "std::fs/std::process calls inside async code use tokio::fs/tokio::process, or move the synchronous work into tokio::task::spawn_blocking." The codebase already has spawn_blocking_supervised (crates/tui/src/utils.rs, used e.g. in crates/tui/src/core/engine.rs) for exactly this.

Note scripts/check-blocking-calls-budget.py's regex only matches thread::sleep and std::fs::*, not std::process::*, so this doesn't trip the automated ratchet even though it's the same class of issue the ratchet exists to catch.

In practice kill() sends SIGKILL so the subsequent wait() should return almost immediately, which limits real-world impact — but it's still a syscall inline on the executor thread on every turn boundary, and Command::spawn() (fork+exec) is not free either. Worth wrapping the spawn and the Drop reap in spawn_blocking_supervised (or moving to tokio::process::Command) to match the stated contract.

Fix this →

Hmbown commented Sep 21, 2026

Copy link
Copy Markdown
Owner Author

The eight review threads posted on the merged head were verified against main 056eb55. Six hold and are fixed in #6371:

  • 🔴 approval.rs user-input timeout reset by the heartbeat — confirmed, fixed with one absolute deadline plus a regression test that fails on this head.
  • 🟡 sleep_guard.rs inline std::process in run_turn (also Claude Code Review's finding) — confirmed, now a tokio::process child with kill_on_drop.
  • 🟡 runtime_api/terminal.rs PTY write/resize/kill under the mutex on a worker — confirmed, every session touch runs on the blocking pool.
  • 🟡 runtime_api.rs OpenHarmony terminal_* capability flags — confirmed, flags follow the routes' gate.
  • 🟡 terminal_session.rs future cursor echoed back — confirmed, clamps to total (a deliberate contract change from feat(runtime-api): terminal byte stream (#34), stream resume + idempotent submit (#76), pet agent-count pin (#12) #6361, called out in the PR).
  • 🔍 docs/RUNTIME_API.md input encoding default — confirmed, docs now say base64.

Not acted on: the CHANGELOG.md note. CONTRIBUTING reserves changelog edits for the merge-time receipts pass, and this PR was that pass; it is already on main.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(tests): configured_model_api_tests overflow the test thread stack and abort the tui lib suite

3 participants