0.10.0 release readiness: green CI, #6362 stack fixes, water cadence, Extensions trust review, SIGPIPE-safe MCP startup - #6370
Conversation
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
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. |
|
Claude finished @Hmbown's task in 5m 16s —— View job Claude's review of #6370Todo list
SummaryReviewed 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 1 finding — posted inline
Good choices worth noting
No provider/model/route-safety issues, no correctness issues in the terminal HTTP routes' bounds-checking ( |
| _ = 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; |
There was a problem hiding this comment.
🔴 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
| terminal_stream: cfg!(unix), | ||
| terminal_input: cfg!(unix), | ||
| terminal_resize: cfg!(unix), | ||
| terminal_kill: cfg!(unix), |
There was a problem hiding this comment.
🟡 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.
| 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"))), |
Was this helpful? React with 👍 or 👎 to provide feedback.
| 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, |
There was a problem hiding this comment.
🟡 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.
| 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, |
Was this helpful? React with 👍 or 👎 to provide feedback.
| let guard = lock_session(&session)?; | ||
| terminal_session::write_bytes(&guard, &bytes).map_err(ApiError::internal)?; |
There was a problem hiding this comment.
🟡 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
| Command::new(program) | ||
| .args(args) | ||
| .stdin(Stdio::null()) | ||
| .stdout(Stdio::null()) | ||
| .stderr(Stdio::null()) | ||
| .spawn() | ||
| .ok() |
There was a problem hiding this comment.
🟡 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
| - 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. |
| - `POST /v1/terminal/{name}/input` — `{ "data", "encoding"? }`, UTF-8 text by | ||
| default or `base64` for exact bytes → `{ "name", "written" }` |
| #[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(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Blocking std::process calls run inline on the Tokio runtime.
SleepGuard::hold() (line 49, via start_inhibitor → spawn 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.
|
The eight review threads posted on the merged head were verified against main
Not acted on: the Generated by Claude Code |
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 head752bae3is green on every leg of the PR run, on an exact-headworkflow_dispatchof ci.yml (heavy gates forced), on the OpenHarmony check, and on the exact-head release-candidate run that builds all seven artifact targets.ConfigFile.baseboxed;runtime_store_binding_survives_launch_snapshot_and_resumesplit into boxed phases on a pinned 2 MiB thread; explicit 2 MiB guard tests. No stack ceilings were raised.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=SIG_DFL, so a stdio MCP server that exits beforeinitializeis 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 andwaitpid: 6 of 12 launches died of SIGPIPE at ~0.4 s.run_tuinow 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.pub(crate), the Unix-only constants andbase64import carry the handler gate, the request structs expect unread fields on ohos/Windows, andsleep_guard'sspawn/Command/Stdiocarry the macOS/Linux gate of their only callers (Android is Unix but a no-op).sandbox_detailsis listed as an action row inevery_settings_row_reaches_a_store.check:factsderives each model'saddedAtfrom 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.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 withexpected_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:904314efailed Test ×3 (every_settings_row_reaches_a_store) and ohos (E0364/E0603);8811455failed ohos on dead code;2ca42e9/d5cfae1failed 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 -- --checkclean; 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.2ca42e9):every_settings_row_reaches_a_store1 passed (failed on904314e);sleep_guard2 passed;runtime_api::terminal4 passed;profile_document_parses_within_the_default_test_thread_stack1 passed;runtime_store_binding_survives_launch_snapshot_and_resume1 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.pty.fork+waitpid,/usr/bin/falseas a required MCP server): pre-fix binary 6/12 launches killed by SIGPIPE; post-fix 0/12, 12/12 reached the launch card.launch_card_ptygroup 10 passed / 0 failed (twice, old and new harness);launch_mcp_summary_opens_manager_by_click_and_keyboard3/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 passedwork_bar_keys,config_theme_nav,active_composer_pointer,search_text,screen_mode_inline(2),contextual_tips(2),automations_editor(2).cargo build --release --locked -j 2 -p codewhale-cli -p codewhale-tui, tree2ca42e9, 27m30s):codewhale 0.10.0 (dev)sha256f2b94b88f9fcc2b738fe1f40491e1b8629723a8956877b204bd4431f52a9c769,codewhale-tui 0.10.0 (dev)sha256f245094a763e6c1643d7d219caa084fa06d4049d0ad33960dd7cb6c1ce54860d.752bae3differs from2ca42e9only 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 -- --checkcargo clippy --workspace --all-targets --all-features --locked— CI Lint ✓ on752bae3cargo test --workspace --all-features --locked— CI Test ubuntu/macos/windows ✓ on752bae3Checklist
sleep_guardandruntime_api/terminalare documented over their existing owners)🤖 Generated with Claude Code
https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv