Skip to content

v0.9.14 slice run 2: lazy MCP, session recovery + picker UX, launch remedy row - #6175

Merged
Hmbown merged 51 commits into
mainfrom
v0914-chunk2
Sep 15, 2026
Merged

Hmbown merged 51 commits into
mainfrom
v0914-chunk2

Conversation

@Hmbown

@Hmbown Hmbown commented Sep 15, 2026

Copy link
Copy Markdown
Owner

What / why

Second stacked slice run on the v0.9.14 milestone, stacked on origin/main (433685b2, post-#6161 merge). Nine issue slices plus one lint fix, one commit per issue, each verified before commit.

Closes

Refs (partial slices — status comments on the tracker)

Commits

Gate output (what actually ran on this branch)

  • cargo fmt --all -- --check → clean
  • cargo check -p codewhale-tui --all-targets --locked → clean
  • Focused suites per slice: session_picker 29 · session_control acceptance 42 · localization parity 51 · session-filtered lib 766 · subagent 783 · doctor 123 · engine MCP 462 · launch_card 16 — all green
  • scripts/check-dead-code-budget.py → PASS (369, exactly at budget) · check-blocking-calls-budget → PASS (616 sites)
  • One known environment-sensitive lib failure observed once (compaction-count flake); it fails identically on origin/main without these changes.
  • Not run locally: the full workspace nextest suite, npm test, npm run check:web — hosted CI on this PR is the gate.

Generated with Devin


Devin Review

Note

Medium Risk
Changes MCP connection timing, engine turn admission, and daemon async I/O on core runtime paths; wire send_message JSON stays compatible but behavior shifts when servers connect.

Overview
This PR lands a v0.9.14-style slice across runtime conventions, MCP boot, sessions UI, and execpolicy structure.

Blocking calls (#6149): Documents the Tokio rule in AGENTS.md, adds a CI blocking-calls budget ratchet, moves daemon socket setup to tokio::fs, runs local Whisper transcription on spawn_blocking, and replaces thread::sleep with yield_now on async user-command paths.

MCP (#6033): Session boot connects only required / always-load / tool-selected servers; mcp_tools no longer sweeps every server each turn. Explicit tool selections start and await their own connects (with deadline/cancel), and “connecting” is tracked from the pool instead of inferred as enabled−connected.

Sessions (#5715, #6014): Injects a recovery hint into the frozen system prompt when a prior interrupted checkpoint exists; the picker gets current-session labeling, full ID in preview, updated PgUp/PgDn hints, and passes with_current_session when opened.

Protocol / engine: Extracts per-turn fields into TurnSpec (Op::SendMessage(TurnSpec) and protocol twin) without changing the tagged JSON shape; refactors handle_send_message to take one spec struct.

Execpolicy (#6141): Consolidates TOML rules in codewhale-execpolicy (matcher, toml_rules, RuleDecision, parse); drops default-path loaders from the crate.

Also documents codewhale exec --hooks, bumps base64/shlex, and updates session-related locale strings.

Reviewed by Cursor Bugbot for commit 137fb70. Bugbot is set up for automated code reviews on this repo. Configure here.

CodeWhale Bot and others added 14 commits September 14, 2026 13:55
…6099)

Headless runs fired no hooks at all; --hooks arms the same HookExecutor the
TUI builds (global config, reviewed plugin snapshots, trusted project
hooks.toml) and threads it through the engine config, the SendMessage op,
and the tool runtime services. tool_call_before can still deny and
shell_env still applies; hook `ask` resolves fail-closed headlessly. Fleet
worker subprocesses never opt in. permissions.toml typed rules already ran
in this turn loop and are unchanged.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
A role member pinned to the fleet's own operator route resolved to that
route either way — the pin only stopped it following when the operator
moved. FleetFile::parse now reads the redundant pin as inheritance, and
add_fleet_model writes inherit for role members on the operator route.
models_of attributes inherited roles to the operator route they resolve
to, and the picker toggle still reports that coverage as present.
Different-route pins and shortlist rows keep their pins — the pin is the
deliberate opt-out.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The engine drains rx_op only between turns, so an awaited send into a
saturated 32-slot mailbox froze all input for the rest of the turn. The
SendMessage dispatch already reserved capacity off the render thread; this
finishes the audit for the remaining input-path sites:

- try_send for droppable ops whose rejection is reported and retryable:
  sidebar/apply CancelSubAgent, PreviewOutboundRequest, bang shell input,
  PurgeContext, and the single-op settings updates.
- try_reserve before committing state, then send_reserved_op, where the op
  must land if the UI changed: BacktrackConfirm's SyncSession and the
  search-provider update.
- sync_mode_update uses try_send because ChangeMode's live authority is
  published even on a full channel and applied at the next drain.

Left awaited, now documented on EngineHandle::send and at the site: the
ordered must-deliver ops of committed transitions (session/provider
reload, Shutdown+SyncSession+SetCompaction) where a drop would desync
engine and UI. Removing those wedges needs an engine-side mid-turn op
drain or deferred application — a separate slice.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Audit of `thread::sleep` and `std::fs` against the runtime: the named
sites were already safe (`cloud_dispatch::wait_ready` runs on the
dedicated dispatch thread; `lane::runtime`'s 10ms sleep is test-only),
but the sweep found real violations — sync contention retries and
filesystem calls reachable from async tool/UI/engine paths.

Fixes:
- `work_graph::retry_lock`, `App::retry_lock`, and the `/clear`-style
  lock retries in `commands::user_registry` parked a worker up to
  100ms on `try_lock` contention; they now `yield_now` between attempts
  and still degrade to the existing "state is busy" error.
- `TerminalInputPump::pause_for_child_terminal` (event-loop task, up to
  500ms) is now async on `tokio::time::sleep`.
- `transcribe_local_whisper` ran `Command::output()` plus temp-file I/O
  inline; the whole body now runs on the blocking pool.
- `std::fs` inside `async fn`s converted to `tokio::fs` (session load in
  apply.rs, notes tool, resource admission, daemon socket lifecycle,
  doctor config read, speech output) or moved into `spawn_blocking`
  (MCP registry cache write, Fleet manager lock setup) where the sync
  helper's retry semantics should be preserved.

Convention recorded in AGENTS.md: async code uses tokio::fs/tokio::time
or isolates sync work in `spawn_blocking`; `thread::sleep` is for
dedicated threads and bounded sync-API retries only.

New ratchet `scripts/check-blocking-calls-budget.py` (+ JSON budget,
CI step) counts unprotected blocking-call sites — anything outside
spawn_blocking/dedicated-thread/test scopes — so the 616-site ceiling
can only shrink. Hermetic self-tests in
`scripts/test_check_blocking_calls_budget.py`.

Verified: cargo check -p codewhale-tui -p codewhale-app-server --locked;
cargo nextest run -p codewhale-tui (750 passed across work_graph,
mcp_registry, resource_admission, fleet, notes, voice, registry);
cargo test -p codewhale-app-server (4 passed); gate self-tests 9/9;
budget gate fails on an injected async-scope sleep (exit 1).
`tui/src/execpolicy/` was the last private copy of the policy surface —
~110 call sites already use `codewhale_execpolicy`, and shell.rs was the
sole remaining consumer. Migrate the last consumer or do not start:
this finishes it.

- `matcher.rs` and `rules.rs` move to `crates/execpolicy/src/` as
  `matcher` and `toml_rules`. The TOML verdict enum is renamed
  `ExecPolicyDecision` -> `RuleDecision` so it stops colliding with the
  crate's engine output type of the same name.
- The `~/.deepseek/execpolicy.toml` path resolution (a config-home
  concern) stays in shell.rs as `load_default_policy`; the crate only
  knows how to parse and evaluate.
- The load is now wrapped in `spawn_blocking` — `from_path` does a
  synchronous `read_to_string` and `execute` runs on the Tokio runtime
  (blocking-call convention, #6149).
- The stale doc reference in `command_safety.rs` pointing at the old
  `crate::execpolicy::matcher` path is corrected.

Verified: cargo test -p codewhale-execpolicy --offline (matcher 7 +
toml_rules 4 incl. the shell-spelling deny-pattern suite, all pass);
cargo nextest -p codewhale-tui -E 'execpolicy or shell' (424 passed);
cargo check -p codewhale-tui --offline clean; dead-code budget 413/413;
blocking-calls budget within limits.
…#5529)

A wall-time or token death used to report only what the model managed to
say in its hand-back — the uncommitted files it actually left behind were
invisible, so salvage meant guessing. (Observed: two workers died at the
1800s cap with a complete fix uncommitted and no record of where it lived.)

Both terminal paths now inventory the worker's workspace changes against
its spawn-time delivery baseline (`DeliveryEvidence::changed_paths`) and
append the receipt to the result text: the changed paths (bounded to 12 +
a count) and the workspace they survive under. A read-only worker has no
baseline and gets no invented inventory; a write-scoped worker that
changed nothing gets an explicit "no workspace changes" line. The git/fs
reads run under `spawn_blocking` per the #6149 convention, and in the
task-error path before the manager write lock is taken.

Deliberately not done: `checkpoint.continuable` stays false and
`BudgetExhausted` stays non-resumable. Continuation narrows inherited
allowances rather than resetting them ("continuation cannot reset its
deadline"), so resuming a budget-dead child always fails honestly —
flagging the checkpoint continuable would promise a path that cannot
exist. Preservation here means naming the surviving work, not laundering
the dead allowance.

Verified: cargo test -p codewhale-tui --lib --offline subagent (783
passed, 0 failed) incl. new
budget_death_preservation_note_names_surviving_workspace_changes;
cargo check clean; fmt clean; blocking-calls budget within limits.
A member or profile pin is only drift when the provider's *own* roster no
longer offers the id — the same id may still be served by other hosts, so
a global retire-the-id rule would be wrong. The operate_fleet report now
collects every (provider, model) pin across fleet files (operator +
members) and the effective roster, and flags a pin only when a FRESH
cached live roster for that exact route exists and does not list it.
Stale, failed, or absent rosters prove nothing and are counted under
`unverifiable` instead of producing false warnings.

Each drifted row names the route and every owner of the pin so the
operator can see the full blast radius; the message states the id may
still answer and that the pin is left unchanged. No writes, no rewrites —
surfacing only, per the no-silent-rewrite contract.

Verified: new
doctor_fleet_report_flags_pins_absent_from_fresh_live_roster (member pin
flagged, operator pin on the listed id not flagged); all 123 doctor
tests pass; cargo check clean; fmt clean.
Convert 40+ #[allow(dead_code)] markers to #[cfg(test)] on items whose
only callers are unit tests, so they stop compiling into production
builds instead of being lint-suppressed there:

- runtime_threads: AgentRebindHint/AgentRebindStatus/
  collect_agent_rebind_hints and the pending_*_count probes
- app.rs: cycle_mode_reverse, accrue_*_cost, displayed_session_cost,
  push_pending_steer
- tools: spec.rs with_trust_mode/with_lsp_manager/is_sandboxable,
  shell.rs remember_stale_job, subagent queued_mail_depth/
  child_was_woken/pending_child_approvals
- the whole tideline translation-scaffolding archipelago (#5698
  landing slice): work_surface::tideline and work_surface::panels
  modules, views::tideline_preview module, the settings rail/stage
  items in views, the inbox cluster in notifications, the theme-list
  cluster in theme_picker, composer shell/render fns, and the
  history.rs tideline re-export
- palette::ui_theme_from_settings

Items with zero callers in any config (_assert_var_handle_shape,
_diagnostic_level_label/_diagnostic_path) keep their allow markers —
cfg(test) would re-trigger dead_code in test builds.

Dead-code budget ratcheted 413 -> 370.

Verified: cargo check (lib + --tests) clean; 135 focused tests pass
including the tideline golden suites, collect_agent_rebind_hints_*,
steer/cost, and palette theme tests.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The method was introduced by the #6141 TOML-rules port; clippy's
should_implement_trait flags a from_str that is not FromStr. parse is the
same code with a name that does not shadow the trait convention.
Configured MCP servers used to spawn all at session start: connect_all
drove every enabled server, and "connecting" was inferred as
enabled-minus-connected, so a server nobody asked for still paid a
handshake and read as mid-flight.

The pool now owns the truth with a `connecting` set marked at spawn and
cleared on resolution or abort. The boot pass scopes to the eager set —
`required` servers plus ones covered by `tools.always_load`/`allowed_tools`
selections; `collect_pending_connects` takes a scope and skips names
already in flight. A turn whose selection names an unstarted server
spawns its connects beside the boot pass and waits on both under the
existing five-second deadline, with the same config-invalidation abort
discipline. `mcp_tools` no longer sweeps every turn; required-server
gaps still get an honest diagnosis. Lazy dispatch itself was already
there through `resolve_advertised_tool`/`get_or_connect` — this wires
boot and the tool catalog to match it.

Surfaces stop inferring: session-boot rows, the Extensions tab, the
launch card, and the pre-event prediction all read the real in-flight
set, so a configured-but-unstarted server shows as configured, never
connecting. docs/MCP.md documents the lifecycle.

Verified: 444 focused nextest tests pass including
lazy_boot_leaves_unselected_servers_unspawned and the invalidate-config
abort path; cargo fmt clean; dead-code budget 369 at budget;
blocking-call budget within.
…ls (#6140)

Two cleanups in the stdio MCP server behind `codewhale serve --mcp`:

- The loop ran on its own `Runtime::new()` under `block_in_place` and
  `block_on`ed `registry.execute_full` per request. A stdio JSON-RPC
  server is serialized by definition, so it now runs `async` on the
  caller's runtime — `tokio::io::stdin` lines in, `execute_full().await`
  per call — and the private runtime, `block_in_place`, and every
  `block_on` are gone. Synchronous config and session-listing reads move
  to the blocking pool per the workspace convention.

- The `deepseek`/`deepseek-reply` tools called `DeepSeekClient::
  create_message` directly with their own thread map — a second model
  authority beside the engine, and provider-exclusive naming to boot.
  Per the issue ("delete it or generalize through the engine; never a
  special-cased client call") they are deleted: there is no engine in
  this surface to route through, and the resources it exposed already
  exist elsewhere. Configs still naming them degrade to a tool error
  instead of silently succeeding. Session resource URIs are now
  `codewhale://session/`.

Verified: 16 mcp_server-focused nextest tests pass including a new
retirement test; fmt clean; dead-code and blocking-call budgets hold.
…5715)

After a force-quit the previous session's work sat on disk but the model
had no way to know it existed. Two bounded additions on the existing
session store:

- `SessionManager::interrupted_workspace_session` finds the newest
  workspace-scoped session that still holds a crash-recovery checkpoint —
  the durable sign a session ended mid-turn. It reads metadata only,
  excludes the live session's own id and sessions this process instance
  created (boot-owner stamp), and skips malformed records rather than
  letting one poison the scan.
- `session_recovery_hint` renders that as a one-line notice in a new
  `## Prior Session` block of the session-pinned prompt prefix, computed
  at engine construction and refresh. Clean sessions get no block, so
  their prefix bytes are unchanged.
- `session_search` / `session_get` give the model bounded, read-only,
  workspace-scoped recall over the same store: one-line session summaries
  and an 8-message text tail, labeled untrusted user data. Registered on
  the standard agent runtime surface; both run their filesystem reads on
  the blocking pool.

Resuming remains the user's decision — the hint tells the model to offer
a summary or continuation (e.g. /resume), not to silently continue.

Tests: newest-first pick, current-session and current-instance exclusion,
other-workspace isolation, settled-session silence, hint render/absence
byte stability, tool scoping, bounds, and trust labeling.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- Hide empty auto-created sessions in the browse view through the shared
  projection (`SessionQuery::without_empty_auto_created`) — the same
  definition the launch list and `--continue` apply. `new_selecting` lifts
  the filter as a last resort so an explicit handoff still lands.
- Mark the live session's row with a localized `current` label plus
  WHALE_ACTION ink — text, not colour alone.
- List the full session id in the history preview; the row only has room
  for the truncated form.
- PgUp/PgDn page the session list by one viewport (clamped); the history
  preview keeps paging on Shift+PgUp/PgDn. Pane titles updated in every
  complete locale pack; wide layout gives the list 44% instead of 36%.

Evidence: 29 session_picker tests, 42 session_control acceptance tests,
51 localization parity tests, 766 session-related lib tests — all green.
FMT-OK, dead-code budget PASS (369, exactly at budget).

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
)

The row already printed `/mcp login <name>` or `/mcp` but the user had to
retype it by hand. It now joins `LaunchRowId` — the shared
paint/click/keyboard ordering — so Up/Down lands on it and Enter/click
type the printed remedy into the composer. Typing beats copying: no
clipboard dependency over SSH, and the user sees the command before a
second Enter sends it. One `mcp_remedy_command` helper serves the row's
tail and its action, so what is painted is what runs.

Evidence: 16 launch_card tests green including three new ones covering
row-ordering parity, composer fill, and the no-problem noop.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings September 15, 2026 01:24

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.

@cursor

cursor Bot commented Sep 15, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_74341abb-60f8-410c-bc25-7306ad9d2a85)

@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 1m 51s —— View job


Review in progress

Todo list

@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.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

Devin Review found 11 potential issues.

Devin Review

Comment on lines +6472 to +6476
if !explicit.connects.is_empty() {
explicit.connects.abort_all();
if let Some(pool) = self.mcp_pool.as_ref() {
pool.lock().await.cancel_connecting(&explicit.names);
}

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.

🟡 Slow selected MCP servers never connect

wait_for_explicit_mcp_boot aborts a selected server when its handshake exceeds five seconds. Slow cold-start servers restart every turn and never expose their tools.

Learn more

Explicit lazy connects use the five-second UI deadline, while each MCP server can have a longer connection timeout. When the deadline expires, abort_all cancels those handshakes and clears their in-flight markers. The comment above the loop says connects continue for later turns, but only the separate boot pass continues. The next turn starts the selected server from scratch, so a consistently slow cold start can never complete.

Example: An npx MCP server needs eight seconds on a cold cache and has a 30-second connect timeout. Every turn aborts it at five seconds. The expected tool schema never appears, although the configured timeout allows the handshake.

Recommended fix: Detach unfinished explicit connects into an engine-owned background task that stores authority-checked results after the turn proceeds. Keep the five-second bound only on waiting for schemas, not on the connection lifecycle. Ensure config reload and shutdown still abort and clear the exact pending names.

Devin Review

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

Comment thread crates/tui/src/tui/app.rs
Comment on lines 5905 to +5908
if let Ok(guard) = mutex.try_lock() {
return Some(guard);
}
std::thread::sleep(std::time::Duration::from_millis(1));
std::thread::yield_now();

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.

🟡 Lock retries collapse into immediate failures

retry_lock performs all retries without yielding the Tokio task that may hold the mutex. Brief contention now becomes save, restore, or clear failures.

(Refers to this code)

Learn more

std::thread::yield_now yields the operating-system thread, not the current async task. On a current-thread runtime, or when the holder is queued on the same worker, the mutex holder cannot run between attempts. Even on a multithreaded runtime, 100 immediate attempts provide almost no contention window compared with the previous 100 milliseconds. The same mechanism also affects command resets in try_dispatch and work-graph operations in retry_lock.

Example: A task holds plan_state briefly and is ready to release it after its next poll. A session restore calls retry_lock; all 100 attempts finish before the holder is polled, so restore reports that plan state is busy.

Recommended fix: Make these call paths async and retry with tokio::task::yield_now or a bounded tokio::time wait. Where a synchronous API is mandatory, redesign ownership so it does not poll an async mutex from the runtime task.

Devin Review

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

Comment on lines +6987 to +6992
// Recomputed on each refresh (#5715): the prior session's checkpoint
// may have settled or been resumed since construction.
let recovery_hint = crate::session_manager::session_recovery_hint(
&self.config.workspace,
Some(self.session.id.as_str()),
);

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.

🟡 Recovery refresh rewrites pinned prompt prefix

A prompt refresh recomputes recovery_hint, so checkpoint changes rewrite the session-pinned prefix. Later turns lose prefix-cache reuse and receive different prior-session context.

Learn more

The repository contract treats the system prompt and tool catalog as a session-pinned KV-cache prefix. Engine construction computes a recovery hint, but every mode, model, goal, or system refresh scans mutable checkpoint state again. Removing a checkpoint or adding a newer interrupted session therefore changes prefix bytes after the session starts. This also contradicts the field's frozen-prefix contract in PromptSessionContext.

Example: A session starts with a hint for interrupted session A. Session A is resumed elsewhere and its checkpoint disappears. Changing mode rebuilds this session's prompt without the hint, invalidating the provider prefix cache and changing prior-session context mid-session.

Recommended fix: Store the construction-time recovery hint on Engine and reuse it for every prompt refresh. If live checkpoint changes must reach the model, append them as user-role history instead of changing the pinned prefix.

Devin Review

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

Comment on lines +121 to +130
let mut sessions = manager.list_sessions()?;
sessions.retain(|session| {
workspace_scope_matches(&session.workspace, &workspace)
&& query.as_deref().is_none_or(|query| {
let query = query.to_lowercase();
session.title.to_lowercase().contains(&query)
|| session.id.starts_with(query.as_str())
})
});
sessions.truncate(limit);

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.

🟡 Current session pollutes prior search

session_search includes the active session among prior sessions and truncates afterward. The newest active session can displace interrupted work from bounded results.

Learn more

The tool is registered as prior-session recall, but ToolContext supplies only the workspace here. Once the live session has been persisted, list_sessions normally returns it first because it is newest. The workspace filter retains it and the limit is applied before any current-session exclusion. The output then labels that row as a prior session.

Example: The current session and eight older sessions exist in one workspace. Calling session_search with the default limit of eight returns the current session plus seven older sessions. The eighth older session is omitted even though the tool claims to list prior work.

Recommended fix: Thread the active session ID into ToolContext or the runtime session services. Exclude that exact ID before truncating in SessionSearchTool, and reject it consistently in SessionGetTool when these tools are used for prior-session recall.

Devin Review

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

Comment thread crates/tui/src/mcp.rs
Comment on lines +2610 to +2617
pub(crate) fn tool_selection_covers_server(requested: &[String], server: &str) -> bool {
let prefix = format!("mcp_{}_", server.to_ascii_lowercase());
requested.iter().any(|name| {
name.starts_with(&prefix)
|| name
.strip_suffix('*')
.is_some_and(|rule| prefix.starts_with(rule))
})

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.

🟡 Overlapping MCP names start extra servers

Selecting mcp_a_b_tool also covers server a, because matching accepts every shorter server prefix. Lazy boot starts unrelated servers and waits for them.

Learn more

MCP tool names concatenate the server and tool with underscores, so server names can prefix one another. Existing live routing handles this ambiguity by choosing the longest configured server name in needs_auth_server_for_tool_name. This new coverage predicate independently matches every prefix. Lazy eager-set construction and explicit per-turn connection both use it, so both start the extra server.

Example: Configure servers git and git_enterprise. Selecting mcp_git_enterprise_search is routed to git_enterprise, but this predicate also covers git. Both servers start and the turn can wait five seconds for an unrelated git handshake.

Recommended fix: Resolve each exact requested tool against the configured and dynamic server-name set with the same longest-name rule as runtime routing. Apply wildcard coverage separately, preserving its intentional multi-server semantics.

Devin Review

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

Comment on lines +211 to +213
if "spawn_blocking" in tok or tok in ("thread::spawn", "thread::Builder::new"):
pending_blocking = True
continue

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-call gate misses later calls

A call-form spawn_blocking(work) leaves pending_blocking set until the next brace. Blocking calls inside a later unrelated block escape the budget.

Learn more

The scanner assumes every blocking spawn is followed by a closure brace. Rust also accepts a function item or closure variable directly, such as spawn_blocking(read_config). In that form no brace belongs to the spawn, but pending_blocking remains true across lines. The next unrelated { opens a synthetic blocking scope, and matching std::fs or thread::sleep calls inside it are not counted.

Example: spawn_blocking(load).await; if retry { std::fs::read(path); } records zero filesystem sites. The if block is not executed by spawn_blocking, so the read remains inline on the async task.

Recommended fix: Parse the call expression sufficiently to associate protection only with an inline closure body, or conservatively exempt only recognized spawn_blocking(... || { ... }) forms. Add a regression test for a function-argument call followed by an unrelated braced block.

Devin Review

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

Comment thread AGENTS.md
Comment on lines +146 to +157
- Blocking-call convention (#6149): code on the Tokio runtime — tool
handlers, engine tasks, the UI event loop, anything reached through an
`async` call chain — must not run blocking operations inline.
`std::fs`/`std::process` calls inside `async` code use `tokio::fs`/
`tokio::process`, or move the synchronous work into
`tokio::task::spawn_blocking` (`utils::spawn_blocking_supervised` for
fire-and-forget). `thread::sleep` is for dedicated `std::thread`s and
bounded contention retries in synchronous APIs that are only reachable
from blocking scopes — an async-path wait uses `tokio::time`. A sync
helper containing blocking calls must only be called under
`spawn_blocking` or from a dedicated thread; `scripts/
check-blocking-calls-budget.py` ratchets the unprotected-site count.

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.

🔍 PR spans many behavior boundaries

This PR combines MCP, sessions, hooks, fleet, UI, runtime, and CI changes. Repository guidance asks layered PRs to keep one behavior boundary per review.

Devin Review

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

Comment on lines +1922 to +1928
// #6150: the input path never awaits a full op channel.
if engine_handle
.try_send(Op::SetStreamChunkTimeout { timeout_secs })
.is_err()
{
app.status_message =
Some("Engine busy — setting not applied; try again".to_string());

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.

🔍 Busy notices use legacy status sink

New mailbox-rejection messages write status_message. TUI guidance requires new notices to use typed toasts with explicit levels and lifetimes.

Devin Review

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

Comment on lines 267 to 270
"search".to_string(),
"apply_patch".to_string(),
"shell".to_string(),
"deepseek".to_string(),
"deepseek-reply".to_string(),
]

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.

🔍 Retired MCP tools lack migration notice

Configurations naming deepseek or deepseek-reply now expose neither tool. Existing users receive no targeted migration diagnostic for the removed surface.

(Refers to this code)

Devin Review

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

Comment thread .github/workflows/ci.yml
Comment on lines +434 to +437
- name: Check blocking-calls budget
if: needs.changes.outputs.heavy == 'true'
continue-on-error: ${{ github.event_name == 'pull_request' }}
run: python3 scripts/check-blocking-calls-budget.py

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-call ratchet remains advisory

Pull requests continue after this check fails. New blocking calls can merge despite the newly documented mandatory runtime convention.

Devin Review

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

CodeWhale Bot and others added 3 commits September 14, 2026 19:07
The 23-field struct variant was the god-payload every ACP/app-server fix
had to widen. `Op::SendMessage(TurnSpec)` on both the engine op and the
serializable protocol op gives per-turn authority one home; the protocol
newtype keeps the internally-tagged wire shape byte-identical, verified
by the existing round-trip tests.

`handle_send_message` now takes the spec and drops its 23-parameter
signature and too_many_arguments allow; protocol parity projects
TurnSpec -> wire TurnSpec in one helper.

Also converts the audit's one missed input-path site: the agent-focus
FollowUpSubAgent send in dispatch_composer_message now uses try_send and
reports a full mailbox as "engine busy" instead of awaiting it mid-turn.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Audit of `cargo tree --duplicates`: the only splits the workspace itself
caused were base64 (config on 0.22 vs tui on 0.23) and shlex (our 1.3 pins
vs cc's 2.0 build-dep). Both now resolve to one version.

Every remaining duplicate pair is pinned by an upstream crate at its
latest release: oauth2 5 alone holds reqwest 0.12, thiserror 1, sha2
0.10, and tower-http 0.6; portable-pty 0.9 holds bitflags 1 and
filedescriptor; rust-i18n-support 4.2.2 still requires globwalk 0.8 and
toml 0.8; bindgen 0.72 holds shlex 1 for the rquickjs-sys build;
hyper-util holds base64 0.22; shellexpand 3.1.2 caps dirs at <7.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Strip 112 uncommented `#[allow(dead_code)]` across crates/tui/src and let the
compiler re-adjudicate each under `-D warnings` in every target:

- ~50 items are prod-dead but test-reached: `#[cfg_attr(not(test),
  expect(dead_code))]` asserts that and self-verifies against future callers.
- ~33 items are dead in every build (documented wire captures, reserved
  enum variants, pre-wired seams): `#[expect(dead_code)]` keeps them honestly.
- `install`/`update` (skills) and `log_exec`/`append_log_static`
  (shell_dispatcher) are used only from the `#[path]`-included integration
  crate: `#[cfg_attr(test, allow(dead_code))]` covers exactly those compiles.
- `utils::ensure_dir` had zero callers and no stated intent: deleted.
- `subagent_routing` one cfg_attr was fully stale (used in both builds): gone.

Budget ratchets 369 -> 254. Verified: cargo check --all-targets --locked
clean, fmt clean, integration 177 passed / 0 failed.
@cursor

cursor Bot commented Sep 15, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_9e216b10-4c3b-447b-bdf8-b3d7811c8a9e)

@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 1m 38s —— View job


Review in progress

Todo list

The always-on session_get/session_search registration (#5715) widened every
role surface and the parent prompt+catalog envelope without updating the
drift fixtures: re-pin the role constants and the read-only inline list,
and raise the measured parent ceiling (85,913B actual at commit time).

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 1m 44s —— View job


Review complete

Todo list

This push added three commits since the last review round (d5291df..HEAD), but only one is new work from this branch: eb195d8 (dependabot's chore(deps): bump rustls 0.23.43 → 0.23.44) and 4e287e1 (the merge commit that pulled it in) both originate on origin/main, not this PR — confirmed via git log --format='%an' showing dependabot[bot] as author. The only actual new content is aeabd5a "web: regenerate the derived facts so the lint gate matches the workspace." Findings from the prior review rounds are all still present in HEAD and unresolved — not re-detailed here, just listed for tracking:

  • retry_lock's std::thread::yield_now() doesn't actually yield to the Tokio task holding the lock (crates/tui/src/tui/app.rs:5897, same pattern in work_graph/runtime.rs:1450).
  • recovery_hint is recomputed on every prompt refresh, contradicting the session-pinned KV-cache prefix contract, and does synchronous fs I/O on the async engine path with no spawn_blocking (crates/tui/src/core/engine.rs:6965).
  • session_search/session_get never exclude the live session before truncating to limit, and session_get resolves through resume semantics (load_session_by_prefix), which can fabricate crash-repair results for a sibling session that's still actively running (crates/tui/src/tools/session.rs:130,202).
  • tool_selection_covers_server matches any shorter configured server name as a literal prefix, no longest-match rule (crates/tui/src/mcp.rs:2617).
  • check-blocking-calls-budget.py's spawn_blocking(some_fn) bare-function-reference form undercounts real sites.
  • fleet_drift_summary does synchronous fs reads on the Tokio runtime and duplicates build_route_rows's route-resolution logic (crates/tui/src/commands/groups/config/status.rs:352).
  • goal_continuation_if_active builds its GoalBudget via GoalBudget::unbounded()..., so the #6013 hard-stop's StopReason::BudgetLimit arm is unreachable from the engine-owned continuation path (crates/tui/src/core/engine.rs:3998) — confirmed independently by Devin's review as well.
  • SessionManager::resume_session/resume_session_by_prefix (#6185) do synchronous, fsync'd disk I/O and are wired into six async Tokio-runtime call sites with no spawn_blocking (session_manager.rs:2384, plus six call sites listed in earlier rounds).
  • 230911b's doc claim about a MAX_PENDING_BYTES size-based pacing bypass in crates/tui/src/tui/streaming/mod.rs:30-32 doesn't match any existing code.
  • analyze_workflow_plan_approval/analyze_plan_object (#6189) build the approval card from a hardcoded WorkflowConfigToml::default() instead of the real config, so the card can never show high_budget for an operator-configured default_token_budget (crates/tui/src/tools/workflow_plan_approval.rs:495, crates/tui/src/tui/approval.rs:398,571).
  • crates/tui/src/runtime_api.rs:2965's list_commands (v0.9.14: App-server typed command catalog for client slash-command surfaces #6178) and workspace_instructions's unprotected std::fs::canonicalize (crates/tui/src/runtime_api/workspace.rs:310, App-server: GET /v1/instructions for GPUI Memory/instructions (or name the real path) #6168) are fresh instances of the same blocking-call class.

New in this push (aeabd5a)

No correctness bugs — verified, not just trusted. The commit's own claim is that web/lib/facts.generated.ts's toolCount was stale at 76 while the workspace now has 78 tools, because the #5715 slice's two new tools (SessionSearchTool, SessionGetTool) never reached the generated file. I independently recomputed what deriveToolCount() (web/scripts/facts-lib.mjs:185-195) actually measures — a non-recursive readdirSync over crates/tui/src/tools/*.rs counting ^impl ToolSpec for — rather than trusting the commit message:

  • Grepping ^impl ToolSpec for across the whole tools/ tree (including subdirectories) gives 91 matches across 59 files.
  • Subtracting the subdirectory files the non-recursive scan can't see (registry/tests.rs: 4, subagent/coord.rs: 6, subagent/mod.rs: 1, workflow/mod.rs: 1, github/mod.rs: 1 — 13 total) leaves exactly 78, matching the new committed value precisely.
  • The diff is otherwise inert: only generatedAt (a timestamp, explicitly excluded from the committed-vs-fresh comparison per check-facts.mjs) and toolCount changed; the commit message's claim that version/crates/providers/sandboxes/default-model/node/license are all unchanged checks out against the diff.
  • Authored by the repo's own bot account (CodeWhale Bot), consistent with the stated purpose of unblocking the Lint & Type Check gate rather than being human-reviewed application code — appropriately mechanical, single-purpose, and self-contained.

No provider/model/route safety, reuse/dead-code, test-coverage, or security issues in this increment — it's a regenerated data file plus an upstream dependency-bump merge, neither of which touches application logic.

Note: No Bash/cargo execution beyond local git/grep inspection was available in this session; the deriveToolCount verification above was done by hand-recomputing the script's own counting logic against the current tree, not by running npm run check:facts.
· Branch

CodeWhale Bot and others added 2 commits September 15, 2026 02:51
#6105 bumped rustls 0.23.43 -> 0.23.44 and landed on main, but the advisory
still fires: cargo-deny's own output says "Solution: Upgrade to >=0.23.45".
0.23.44 is not the patched release, so every open PR kept a red
cargo-deny (advisories) and cargo-audit for a reason nobody's diff caused.

RUSTSEC-2026-0285 is "TLS 1.3 handshake messages incorrectly accepted across
encryption level boundaries" — it reaches us through every HTTP path we have
(reqwest 0.12 and 0.13, hyper-rustls, tokio-rustls, rustls-platform-verifier),
so this is the whole provider wire, not a dev-only dependency.

`cargo update -p rustls` only: one package moved, 102 unchanged dependencies
left alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This branch added two tools. `web/lib/facts.generated.ts` is derived and was
regenerated in aeabd5a, but `docs/public-surface-facts.json` holds the same
fact by hand and still said 76, so `public-surface-contract.test.ts:142` —
which exists precisely to keep the two in step — failed with
"expected 76 to be 78" and took `Lint & Type Check` down with it.

Only `sourceCandidate.toolCount` moved; version 0.9.13, providerCount 47 and
the sandbox backends already agreed with the derived facts.

The duplication is the real defect: one fact, two files, one of them manual.
Worth folding into the derive step, but not in a CI-unblocking commit.

Checks: `cd web && npm test` — "Test Files 51 passed (51) / Tests 470 passed
(470)", up from 469 passed / 1 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 57s —— View job


Review complete

Todo list

This push added exactly two new commits since the last review round (aeabd5a..HEAD): 61f4bff "deps: take rustls 0.23.45 to actually clear RUSTSEC-2026-0285" and 77e24c0 "docs: carry the tool count to 78 in the public surface matrix." Neither touches the PR's core issue slices (MCP, sessions, fleet, execpolicy, hooks). Findings from prior review rounds are all still present in HEAD and unresolved — not re-detailed here, just listed for tracking:

  • retry_lock's std::thread::yield_now() doesn't actually yield to the Tokio task holding the lock (crates/tui/src/tui/app.rs:5897, same pattern in work_graph/runtime.rs:1450).
  • recovery_hint is recomputed on every prompt refresh, contradicting the session-pinned KV-cache prefix contract, and does synchronous fs I/O on the async engine path with no spawn_blocking (crates/tui/src/core/engine.rs:6965).
  • session_search/session_get never exclude the live session before truncating to limit, and session_get resolves through resume semantics (load_session_by_prefix), which can fabricate crash-repair results for a sibling session that's still actively running (crates/tui/src/tools/session.rs:130,202) — this was independently confirmed as a 🔴 by Devin's review too.
  • tool_selection_covers_server matches any shorter configured server name as a literal prefix, no longest-match rule (crates/tui/src/mcp.rs:2617).
  • check-blocking-calls-budget.py's spawn_blocking(some_fn) bare-function-reference form undercounts real sites.
  • fleet_drift_summary does synchronous fs reads on the Tokio runtime and duplicates build_route_rows's route-resolution logic (crates/tui/src/commands/groups/config/status.rs:352).
  • goal_continuation_if_active builds its GoalBudget via GoalBudget::unbounded()..., so the #6013 hard-stop's StopReason::BudgetLimit arm is unreachable from the engine-owned continuation path (crates/tui/src/core/engine.rs:3998) — Devin flagged this 🔴 as well, independently.
  • SessionManager::resume_session/resume_session_by_prefix (#6185) do synchronous, fsync'd disk I/O and are wired into six async Tokio-runtime call sites with no spawn_blocking (session_manager.rs:2384, plus call sites in apply.rs:1332, runtime_api/sessions.rs:298, handlers.rs:1433, event_loop.rs:771, acp_server.rs:1589, runtime_threads.rs:10315).
  • 230911b's doc claim about a MAX_PENDING_BYTES size-based pacing bypass in crates/tui/src/tui/streaming/mod.rs:30-32 doesn't match any existing code.
  • analyze_workflow_plan_approval/analyze_plan_object (#6189) build the approval card from a hardcoded WorkflowConfigToml::default() instead of the real config, so the card can never show high_budget for an operator-configured default_token_budget (crates/tui/src/tools/workflow_plan_approval.rs:495, crates/tui/src/tui/approval.rs:398,571).
  • crates/tui/src/runtime_api.rs:2965's list_commands (v0.9.14: App-server typed command catalog for client slash-command surfaces #6178) and workspace_instructions's unprotected std::fs::canonicalize (crates/tui/src/runtime_api/workspace.rs:310, App-server: GET /v1/instructions for GPUI Memory/instructions (or name the real path) #6168) are fresh instances of the same blocking-call class.
  • Devin also flagged: record_session_boot_owner's sidecar read-modify-write is not serialized across concurrent sessions (session_manager.rs:1894), and FleetFile::parse's #6037 migration should be double-checked against any historical member pin that matched the operator route intentionally rather than by inheritance (fleet/store.rs:408).

New in this push

61f4bff — dependency bump only. cargo update -p rustls moves rustls 0.23.44 → 0.23.45 to actually clear RUSTSEC-2026-0285 (0.23.44 wasn't the patched release cargo-deny wanted). Cargo.lock-only diff, exactly one package version and checksum changed, matching the commit's own claim of "102 unchanged dependencies left alone." No source changes, no risk.

77e24c0 — docs-only fact sync. Updates the hand-maintained docs/public-surface-facts.json's sourceCandidate.toolCount from 76 to 78 to match the tool count already regenerated into web/lib/facts.generated.ts by aeabd5a (reviewed in the prior round and independently re-verified there against ^impl ToolSpec for counts). The commit message itself correctly names the underlying defect — this fact is duplicated by hand across two files instead of derived once — and explicitly scopes itself to unblocking CI rather than fixing that duplication, which is a reasonable call for a targeted fix.

No correctness bugs, provider/model/route safety issues, reuse/dead-code, test-coverage gaps, or security issues in this increment — both commits are mechanical and outside application logic.
· Branch

`session/new` returned `codewhale-<uuid>`; `session/list` advertised bare
uuids; `session/load` accepted only the bare form and answered `-32602` for
anything else. A client has no id for a session it just created other than the
one `session/new` handed it, so persisting and replaying that id — the normal
thing to do — could never resume the session.

The prefix was the visible seam, not the defect. `session/new` inserts only
into the process-local `self.sessions` map (a cap-64 LRU); nothing on the ACP
path writes the durable store, and `session/list`/`session/load` read that
store. So the two methods addressed disjoint namespaces, and merely stripping
the prefix would have left the bug intact with a different error message: a
bare uuid from `session/new` still has no durable record to find. That also
explains the reporter's last observation — naming a session file after the
prefixed id changed nothing, because `resolve_session_id_prefix` filters on
ids from `list_sessions()`, not on filenames.

Two changes:

- `new_session` mints a bare uuid, the shape `create_saved_session` produces
  and `session/list` advertises, so there is one id namespace instead of two.
- `load_session` resolves an id this connection already holds before consulting
  the store. That is what makes a `session/new` id loadable at all, and it also
  makes reloading an already-loaded durable session free of store side effects.

Deliberately NOT done: persisting ACP sessions on turn commit. That would make
a `session/new` id survive a restart, but it also deepens `acp_server.rs` as a
second conversation store — exactly what #6088 exists to retire, and what #5835
would have to undo when ACP moves onto the real thread/turn runtime. The
in-memory session remains honestly ephemeral: an id evicted from the cap-64 LRU
still fails, and it should.

No code or test depended on the old prefix (checked repo-wide; the other
`codewhale-` matches are remote-bridge unit names and update asset stems).

Checks: `./scripts/dev-test.sh tui acp_server` — 57 tests run: 57 passed,
12669 skipped. Includes the two new regressions and the pre-existing
`session_list_and_load_reach_the_durable_codewhale_sessions`, which still
passes, so the durable path is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
warning: Some(WHALE_IGNORED_WARNING.to_string()),
});
}
let mut scope_loaded = false;
});
}
}
let mut scope_loaded = false;
// mark the files the bounded loader's own selection walk picks.
let fragment_candidates = fragment_candidates_for(imports);
let selected: std::collections::BTreeSet<PathBuf> =
codewhale_core::fragments::selected_project_instruction_candidate_files(
},
bytes: file_len(&path),
warning: if selected_file && !loaded {
Some("selected but empty or unreadable".to_string())
path: file.clone(),
exists: true,
status: if loaded {
InstructionSourceStatus::Loaded

// `.codewhale/constitution.json`, workspace → repository root. The loader
// stops at the first existing candidate whether it parses or not, so
// later candidates — even ones that exist — are never evaluated.
`Lint` has been red on every push to this branch. CI runs the release gate
shape — `cargo clippy --workspace --all-targets --all-features` — so it lints
test targets too, which is where five of the seven were hiding.

- `runtime_api.rs`: `user_commands.get(**alias)` — explicit deref the compiler
  does anyway (`explicit_auto_deref`).
- `session_picker.rs`: `Vec::new()` then an immediate `push`
  (`vec_init_then_push`); now one `vec![]`.
- `session_manager.rs` and `tools/session.rs` (×3): `&home.join("codewhale")`
  borrowed a temporary that `EnvVarGuard::set` takes by value anyway
  (`needless_borrows_for_generic_args`).
- `markdown_render.rs`: `probe_incremental_stream_cost` prints its per-append
  timings, which `#![deny(clippy::print_stdout)]` forbids module-wide. The
  printing is the point of a probe, so the ban is lifted locally with a reason,
  exactly as `core/engine/tests.rs` already does for its four probes. Left
  running rather than silenced: it is the measurement #6196 is built on.

No behavior change; the probe keeps printing and every other edit is a
compile-time simplification.

Checks: the exact CI invocation —
`cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
-A clippy::uninlined_format_args -A clippy::too_many_arguments
-A clippy::unnecessary_map_or` — exits 0, from 7 errors.
`./scripts/dev-test.sh tui` on the same tree: 12707 tests run, 12707 passed,
19 skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 1m 27s —— View job


Review complete

Todo list

This push added exactly two new commits since the last review round (77e24c0..HEAD): 2e467d6 "acp: mint session ids session/load can resolve (#6174)" and 26b6ecb "lint: clear the seven clippy errors blocking the branch gate." Findings from the prior review rounds are all still present in HEAD and unresolved — not re-detailed here, just listed for tracking:

  • retry_lock's std::thread::yield_now() doesn't actually yield to the Tokio task holding the lock (crates/tui/src/tui/app.rs:5897, same pattern in work_graph/runtime.rs:1450).
  • recovery_hint is recomputed on every prompt refresh, contradicting the session-pinned KV-cache prefix contract, and does synchronous fs I/O on the async engine path with no spawn_blocking (crates/tui/src/core/engine.rs:6965).
  • session_search/session_get never exclude the live session before truncating to limit, and session_get resolves through resume semantics (load_session_by_prefix), which can fabricate crash-repair results for a sibling session that's still actively running (crates/tui/src/tools/session.rs:130,202) — independently confirmed 🔴 by Devin's review.
  • tool_selection_covers_server matches any shorter configured server name as a literal prefix, no longest-match rule (crates/tui/src/mcp.rs:2617).
  • check-blocking-calls-budget.py's spawn_blocking(some_fn) bare-function-reference form undercounts real sites.
  • fleet_drift_summary does synchronous fs reads on the Tokio runtime and duplicates build_route_rows's route-resolution logic (crates/tui/src/commands/groups/config/status.rs:352).
  • goal_continuation_if_active builds its GoalBudget via GoalBudget::unbounded()..., so the #6013 hard-stop's StopReason::BudgetLimit arm is unreachable from the engine-owned continuation path (crates/tui/src/core/engine.rs:3998) — independently confirmed 🔴 by Devin's review.
  • SessionManager::resume_session/resume_session_by_prefix (#6185) do synchronous, fsync'd disk I/O and are wired into async Tokio-runtime call sites with no spawn_blocking (session_manager.rs:2384, plus call sites in apply.rs:1332, runtime_api/sessions.rs:298, handlers.rs:1433, event_loop.rs:771, acp_server.rs — see below, runtime_threads.rs:10315).
  • 230911b's doc claim about a MAX_PENDING_BYTES size-based pacing bypass in crates/tui/src/tui/streaming/mod.rs:30-32 doesn't match any existing code.
  • analyze_workflow_plan_approval/analyze_plan_object (#6189) build the approval card from a hardcoded WorkflowConfigToml::default() instead of the real config (crates/tui/src/tools/workflow_plan_approval.rs:495, crates/tui/src/tui/approval.rs:398,571).
  • crates/tui/src/runtime_api.rs's list_commands (v0.9.14: App-server typed command catalog for client slash-command surfaces #6178) and workspace_instructions's unprotected std::fs::canonicalize (crates/tui/src/runtime_api/workspace.rs:310, App-server: GET /v1/instructions for GPUI Memory/instructions (or name the real path) #6168) are fresh instances of the same blocking-call class.
  • Also outstanding from Devin's review: record_session_boot_owner's sidecar read-modify-write is not serialized across concurrent sessions (session_manager.rs:1894), and FleetFile::parse's #6037 migration should be double-checked against any historical member pin that matched the operator route intentionally.

New in this push

2e467d6 (#6174) — no correctness bugs found; a well-reasoned fix. session/new now mints a bare uuid instead of a codewhale-<uuid>-prefixed id, and load_session checks the in-memory self.sessions map before consulting the durable store. I traced this for correctness:

  • No namespace collision risk in practice. session/new sessions now share the same bare-uuid shape as durable sessions (create_saved_session), but they're never written to the durable store from the ACP path, so the only way an in-memory and durable id could collide is an actual UUIDv4 collision — not a realistic concern.
  • Eviction bookkeeping is correct. self.sessions is capped at MAX_ACP_SESSIONS with a VecDeque<String> insertion_order for FIFO eviction. The new memory-hit branch in load_session (contains_key → return session_configuration) skips re-inserting into insertion_order entirely, which is more correct than before — the old code path (durable-store branch, still used for a genuinely unloaded id) unconditionally pushes to insertion_order even on a re-load of an already-present id, which was pre-existing behavior and out of scope here.
  • Doesn't weaken the reject-unknown-id path. A random uuid that is neither in self.sessions nor the durable store still correctly falls through to resume_session_by_prefix and errors, covered by the new session_load_still_rejects_an_id_no_one_minted test.
  • The load_sessionresume_session_by_prefix call site is not a new blocking-call instance — it's the exact same pre-existing call site already flagged in prior rounds (acp_server.rs, now at line ~1601 after this commit's edits) as one of the six #6185 blocking-I/O-on-async-handler sites; this commit didn't introduce or worsen it, just shifted the line number.
  • New tests (session_new_returns_an_id_that_session_load_resolves, session_load_still_rejects_an_id_no_one_minted) directly pin both the fix and the negative case, and the commit message notes the pre-existing session_list_and_load_reach_the_durable_codewhale_sessions test still passes, confirming the durable path is unaffected.

26b6ecb — mechanical clippy fixes only, no behavior change. Verified each hunk: user_commands.get(alias) drops a redundant explicit deref (explicit_auto_deref, compiler already does this coercion); Vec::new() + pushvec![...] in session_picker.rs (vec_init_then_push) preserves the exact same two-element ordering; &home.join("codewhale")home.join("codewhale") in three test helpers is a no-op deref removal since EnvVarGuard::set already takes its value by-value; #[allow(clippy::print_stdout)] on probe_incremental_stream_cost matches the existing precedent in core/engine/tests.rs for other measurement-only probes and doesn't touch the probe's actual behavior (reviewed in an earlier round of this same PR review).

No provider/model/route safety, reuse/dead-code, test-coverage, or security issues in this increment — neither commit touches provider/model selection, exec policy, or secrets.
· Branch

`Test (windows-latest)` has not compiled on this branch: five errors, all in
shell_dispatcher.rs, and all the same mistake seen from two sides.

Which `ShellKind` variants are live is exactly a platform split. `detect`
builds `Pwsh`, `WindowsPowerShell`, `Cmd` and `Bash` inside `cfg(windows)`
(:446-468) and `Sh` and `Custom` inside `cfg(not(windows))` (:471-504). The
annotations from the #5587 sweep were written from the unix side only:
the four Windows variants carried an unconditional
`cfg_attr(not(test), expect(dead_code))`, and `Sh`/`Custom` carried none.

On unix that is right and CI stayed green. On Windows both halves invert:
the four expectations go unfulfilled because those variants *are* constructed,
and `Sh`/`Custom` become genuinely unconstructed. `expect` is an error when
unfulfilled, so the lib failed to compile before a single test ran.

Each annotation now names the platform it is dead on. No variant is silenced
on a platform where it is live, so the ratchet #5587 installed still bites on
both.

Verification, stated honestly: `cargo check -p codewhale-tui --lib --locked`
is clean on unix, and the full CI clippy gate
(`--workspace --all-targets --all-features -D warnings`) exits 0. The Windows
half could NOT be checked locally — `cargo check --target
x86_64-pc-windows-msvc` cannot build `ring`, whose build script needs an
MSVC-targeting C compiler this host does not have. The Windows claim rests on
the cfg structure above, and CI is its first real test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 1m 24s —— View job


Review complete

Todo list

This push added exactly one new commit since the last review round (26b6ecb..HEAD): aca2174 "fix(tui): make ShellKind's dead_code expectations platform-accurate." It doesn't touch any of the PR's core issue slices (MCP, sessions, fleet, execpolicy, hooks) — it's a CI-breakage fix for the Test (windows-latest) job. Findings from the prior review rounds are all still present in HEAD and unresolved — not re-detailed here, just listed for tracking:

  • retry_lock's std::thread::yield_now() doesn't actually yield to the Tokio task holding the lock (crates/tui/src/tui/app.rs:5897, same pattern in work_graph/runtime.rs:1450).
  • recovery_hint is recomputed on every prompt refresh, contradicting the session-pinned KV-cache prefix contract, and does synchronous fs I/O on the async engine path with no spawn_blocking (crates/tui/src/core/engine.rs:6965).
  • session_search/session_get never exclude the live session before truncating to limit, and session_get resolves through resume semantics (load_session_by_prefix), which can fabricate crash-repair results for a sibling session that's still actively running (crates/tui/src/tools/session.rs:130,202) — independently confirmed 🔴 by Devin's review.
  • tool_selection_covers_server matches any shorter configured server name as a literal prefix, no longest-match rule (crates/tui/src/mcp.rs:2617).
  • check-blocking-calls-budget.py's spawn_blocking(some_fn) bare-function-reference form undercounts real sites.
  • fleet_drift_summary does synchronous fs reads on the Tokio runtime and duplicates build_route_rows's route-resolution logic (crates/tui/src/commands/groups/config/status.rs:352).
  • goal_continuation_if_active builds its GoalBudget via GoalBudget::unbounded()..., so the #6013 hard-stop's StopReason::BudgetLimit arm is unreachable from the engine-owned continuation path (crates/tui/src/core/engine.rs:3998) — independently confirmed 🔴 by Devin's review.
  • SessionManager::resume_session/resume_session_by_prefix (#6185) do synchronous, fsync'd disk I/O and are wired into async Tokio-runtime call sites with no spawn_blocking (session_manager.rs:2384, plus call sites in apply.rs:1332, runtime_api/sessions.rs:298, handlers.rs:1433, event_loop.rs:771, acp_server.rs, runtime_threads.rs:10315).
  • 230911b's doc claim about a MAX_PENDING_BYTES size-based pacing bypass in crates/tui/src/tui/streaming/mod.rs:30-32 doesn't match any existing code.
  • analyze_workflow_plan_approval/analyze_plan_object (#6189) build the approval card from a hardcoded WorkflowConfigToml::default() instead of the real config (crates/tui/src/tools/workflow_plan_approval.rs:495, crates/tui/src/tui/approval.rs:398,571).
  • crates/tui/src/runtime_api.rs's list_commands (v0.9.14: App-server typed command catalog for client slash-command surfaces #6178) and workspace_instructions's unprotected std::fs::canonicalize (crates/tui/src/runtime_api/workspace.rs:310, App-server: GET /v1/instructions for GPUI Memory/instructions (or name the real path) #6168) are fresh instances of the same blocking-call class.
  • Also outstanding from Devin's review: record_session_boot_owner's sidecar read-modify-write is not serialized across concurrent sessions (session_manager.rs:1894), and FleetFile::parse's #6037 migration should be double-checked against any historical member pin that matched the operator route intentionally.

New in this push (aca2174)

No correctness bugs — verified against the actual detect_shell_unlocked/unix_shell_kind code, not just the commit message's claim. I traced every non-test construction site of each ShellKind variant in crates/tui/src/shell_dispatcher.rs:

  • detect_shell_unlocked's #[cfg(windows)] branch (lines ~452-476) constructs Bash, Pwsh, WindowsPowerShell, and Cmd — all four annotated #[cfg_attr(all(not(test), not(windows)), expect(dead_code))] after this fix, i.e. dead only on non-Windows. Correct: none of these four is constructed anywhere in the #[cfg(not(windows))] path.
  • detect_shell_unlocked's #[cfg(not(windows))] branch falls back to ShellKind::Sh (line 486), and the #[cfg(not(windows))]-only unix_shell_kind helper (lines 490-514) always wraps its result in ShellKind::Custom { .. } — it never returns ShellKind::Bash, contrary to what the variant name might suggest at a glance. So Sh and Custom are annotated dead only on Windows, which is correct: neither is constructed in the #[cfg(windows)] branch.
  • This confirms the commit's central claim precisely: the pre-fix annotations were unconditional (not(test), no platform gate) except for Sh/Custom which had none at all — so on Windows, Pwsh/WindowsPowerShell/Cmd/Bash's expect(dead_code) would be unfulfilled (an error, since expect promotes to a hard failure when the lint doesn't fire) while Sh/Custom would have been flagged as genuinely dead code with no suppression. The platform-gated annotations fix both directions at once.
  • Purely additive to attribute conditions — no behavioral/runtime code changed, so no risk of altering shell-selection logic itself.

Reuse/tests: this is a minimal, surgical fix scoped to exactly the five variants that broke the Windows build; it doesn't touch or duplicate the #5587 test-only dead-code sweep's approach elsewhere. No new tests were added, which is appropriate here — the change is annotation-only and its correctness is inherently a compile-time property per platform (verified by reading the cfg structure directly, not by a runtime test) rather than something a unix-run unit test could exercise for the Windows half. The commit message is honest about the untestable half: cargo check --target x86_64-pc-windows-msvc couldn't run locally (no MSVC C toolchain for ring's build script), so the Windows claim rests on the cfg analysis above and CI is its first real check.

No provider/model/route safety, security, or reuse issues in this increment — it's a compile-fix confined to shell-detection dead-code annotations.
· Branch

Four PTY suites fail on any developer machine that happens to be running
Ollama, and pass everywhere else. The frame from a failing run says why:

    Note Provider switched: deepseek -> ollama
         Model: deepseek-flash -> nomic-embed-text:latest
         Endpoint: localhost:11434

The startup adoption probe finds a live :11434 catalog and rewrites the launch
screen — so `wait_for_text("Choose your model provider")` never matches and the
test times out. Nothing is wrong with the product here; the harness simply is
not as sealed as it claims.

`seal_home` exists so "the spawned binary cannot read or mutate the developer's
real user config", but it only seals the filesystem. This leak arrives over
loopback, so no amount of HOME redirection closes it.
`spawn_local_ollama_adoption_probe` is already inert under `cfg(test)`, and that
guard looks sufficient until you notice the PTY suites spawn the real binary,
where `cfg(test)` is not set.

So the probe gains an explicit env opt-out and `seal_home` sets it, next to the
config-path seals and for the same reason.

Scope, stated honestly: this fixes the developer-machine failure, which is the
one reproduced here. CI's failures on this branch ran 60-66s against 18s
locally, and a GitHub runner is unlikely to have Ollama on :11434 — so this may
not be the same defect CI is hitting, and CI is the test of that.

Checks: `cargo nextest run -p codewhale-tui --test cucumber --features
long-running-tests -E 'test(/_pty::/)'` — 9 tests run: 9 passed (1 slow), 28
skipped, from 5 passed / 4 failed on the same machine before this change.
CI clippy gate exits 0; `cargo fmt --check` clean on both files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 46s —— View job


Review complete

Todo list

This push added exactly one new commit since the last review round (aca2174..HEAD): 0d0a119 "test(tui): seal the PTY harness against a live local Ollama." It doesn't touch any of the PR's core issue slices (MCP, sessions, fleet, execpolicy, hooks) — it's a test-harness fix for a real, previously-noted-but-undiagnosed CI/local flake source. Findings from the prior review rounds are all still present in HEAD and unresolved — not re-detailed here, just listed for tracking:

  • retry_lock's std::thread::yield_now() doesn't actually yield to the Tokio task holding the lock (crates/tui/src/tui/app.rs:5897, same pattern in work_graph/runtime.rs:1450).
  • recovery_hint is recomputed on every prompt refresh, contradicting the session-pinned KV-cache prefix contract, and does synchronous fs I/O on the async engine path with no spawn_blocking (crates/tui/src/core/engine.rs:6965).
  • session_search/session_get never exclude the live session before truncating to limit, and session_get resolves through resume semantics (load_session_by_prefix), which can fabricate crash-repair results for a sibling session that's still actively running (crates/tui/src/tools/session.rs:130,202) — independently confirmed 🔴 by Devin's review.
  • tool_selection_covers_server matches any shorter configured server name as a literal prefix, no longest-match rule (crates/tui/src/mcp.rs:2617).
  • check-blocking-calls-budget.py's spawn_blocking(some_fn) bare-function-reference form undercounts real sites.
  • fleet_drift_summary does synchronous fs reads on the Tokio runtime and duplicates build_route_rows's route-resolution logic (crates/tui/src/commands/groups/config/status.rs:352).
  • goal_continuation_if_active builds its GoalBudget via GoalBudget::unbounded()..., so the #6013 hard-stop's StopReason::BudgetLimit arm is unreachable from the engine-owned continuation path (crates/tui/src/core/engine.rs:3998) — independently confirmed 🔴 by Devin's review.
  • SessionManager::resume_session/resume_session_by_prefix (#6185) do synchronous, fsync'd disk I/O and are wired into async Tokio-runtime call sites with no spawn_blocking (session_manager.rs:2384, plus call sites across apply.rs, runtime_api/sessions.rs, handlers.rs, event_loop.rs, acp_server.rs, runtime_threads.rs).
  • 230911b's doc claim about a MAX_PENDING_BYTES size-based pacing bypass in crates/tui/src/tui/streaming/mod.rs:30-32 doesn't match any existing code.
  • analyze_workflow_plan_approval/analyze_plan_object (#6189) build the approval card from a hardcoded WorkflowConfigToml::default() instead of the real config (crates/tui/src/tools/workflow_plan_approval.rs:495, crates/tui/src/tui/approval.rs:398,571).
  • crates/tui/src/runtime_api.rs's list_commands (v0.9.14: App-server typed command catalog for client slash-command surfaces #6178) and workspace_instructions's unprotected std::fs::canonicalize (crates/tui/src/runtime_api/workspace.rs:310, App-server: GET /v1/instructions for GPUI Memory/instructions (or name the real path) #6168) are fresh instances of the same blocking-call class.
  • Also outstanding from Devin's review: record_session_boot_owner's sidecar read-modify-write is not serialized across concurrent sessions (session_manager.rs:1894), FleetFile::parse's #6037 migration should be double-checked against any historical member pin that matched the operator route intentionally, and CodeQL flagged the new #6168 instruction-source listing route's path handling (uncontrolled-data-in-path-expression) — read-only, workspace-scoped enumeration, but worth a look at whether configured/scope_dir paths need normalization against traversal before the "Loaded"/"Skipped" status is trusted.

New in this push (0d0a119)

No correctness bugs — verified against the actual call site, not just the commit message's claim. This closes a real gap in the PTY test harness's isolation, not a product bug:

  • spawn_local_ollama_adoption_probe (crates/tui/src/local_ollama.rs:207-221) is called unconditionally from the real event loop (event_loop.rs:1480), and its existing #[cfg(test)]-gated short-circuit never applies to the PTY suites since those spawn the actual compiled binary as a subprocess, not code running in the test harness's own process — I confirmed this by checking that the qa PTY harness (tests/support/qa_harness/harness.rs) launches a real child process rather than calling into library code directly.
  • The new local_ollama_probe_disabled() gate is checked first in spawn_local_ollama_adoption_probe, alongside the pre-existing should_probe check (!should_probe || local_ollama_probe_disabled()) — correctly additive, doesn't change behavior when the env var is unset.
  • seal_home (harness.rs:125-131) sets CODEWHALE_DISABLE_LOCAL_OLLAMA_PROBE=1 alongside the other seal env vars (XDG_CONFIG_HOME, CODEWHALE_CONFIG_PATH, DEEPSEEK_CONFIG_PATH) it already sets for the same reason — filesystem sealing doesn't close a loopback-network leak, so this is the right rung-2 call: extend the existing seal mechanism rather than invent a new one.
  • No behavior change to the real product path: an operator running the actual binary without the env var set sees identical adoption-probe behavior before and after this commit.

Reuse: correctly reuses the harness's existing "seal via env var" pattern instead of adding a new isolation mechanism, and the fix is scoped to exactly the leak described (loopback probe), not a broader harness rewrite.

Tests: this commit is the test fix — the commit message reports going from 5 passed/4 failed to 9/9 passed locally with a live Ollama running on the same port the probe checks, which is a legitimate before/after repro rather than a claim taken on faith. No new test was added (nor should one be — this is an isolation fix for existing tests, not new product behavior).

No provider/model/route safety, reuse/dead-code duplication, or security issues in this increment — it's a test-isolation fix confined to a dev/CI-only probe gate.

Branch ·

@Hmbown

Hmbown commented Sep 15, 2026

Copy link
Copy Markdown
Owner Author

Overnight CI triage on this branch. Five of the six red checks are fixed; one is not mine and is described below so it does not get lost.

Fixed on this branch

Check Cause Fix
cargo-deny (advisories) / cargo-audit RUSTSEC-2026-0285 (rustls, TLS 1.3 handshake across encryption levels) 61f4bff1a2 — rustls 0.23.45. Note #6105 bumped 0.23.44 and merged to main, but cargo-deny's own output says "Upgrade to >=0.23.45", so that bump did not clear it
Lint & Type Check (web) facts.generated.ts stale at toolCount 76, workspace has 78 aeabd5a96d — regenerated via npm run prebuild
Lint & Type Check (web, second failure) docs/public-surface-facts.json holds the same fact by hand, also 76 77e24c0b5f
Lint (clippy) 7 errors, 5 of them in test targets that only --all-targets reaches — incl. a println! probe under #![deny(clippy::print_stdout)] 26b6ecba5b
Test (windows-latest) Did not compile. ShellKind's expect(dead_code) annotations were written unix-first; on Windows the four Windows variants are constructed (expectations unfulfilled) and Sh/Custom are not (dead) aca2174447

Also on the branch: 2e467d64b0 fixes #6174 (ACP session/new minting ids session/load cannot resolve), and 0d0a1196e3 seals the PTY harness against a live local Ollama — four PTY suites fail on any developer machine running Ollama on :11434, because the startup adoption probe rewrites the launch screen the tests wait for. seal_home only sealed the filesystem; that leak arrives over loopback.

Not mine: portable

portable (Pet conformance) fails at ./pet/verify.sh --no-swift on a TypeScript↔Rust frame-hash parity mismatch, not on anything in the checks above. It has been red on this branch since a06d2296 at 08:12, before any of tonight's triage, and Pet conformance has never run on main — so this branch is where it is exercised first.

The fix is very likely in the uncommitted pet_sim.rs / pet_widget.rs / pet_cameo.rs sitting in the codewhale checkout rather than here. Leaving it to whoever owns that slice: merging with it red would put failing pet parity on main, so this should not land until it is resolved or deliberately waived.

Verification

./scripts/dev-test.sh tui — 12707 tests run, 12707 passed, 19 skipped.
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings -A clippy::uninlined_format_args -A clippy::too_many_arguments -A clippy::unnecessary_map_or — exits 0.
PTY: cargo nextest run -p codewhale-tui --test cucumber --features long-running-tests -E 'test(/_pty::/)' — 9 passed, from 5 passed / 4 failed on the same machine.

One honest gap: the Windows fix could not be checked locally — cargo check --target x86_64-pc-windows-msvc cannot build ring without an MSVC-targeting C compiler. CI is its first real test.

Opening a saved session could fail with "This session belongs to another
Runtime host" while the store it names was sitting on disk, validating, and
held by nobody: `validate_existing_store` accepted it, the process-owner lock
was free, and no other Codewhale process was running.

The predicate was not wrong, it was incomplete. The switch path can rebind a
conversation but cannot carry a store's durable work into the running host —
queued tasks, pending approvals, agent mail, scope-pinned automations — so
recovery was scoped to "there is nothing on disk to abandon", i.e. a *missing*
store. That policy has no case for the state a force-quit actually leaves: a
store that exists, is ownerless, and is empty. Refusing it protected nothing
and made the session unopenable.

`RuntimeStoreBinding::has_no_durable_work` adds that case. A store qualifies
only when it is confined to `<state>/sessions/<id>/runtime[-recovered-*]` with
no symlink on the path, every work directory is empty, and `state.json`'s
sequence is still at its initial value — because events can be appended and
later pruned, and the sequence remembers. Anything unreadable, unconfined or
non-empty fails closed, so the guarantee the guard exists to protect is
unchanged; the seven pre-existing tests that pin it still pass untouched.

Deliberately NOT used as the predicate: lock-freeness. A crashed host leaves a
store unlocked whether or not it holds work, so adopting on "the owner lock is
free" would be a policy reversal that silently drops queued work, not a bug
fix. Emptiness is the discriminator that fixes the reported case without
weakening anything.

The refusal message was also describing the wrong thing — it claimed another
host owned the session, when what it means is that the saved store holds work
this process cannot carry across. It now says that.

Confinement checks are now shared by both predicates via
`is_confined_session_store` rather than duplicated.

Possibly also #6185(a): an empty transcript on resume with an intact journal is
what every early return in `apply_loaded_session_with_goal` produces, and this
guard is one of them. Asked the reporter there to confirm the status-line text
before claiming it.

Checks: `./scripts/dev-test.sh tui` — 12708 tests run, 12708 passed, 19
skipped. `./scripts/dev-test.sh tui runtime_store_binding` — 8 passed,
including the new `empty_existing_runtime_store_reports_no_durable_work`, which
asserts each work directory is load-bearing on its own so a directory added to
`open` without being added to the list is caught. CI clippy gate exits 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 1m 33s —— View job


Review complete

Todo list

This push added exactly one new commit since the last review round (0d0a119..HEAD): 68431ba "fix(tui): adopt an existing Runtime store that holds no work (#6207)." It doesn't touch any of the PR's core issue slices (MCP, sessions recall, fleet, execpolicy, hooks) — it's a session-switch bug fix for a real, previously-unrelated regression. Findings from the prior review rounds are all still present in HEAD and unresolved — not re-detailed here, just listed for tracking:

  • retry_lock's std::thread::yield_now() doesn't actually yield to the Tokio task holding the lock (crates/tui/src/tui/app.rs:5897, same pattern in work_graph/runtime.rs:1450).
  • recovery_hint is recomputed on every prompt refresh, contradicting the session-pinned KV-cache prefix contract, and does synchronous fs I/O on the async engine path with no spawn_blocking (crates/tui/src/core/engine.rs:6965).
  • session_search/session_get never exclude the live session before truncating to limit, and session_get resolves through resume semantics (load_session_by_prefix), which can fabricate crash-repair results for a sibling session that's still actively running (crates/tui/src/tools/session.rs:130,202) — independently confirmed 🔴 by Devin's review.
  • tool_selection_covers_server matches any shorter configured server name as a literal prefix, no longest-match rule (crates/tui/src/mcp.rs:2617).
  • check-blocking-calls-budget.py's spawn_blocking(some_fn) bare-function-reference form undercounts real sites.
  • fleet_drift_summary does synchronous fs reads on the Tokio runtime and duplicates build_route_rows's route-resolution logic (crates/tui/src/commands/groups/config/status.rs:352).
  • goal_continuation_if_active builds its GoalBudget via GoalBudget::unbounded()..., so the #6013 hard-stop's StopReason::BudgetLimit arm is unreachable from the engine-owned continuation path (crates/tui/src/core/engine.rs:3998) — independently confirmed 🔴 by Devin's review.
  • SessionManager::resume_session/resume_session_by_prefix (#6185) do synchronous, fsync'd disk I/O and are wired into async Tokio-runtime call sites with no spawn_blocking (session_manager.rs:2384, plus call sites across apply.rs, runtime_api/sessions.rs, handlers.rs, event_loop.rs, acp_server.rs, runtime_threads.rs).
  • 230911b's doc claim about a MAX_PENDING_BYTES size-based pacing bypass in crates/tui/src/tui/streaming/mod.rs:30-32 doesn't match any existing code.
  • analyze_workflow_plan_approval/analyze_plan_object (#6189) build the approval card from a hardcoded WorkflowConfigToml::default() instead of the real config (crates/tui/src/tools/workflow_plan_approval.rs:495, crates/tui/src/tui/approval.rs:398,571).
  • crates/tui/src/runtime_api.rs's list_commands (v0.9.14: App-server typed command catalog for client slash-command surfaces #6178) and workspace_instructions's unprotected std::fs::canonicalize (crates/tui/src/runtime_api/workspace.rs:310, App-server: GET /v1/instructions for GPUI Memory/instructions (or name the real path) #6168) are fresh instances of the same blocking-call class.
  • Also outstanding from Devin's review: record_session_boot_owner's sidecar read-modify-write is not serialized across concurrent sessions (session_manager.rs:1894), FleetFile::parse's #6037 migration should be double-checked against any historical member pin that matched the operator route intentionally, and CodeQL flagged the #6168 instruction-source listing route's path handling (uncontrolled-data-in-path-expression) — read-only, workspace-scoped enumeration, worth a look at whether path normalization is needed before status is trusted.

New in this push (68431ba, #6207)

No correctness bugs — verified against the actual predicate and its call site, not just the commit message's claim.

This fixes a real regression class: opening a saved session could fail with "This session belongs to another Runtime host" even when the on-disk store was ownerless and held nothing, because the old is_missing_session_store predicate only covered a missing store, not an empty one — exactly the shape a force-quit leaves.

Specifics checked:

  • next_seq semantics confirmed correct. RuntimeStoreState::default() sets next_seq: 1 (runtime_threads.rs:1636), and append_event_transaction consumes the current next_seq for the new event's seq and increments it afterward (runtime_threads.rs:2551-2554). So next_seq <= 1 precisely means "zero events have ever been appended," including the case where an event was appended and later pruned — the doc comment's claim ("the sequence remembers") is accurate, not just asserted.
  • The seven RUNTIME_STORE_WORK_DIRS entries match RuntimeThreadStore::open's actual directory creation exactly (threads, turns, items, events, goals, agent-mail, turn-operationsruntime_threads.rs:1737-1750), so the emptiness check can't silently miss a work category the real store creates.
  • state.json always exists after open (runtime_threads.rs:1782-1786: written via write_json_atomic with RuntimeStoreState::default() if absent), so the Err(NotFound) => Ok(true) fallback in has_no_durable_work only fires for a store that predates this file format — still correctly "no work," not a loosened check.
  • Confinement logic is correctly shared, not duplicated. is_confined_session_store is extracted verbatim from the old is_missing_session_store body (symlink rejection on sessions, session_dir, data_dir, and the parent-path checks), and both is_missing_session_store and has_no_durable_work call it first and fail closed (Ok(false)) on anything unconfined — no behavior change to the original missing-store guarantee, matching the commit's claim that the seven pre-existing tests still pass untouched.
  • Fails closed in every direction that matters: a non-empty work directory, an advanced next_seq, an unreadable file, or an unconfined path all return Ok(false) (not adoptable) rather than defaulting to "safe to adopt." I didn't find a path where genuine durable work could be silently dropped.
  • The call site's nothing_to_abandon OR-combination is correct: is_missing_session_store() and has_no_durable_work() are mutually exclusive by construction (the latter requires self.data_dir.is_dir()), so there's no overlap or double-counting risk in apply.rs:3629-3634.
  • The refusal message was also fixed to match reality — it previously claimed "another host owns this session" (never true in the reported case, since no other process was running); it now correctly describes unrecoverable durable work.

Reuse: the refactor is a clean rung-2 move — is_confined_session_store deduplicates logic that would otherwise drift between the two predicates, exactly the kind of shared-checks extraction this repo's AGENTS.md asks for. Deliberately not using lock-freeness as the adoption signal (explained in the commit message) is the right call — a crashed host also leaves the lock free, so that would have been a silent policy loosening rather than a targeted fix.

Tests: empty_existing_runtime_store_reports_no_durable_work opens a real RuntimeThreadStore (not a hand-built directory layout) and then individually round-trips each of the seven work directories (write a marker → assert not-adoptable → remove marker → assert adoptable again), which is exactly the check that would catch a future work directory being added to open() without being added to RUNTIME_STORE_WORK_DIRS — the test's own stated purpose. It also separately pins the pruned-events case (state.json with next_seq: 5 but empty directories) and the unconfined-path fail-closed case. Proportionate, well-targeted coverage for a fail-closed security/data-integrity-adjacent predicate.

No provider/model/route safety or security issues in this increment — it's confined to session-store adoption policy, not provider/model selection.

Note: No Bash/cargo execution beyond local git inspection was available in this session, so cargo check/tests could not be run to independently verify the commit's claimed 12708 passed — findings above are from static reading of the diff and cross-referencing RuntimeStoreState's actual serialization/increment behavior, not a build.

Branch ·

…6207)"

This reverts commit 68431ba.

CI caught what my local runs did not: `runtime_store_binding_survives_launch_snapshot_and_resume`
fails at its line 284 assertion on macOS and ubuntu, while passing in isolation
here even under `--all-features`. Passing alone and failing under the full
suite is the signature of a race, and the race is in the predicate I added.

`has_no_durable_work` reads emptiness at one instant. It has no liveness
check, so a store that is empty *right now* because the host holding it has
not flushed yet is indistinguishable from a store that is empty because it was
abandoned. That test has a live foreign TaskManager holding the store; under
load the adopt path wins the race, the switch succeeds where it must refuse,
and the assertion sees a different error.

Two further gaps, both mine:

- The predicate only walks the runtime store's own directories. Scope-pinned
  automations are recorded outside them, so an automation whose execution
  scope matches the binding is durable work my check cannot see. The review
  that scoped this said the predicate needed "no automation whose
  execution_scope matches the binding"; I did not implement that.
- I reported that the seven pre-existing tests "pass untouched". That was true
  of the run I did and false in general, and the distinction matters precisely
  here.

This sits on a data-loss boundary — queued tasks, approvals, agent mail,
automations — so it comes out rather than being patched forward at speed.
#6207 stays open with the corrected design: adopt only when the store is both
provably empty *and* provably unheld, with automation scope included in
"empty", and the shape of the liveness check settled before any of it is
written.

The behaviour returns to refusing, which is the pre-existing bug the issue
reports — a worse user experience than the fix, and a better one than silently
dropping someone's scheduled work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWzjx9Q7Mw2G7K8rpiJy9p
@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 59s —— View job


Review complete

Todo list

This push added exactly one new commit since the last review round (68431ba..HEAD): bc77a0f, a self-revert of 68431ba ("fix(tui): adopt an existing Runtime store that holds no work", #6207) — the commit reviewed favorably last round. I verified this is a clean, exact revert: git diff 68431ba~1 bc77a0f is empty and git diff 0d0a119..HEAD --shortstat is also empty, so HEAD is byte-identical to the tree from before #6207 ever landed — no partial rollback, no stray leftover code.


Why it was reverted (author's own diagnosis, and it holds up)

The commit message is unusually precise about its own failure, and it's worth taking at face value rather than re-deriving independently — it names a real race the prior review round's static analysis couldn't have caught:

  • runtime_store_binding_survives_launch_snapshot_and_resume failed on its line-284 assertion on both macOS and ubuntu CI runners, while passing in isolation locally (including under --all-features) — that pass-alone/fail-under-load signature is a textbook race, and the author correctly locates it in the new predicate rather than in the test.
  • Root cause: has_no_durable_work reads store emptiness at a single instant with no liveness check. A store that's empty right now because the actual owning host hasn't flushed its first write yet is indistinguishable from a store that's empty because it was genuinely abandoned. The test's live foreign TaskManager holding the store exposes exactly this: under load, the adopt path can win the race and incorrectly permit the switch.
  • Two further gaps the author names against themselves: (1) the predicate only walks the runtime store's own seven work directories, but scope-pinned automations are recorded outside them — an automation whose execution_scope matches the binding is durable work the predicate can't see, even though the review that scoped #6207 explicitly called for that check and it was never implemented; (2) the commit's original claim that "the seven pre-existing tests pass untouched" was true of the run performed but not true in general, which matters here specifically because that's the class of claim AGENTS.md's "Claiming a test passed" section exists to guard against.

This is a good outcome for the branch: a CI-only race (empty-by-timing vs. empty-by-abandonment) surfaced after merge, and rather than patching around it or weakening the test, the author reverted cleanly and wrote down precisely what was wrong and what a correct fix would still need (a liveness check, plus the missing automation-scope check). That's exactly the "misconfiguration fails loud" / write-down-what-a-design-does-not-do discipline this same branch's own AGENTS.md additions call for.

No new correctness bugs, provider/model/route safety issues, reuse/dead-code, or security issues in this increment — it's a pure revert.


Standing findings (unchanged, still present in HEAD)

All findings from prior review rounds remain outstanding and are not re-detailed here:

  • retry_lock's std::thread::yield_now() doesn't actually yield to the Tokio task holding the lock (crates/tui/src/tui/app.rs:5897, same pattern in work_graph/runtime.rs:1450).
  • recovery_hint is recomputed on every prompt refresh, contradicting the session-pinned KV-cache prefix contract, and does synchronous fs I/O on the async engine path with no spawn_blocking (crates/tui/src/core/engine.rs:6965).
  • session_search/session_get never exclude the live session before truncating to limit, and session_get resolves through resume semantics (load_session_by_prefix), which can fabricate crash-repair results for a sibling session that's still actively running (crates/tui/src/tools/session.rs:130,202) — independently confirmed 🔴 by Devin's review.
  • tool_selection_covers_server matches any shorter configured server name as a literal prefix, no longest-match rule (crates/tui/src/mcp.rs:2617).
  • check-blocking-calls-budget.py's spawn_blocking(some_fn) bare-function-reference form undercounts real sites.
  • fleet_drift_summary does synchronous fs reads on the Tokio runtime and duplicates build_route_rows's route-resolution logic (crates/tui/src/commands/groups/config/status.rs:352).
  • goal_continuation_if_active builds its GoalBudget via GoalBudget::unbounded()..., so the #6013 hard-stop's StopReason::BudgetLimit arm is unreachable from the engine-owned continuation path (crates/tui/src/core/engine.rs:3998) — independently confirmed 🔴 by Devin's review.
  • SessionManager::resume_session/resume_session_by_prefix (#6185) do synchronous, fsync'd disk I/O and are wired into async Tokio-runtime call sites with no spawn_blocking (session_manager.rs:2384, plus call sites across apply.rs, runtime_api/sessions.rs, handlers.rs, event_loop.rs, acp_server.rs, runtime_threads.rs).
  • 230911b's doc claim about a MAX_PENDING_BYTES size-based pacing bypass in crates/tui/src/tui/streaming/mod.rs:30-32 doesn't match any existing code.
  • analyze_workflow_plan_approval/analyze_plan_object (#6189) build the approval card from a hardcoded WorkflowConfigToml::default() instead of the real config (crates/tui/src/tools/workflow_plan_approval.rs:495, crates/tui/src/tui/approval.rs:398,571).
  • crates/tui/src/runtime_api.rs's list_commands (v0.9.14: App-server typed command catalog for client slash-command surfaces #6178) and workspace_instructions's unprotected std::fs::canonicalize (crates/tui/src/runtime_api/workspace.rs:310, App-server: GET /v1/instructions for GPUI Memory/instructions (or name the real path) #6168) are fresh instances of the same blocking-call class.
  • Also outstanding from Devin's review: record_session_boot_owner's sidecar read-modify-write is not serialized across concurrent sessions (crates/tui/src/session_manager.rs:1894), FleetFile::parse's #6037 migration should be double-checked against any historical member pin that matched the operator route intentionally, and CodeQL flagged the #6168 instruction-source listing route's path handling (uncontrolled-data-in-path-expression) — read-only, workspace-scoped enumeration, worth a look at whether path normalization is needed before the "Loaded"/"Skipped" status is trusted.
    · Branch

@Hmbown

Hmbown commented Sep 15, 2026

Copy link
Copy Markdown
Owner Author

Update, including a retraction.

Reverted: 68431ba097 (the #6207 store-adoption fix) is reverted in bc77a0ff5d. CI caught a race my local runs did not: the predicate I added samples store emptiness with no liveness check, so a store that is empty only because the holding host has not flushed yet is indistinguishable from an abandoned one. runtime_store_binding_survives_launch_snapshot_and_resume failed on macOS and ubuntu at line 284 while passing here in isolation — the classic ordering-dependent signature. It also missed scope-pinned automations, which live outside the runtime store tree. Full reasoning on #6207. That is a data-loss boundary, so it comes out rather than being patched forward at speed.

I also need to correct something I wrote earlier in this thread: I said the seven pre-existing binding tests "pass untouched", offering that as evidence the policy widened by exactly the empty case. A green targeted run is not evidence about a test whose failure mode is ordering-dependent, and I presented it as though it were.

Codewhale review is not a code failure. It is a spend gate:

Complete PR review requires 5 passes at 200000 characters per pass, but max_passes is 1.
Opt in with max_passes/--max-passes of at least 5 only after approving the provider spend and run duration.

Raising it needs a human to approve provider spend, so I have not. It is also a size signal — this branch is large enough to need five review passes.

Still standing from the earlier triage: the rustls advisory (61f4bff1a2), both web fact staleness fixes, the seven clippy errors, the Windows compile fix (aca2174447 — and Test (windows-latest) has since progressed past the compile step that was failing), the #6174 ACP session-id fix, and the PTY Ollama seal.

Still not mine: portable, the pet TypeScript↔Rust frame-hash parity mismatch, red since a06d2296 at 08:12 and never exercised on main.

// uuid shape `session/list` advertises for durable sessions.
assert!(
!session_id.starts_with("codewhale-"),
"session/new must not mint a prefixed id, got {session_id}"
"session/new must not mint a prefixed id, got {session_id}"
);
uuid::Uuid::parse_str(&session_id)
.unwrap_or_else(|e| panic!("session/new must mint a bare uuid, got {session_id}: {e}"));
`Test (windows-latest)` compiles again after aca2174 and now runs, which
surfaced the next thing: 15271 tests, 1 failed —
`get_v1_workspace_instructions_lists_effective_sources` panicking on
"shadowed workspace instructions row".

Two separator bugs, one in the contract and one in the test.

The contract: `relative_path` was built with `relative.display().to_string()`,
which emits the native separator. A wire field describing a repo-relative path
would then read `.codewhale/rules/style.md` from a Unix host and
`.codewhale\rules\style.md` from a Windows one, forcing every client to branch
on the server's OS. It is now joined with forward slashes on all platforms.
The absolute `path` alongside it stays native, because that one is only
meaningful on the machine that produced it. No consumer reads this field yet —
the route landed hours ago in d5291df and GPUI has not wired it — so the
contract is free to fix now and would not have been later.

The test: the shadowed-row finder matched the *native* `path` against the
literal `".codewhale/instructions.md"`, so on Windows it never matched and the
`.expect` fired. It now compares separator-agnostically. Note this does not
weaken the assertion — if that row is absent for any other reason the test
still fails, which matters because the Windows half of this cannot be
reproduced here.

The neighbouring `AGENTS.md` assertion passed only because that literal happens
to contain no separator.

Checks: `./scripts/dev-test.sh tui workspace_instructions` — 1 passed;
`./scripts/dev-test.sh tui runtime_api` — 243 tests run, 243 passed, 12483
skipped. `cargo fmt --check` clean on both files. The Windows half rests on the
separator reasoning above and on CI, for the same reason as aca2174: this
host cannot build for `x86_64-pc-windows-msvc`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWzjx9Q7Mw2G7K8rpiJy9p
@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 45s —— View job


Review complete

Todo list

This push added exactly one new commit since the last review round (bc77a0f..HEAD): b614421 "fix(runtime-api): make workspace instruction paths platform-independent" — a Windows CI follow-up to the #6168 instruction-source listing route (reviewed two rounds ago). Findings from the prior review rounds are all still present in HEAD and unresolved — not re-detailed here, just listed for tracking:

  • retry_lock's std::thread::yield_now() doesn't actually yield to the Tokio task holding the lock (crates/tui/src/tui/app.rs:5897, same pattern in work_graph/runtime.rs:1450).
  • recovery_hint is recomputed on every prompt refresh, contradicting the session-pinned KV-cache prefix contract, and does synchronous fs I/O on the async engine path with no spawn_blocking (crates/tui/src/core/engine.rs:6965).
  • session_search/session_get never exclude the live session before truncating to limit, and session_get resolves through resume semantics (load_session_by_prefix), which can fabricate crash-repair results for a sibling session that's still actively running (crates/tui/src/tools/session.rs:130,202) — independently confirmed 🔴 by Devin's review.
  • tool_selection_covers_server matches any shorter configured server name as a literal prefix, no longest-match rule (crates/tui/src/mcp.rs:2617).
  • check-blocking-calls-budget.py's spawn_blocking(some_fn) bare-function-reference form undercounts real sites.
  • fleet_drift_summary does synchronous fs reads on the Tokio runtime and duplicates build_route_rows's route-resolution logic (crates/tui/src/commands/groups/config/status.rs:352).
  • goal_continuation_if_active builds its GoalBudget via GoalBudget::unbounded()..., so the #6013 hard-stop's StopReason::BudgetLimit arm is unreachable from the engine-owned continuation path (crates/tui/src/core/engine.rs:3998) — independently confirmed 🔴 by Devin's review.
  • SessionManager::resume_session/resume_session_by_prefix (#6185) do synchronous, fsync'd disk I/O and are wired into async Tokio-runtime call sites with no spawn_blocking (session_manager.rs:2384, plus call sites across apply.rs, runtime_api/sessions.rs, handlers.rs, event_loop.rs, acp_server.rs, runtime_threads.rs).
  • analyze_workflow_plan_approval/analyze_plan_object (#6189) build the approval card from a hardcoded WorkflowConfigToml::default() instead of the real config (crates/tui/src/tools/workflow_plan_approval.rs:495, crates/tui/src/tui/approval.rs:398,571).
  • crates/tui/src/runtime_api.rs's list_commands (v0.9.14: App-server typed command catalog for client slash-command surfaces #6178) and workspace_instructions's unprotected std::fs::canonicalize (crates/tui/src/runtime_api/workspace.rs:310 — still present, untouched by this commit) are fresh instances of the blocking-call class #6149 targets.
  • Also outstanding from Devin's review: record_session_boot_owner's sidecar read-modify-write is not serialized across concurrent sessions (session_manager.rs:1894), FleetFile::parse's #6037 migration should be double-checked against any historical member pin that matched the operator route intentionally, and CodeQL flagged the #6168 instruction-source listing route's path handling (uncontrolled-data-in-path-expression) — read-only, workspace-scoped enumeration.

New in this push (b614421)

No correctness bugs — this is a clean, well-targeted fix. It closes the gap aca2174 (the prior Windows compile fix) exposed: once Test (windows-latest) could compile, it ran and failed on a real cross-platform separator bug.

Verified both halves:

  • The contract fix is correct. relative_path now joins Path::components() with "/" instead of relative.display().to_string() (which emits \ on Windows). Since relative is the result of source.path.strip_prefix(&workspace_root), its components are always plain path segments (no ../root components to mis-render), so to_string_lossy()-and-join is a safe, complete fix — not a partial workaround. The absolute path field is correctly left native, since (per the added comment) it's only meaningful on the machine that produced it, and the commit correctly notes no consumer reads relative_path yet, so this is a free, non-breaking fix rather than a later migration.
  • The test fix doesn't weaken the assertion. The shadowed-row finder now compares path.replace('\\', "/").ends_with(".codewhale/instructions.md") instead of a bare ends_with against the native path. This is separator-normalization, not a loosened match — a path missing for any other reason still fails the .expect. The commit message's own reasoning (the neighboring AGENTS.md assertion only passed by accident, since that literal has no separator to expose the bug) is accurate and worth having documented.
  • Scope check: git show --stat confirms only these two files changed (workspace_instructions.rs test, workspace.rs handler) — no unrelated changes bundled in.
  • The previously-flagged unprotected std::fs::canonicalize(&state.workspace) at workspace.rs:310 (right above the code this commit touches) is untouched by this commit — still a fresh instance of the #6149 blocking-call class, not fixed here, not worsened either.

Tests: the fix is proportionate — no new test was added (nor warranted), since the existing test's own assertion was the thing that needed to become platform-correct, and the commit is explicit about the Windows half being unverifiable locally (no MSVC toolchain on this host) and resting on CI as the first real check, consistent with the same honest gap noted in aca2174.

No provider/model/route safety, reuse/dead-code, or security issues in this increment — it's confined to a read-only listing route's wire-format serialization.
· Branch

…indow (#6224)

`Test (windows-latest)` is a required check and this was the one test failing
it: 15271 run, 1 failed, green on ubuntu and macOS.

    assertion failed: sink.failed()

The cause is a wall clock in the code under test, not flakiness. `Target::send`
drops stale audio on purpose:

    fn current(&self) -> bool { self.requested.elapsed() <= Duration::from_millis(500) }
    ...
    if !self.current() { return Ok(()); }          // never reaches try_send

and `failed` is only set when `try_send` observes `Disconnected`. `sink.target()`
stamps `requested` at call time, so the test was implicitly requiring the worker
to consume that target within 500 ms. On a slower Windows runner it does not,
`send` returns early, no failure is ever recorded, and the precondition assert
fires.

The staleness guard is correct product behaviour — audio requested half a
second ago should not play — so the test is what needed fixing, not `send`.

The real assertion in this test is at the end (`after.time_ms > before.time_ms`,
`after.hollow`): output failures must not stop the world. `sink.failed()` is
only the precondition that a failure happened at all. That precondition now
runs through the same path with the wall clock removed — a fresh target, a live
`active()`, and a `try_send` that observes `Disconnected` because `packets` was
dropped. It is not weakened: a receiver that failed to disconnect still fails
the test, and everything downstream is untouched.

Checks: `./scripts/dev-test.sh tui pet_watch` — 16 tests run, 16 passed;
`./scripts/dev-test.sh tui output_failures_do_not_stop_the_world` — 1 passed.
CI clippy gate (`--workspace --all-targets --all-features -D warnings`) exits 0;
`cargo fmt --check` clean. The Windows half rests on the mechanism above and on
CI, since this host cannot build for `x86_64-pc-windows-msvc`.

Touches another agent's subsystem, so it is one assertion and a comment, with
no change to `audio.rs` or to the worker.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWzjx9Q7Mw2G7K8rpiJy9p
@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 1m 15s —— View job


Review complete

Todo list

This push added exactly one new commit since the last review round (b614421..HEAD): be6b7fe "fix(pet): stop output_failures_do_not_stop_the_world racing a 500ms window (#6224)" — a Windows CI flake fix in the pet-watch audio test suite. It doesn't touch any of the PR's core issue slices (MCP, sessions, fleet, execpolicy, hooks). Findings from the prior review rounds are all still present in HEAD and unresolved — not re-detailed here, just listed for tracking:

  • retry_lock's std::thread::yield_now() doesn't actually yield to the Tokio task holding the lock (crates/tui/src/tui/app.rs:5897, same pattern in work_graph/runtime.rs:1450).
  • recovery_hint is recomputed on every prompt refresh, contradicting the session-pinned KV-cache prefix contract, and does synchronous fs I/O on the async engine path with no spawn_blocking (crates/tui/src/core/engine.rs:6965).
  • session_search/session_get never exclude the live session before truncating to limit, and session_get resolves through resume semantics (load_session_by_prefix), which can fabricate crash-repair results for a sibling session that's still actively running (crates/tui/src/tools/session.rs:130,202) — independently confirmed 🔴 by Devin's review.
  • tool_selection_covers_server matches any shorter configured server name as a literal prefix, no longest-match rule (crates/tui/src/mcp.rs:2617).
  • check-blocking-calls-budget.py's spawn_blocking(some_fn) bare-function-reference form undercounts real sites.
  • fleet_drift_summary does synchronous fs reads on the Tokio runtime and duplicates build_route_rows's route-resolution logic (crates/tui/src/commands/groups/config/status.rs:352).
  • goal_continuation_if_active builds its GoalBudget via GoalBudget::unbounded()..., so the #6013 hard-stop's StopReason::BudgetLimit arm is unreachable from the engine-owned continuation path (crates/tui/src/core/engine.rs:3998) — independently confirmed 🔴 by Devin's review.
  • SessionManager::resume_session/resume_session_by_prefix (#6185) do synchronous, fsync'd disk I/O and are wired into async Tokio-runtime call sites with no spawn_blocking (session_manager.rs:2384, plus call sites across apply.rs, runtime_api/sessions.rs, handlers.rs, event_loop.rs, acp_server.rs, runtime_threads.rs).
  • analyze_workflow_plan_approval/analyze_plan_object (#6189) build the approval card from a hardcoded WorkflowConfigToml::default() instead of the real config (crates/tui/src/tools/workflow_plan_approval.rs:495, crates/tui/src/tui/approval.rs:398,571).
  • crates/tui/src/runtime_api.rs's list_commands (v0.9.14: App-server typed command catalog for client slash-command surfaces #6178) and workspace_instructions's unprotected std::fs::canonicalize (crates/tui/src/runtime_api/workspace.rs:310) are fresh instances of the blocking-call class #6149 targets.
  • Also outstanding from Devin's review: record_session_boot_owner's sidecar read-modify-write is not serialized across concurrent sessions (session_manager.rs:1894), FleetFile::parse's #6037 migration should be double-checked against any historical member pin that matched the operator route intentionally, and CodeQL flagged the #6168 instruction-source listing route's path handling (uncontrolled-data-in-path-expression).

New in this push (be6b7fe, #6224)

No correctness bugs — this is a clean, well-diagnosed fix. I traced the mechanism independently against crates/tui/src/tui/pet_watch/audio.rs rather than trusting the commit message:

  • The root-cause diagnosis is accurate. Target::send (audio.rs:49-82) returns early with Ok(()) at line 65 (if !self.current() { return Ok(()); }) whenever requested.elapsed() > 500ms — it never reaches try_send, so fail() is never called and Control::failed stays false. The old test relied on the worker's background thread consuming sink.target() (stamped at the frame_with_audio call) inside that 500ms window; a slow Windows runner can miss it, leaving sink.failed() false and failing the test's own precondition assertion — not a flake in the product, a race in the test's implicit timing assumption.
  • The fix removes the race without weakening the real assertion. The new line calls sink.target() (a fresh Target, requested = Instant::now() at that exact call site) and .send(...) synchronously in the test thread, bypassing the worker's background consumption entirely. Tracing Target::send's branches for this call: active() is true (or, if a prior frame_with_audio call already raced to failure, already false — either way send returns Err(())), current() is true (just constructed), and try_send on the disconnected channel (packets was dropped earlier) hits Err(TrySendError::Disconnected(_)), which calls self.fail() and returns Err(()). So sink.target().send(...).is_err() is true deterministically, and sink.failed() is true immediately after — no dependence on worker scheduling.
  • Downstream assertions are unaffected and still meaningful. Control is one Arc shared across every Target cloned from the same Output (same_stream/control: Arc<Control>), so forcing failed via a direct call has the identical effect as the worker naturally observing the disconnect — the later frame_with_audio(&worker, 800.0, Some(sink.target()))'s after.hollow assertion still reads the same shared Control::failed flag, just set deterministically instead of racily. I checked worker.rs:188-202's Command::Export handler: Notice::ExportFailed in this test actually fires because session is None (Worker::start(None), "No saved session"), independent of the audio failure — that's pre-existing test structure, not something this commit touches or depends on.
  • Scope matches the commit's own framing. git show --stat confirms exactly one file, 12 lines, all additions, no change to audio.rs's Target::send/current() staleness guard (correctly identified as intentional product behavior, not a bug) and no change to the worker.

Tests: this commit is the test fix — proportionate, and the commit message's own repro numbers (15271 run/1 failed pre-fix, isolated to Windows) are consistent with a genuine platform-timing gap rather than general flakiness. No new test was warranted beyond hardening the existing one, and the Windows half is honestly flagged as unverifiable locally (no MSVC toolchain on this host), resting on CI — consistent with the same honest gap noted in this branch's earlier Windows-only fixes (aca2174, b614421).

No provider/model/route safety, reuse/dead-code, or security issues in this increment — it's confined to a test's precondition setup in an unrelated (pet-watch audio) subsystem.
· Branch

`portable` (Pet conformance) has never passed. It did not regress at
`a06d2296`; that is only when the workflow started running. The failure goes
back to `334c720057`, whose message claims "380 matching checkpoints across
v1/v2, with pinned v1 digests unchanged" — a gate that was not true of the
content it committed. `pet_sim.rs` is byte-identical at that commit and at
HEAD, the v1 fixtures were created there and never changed, and Rust has never
matched them.

The cause is not the simulation. `field_target` is line-for-line identical
between `pet_sim.rs` and `pet/src/core/pet-sim.ts` — every channel, every
constant — and both mulberry32 implementations agree (Rust's `u32`
`wrapping_*` is exactly `Math.imul`/`>>>`). The tape's label sequence matched
byte-for-byte all along: the two cores agreed on what the pet was doing and
disagreed on where its particles were, from frame 0.

The authored body is checked in **five** times:

    pet/whale-points.tsv                            f54c19a76021
    pet/dist/whale-points.tsv                       f54c19a76021
    pet/rs/whale-points.tsv                         f54c19a76021
    pet/public/whale-points.tsv                     f54c19a76021
    crates/tui/src/tui/ambient_life/whale-points.tsv   9e56d5280d9b

Four agree; the Rust product core — the one the TUI actually renders — was the
lone dissenter, with a different derivation of the same 980-point shape. No
sync step produces these files, so the existing `git diff --exit-code` guard on
embedded sources could never see the drift.

Rust is canonical (founder's call), so the four pet copies are synced to it and
the v1 fixtures are regenerated from the Rust binary. That makes verify.sh's
fixture check an assertion that TypeScript matches the Rust-derived contract,
rather than TypeScript agreeing with itself.

A CI step now compares the copies directly, because nothing else can: this is
five hand-maintained copies of one fact, and the drift was invisible for days.
Folding them into one generated artifact is the real fix and is left as
follow-up.

Checks: `./pet/verify.sh --no-swift` exits 0 — 380 checkpoints across 2
expression versions and 4 tapes/modes, all eight combinations PASS, up from a
hard failure on the first v1 diff. `npm --prefix pet run check` — 68 pass, 0
fail. `npm --prefix pet run sync` leaves `git diff --exit-code` on the embedded
sources clean.

Refs #6227

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWzjx9Q7Mw2G7K8rpiJy9p
@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 2m 9s —— View job


Review complete

Todo list

This push added exactly one new commit since the last review round (be6b7fe..HEAD): a300b52 "fix(pet): one whale body — reconcile the cores and make conformance pass" (#6227) — this is the fix for the portable (Pet conformance) check that Hmbown's status comment explicitly flagged as "not mine" and unresolved. Findings from the prior review rounds are all still present in HEAD and unresolved — not re-detailed here, just listed for tracking:

  • retry_lock's std::thread::yield_now() doesn't actually yield to the Tokio task holding the lock (crates/tui/src/tui/app.rs:5897, same pattern in work_graph/runtime.rs:1450).
  • recovery_hint is recomputed on every prompt refresh, contradicting the session-pinned KV-cache prefix contract, and does synchronous fs I/O on the async engine path with no spawn_blocking (crates/tui/src/core/engine.rs:6965).
  • session_search/session_get never exclude the live session before truncating to limit, and session_get resolves through resume semantics (load_session_by_prefix), which can fabricate crash-repair results for a sibling session that's still actively running (crates/tui/src/tools/session.rs:130,202) — independently confirmed 🔴 by Devin's review.
  • tool_selection_covers_server matches any shorter configured server name as a literal prefix, no longest-match rule (crates/tui/src/mcp.rs:2617).
  • check-blocking-calls-budget.py's spawn_blocking(some_fn) bare-function-reference form undercounts real sites.
  • fleet_drift_summary does synchronous fs reads on the Tokio runtime and duplicates build_route_rows's route-resolution logic (crates/tui/src/commands/groups/config/status.rs:352).
  • goal_continuation_if_active builds its GoalBudget via GoalBudget::unbounded()..., so the #6013 hard-stop's StopReason::BudgetLimit arm is unreachable from the engine-owned continuation path (crates/tui/src/core/engine.rs:3998) — independently confirmed 🔴 by Devin's review.
  • SessionManager::resume_session/resume_session_by_prefix (#6185) do synchronous, fsync'd disk I/O and are wired into async Tokio-runtime call sites with no spawn_blocking (session_manager.rs:2384, plus call sites across apply.rs, runtime_api/sessions.rs, handlers.rs, event_loop.rs, acp_server.rs, runtime_threads.rs).
  • analyze_workflow_plan_approval/analyze_plan_object (#6189) build the approval card from a hardcoded WorkflowConfigToml::default() instead of the real config (crates/tui/src/tools/workflow_plan_approval.rs:495, crates/tui/src/tui/approval.rs:398,571).
  • crates/tui/src/runtime_api.rs's list_commands (v0.9.14: App-server typed command catalog for client slash-command surfaces #6178) and workspace_instructions's unprotected std::fs::canonicalize (crates/tui/src/runtime_api/workspace.rs:310) are fresh instances of the blocking-call class #6149 targets.
  • Also outstanding from Devin's review: record_session_boot_owner's sidecar read-modify-write is not serialized across concurrent sessions (session_manager.rs:1894), FleetFile::parse's #6037 migration should be double-checked against any historical member pin that matched the operator route intentionally, and CodeQL flagged the #6168 instruction-source listing route's path handling (uncontrolled-data-in-path-expression).

New in this push (a300b52, #6227)

No correctness bugs — verified independently against the actual include_str! call sites, not just the commit message's claim. This is a pure data-reconciliation + CI-guard fix, with no Rust logic changes (confirmed: git diff be6b7fe..HEAD --stat touches only .tsv data files, regenerated .txt fixtures, and .github/workflows/pet.yml — zero .rs/.ts files).

Specifics I checked directly rather than trusting the commit message:

  • The canonical-file claim holds. crates/tui/src/tui/ambient_life/whale-points.tsv is include_str!'d at three call sites — pet_sim.rs:571, pet_watch/worker.rs:119,368, pet_watch/owner.rs:373,425 — i.e. it's genuinely the file the Rust product core (and the /whale-points.tsv HTTP route the TUI serves) compiles in. This file itself is untouched by this commit (not in the diff), consistent with the claim that Rust was the correct body and the other four copies were the ones that drifted.
  • The sync is actually byte-identical, verified with diff -q. I directly diffed the three tracked copies this commit rewrote (pet/whale-points.tsv, pet/rs/whale-points.tsv, pet/public/whale-points.tsv) against crates/tui/src/tui/ambient_life/whale-points.tsv post-commit — all three are now identical (same 980-line point count on both sides).
  • The new CI guard matches what's described. .github/workflows/pet.yml's new "One whale body across every surface" step does exactly what the commit says: diff -q each of the three copies against the Rust canonical file and fails the job (exit 1 with an annotated ::error) on any divergence — this is a real, non-bypassable gate (not continue-on-error), unlike some of this branch's earlier budget-ratchet checks.
  • No missed consumer. I grepped every file referencing whale-points across the repo (.ts/.rs/.js/.mjs/.sh/.py) and confirmed pet-native.js (embedded identically in both crates/tui/src/tui/pet_watch/ and pet/ios/Resources/, guarded by the pre-existing separate "Embedded sources match canonical core" CI step) takes point data as a runtime pointsJSON parameter rather than embedding its own copy — so there was no fifth hardcoded copy this fix needed to touch and missed.
  • Minor, non-blocking documentation imprecision: the commit message lists pet/dist/whale-points.tsv as one of "five" checked-in copies with a matching hash, but pet/dist/ is git-ignored (pet/.gitignore:2, dist/) — it's a build artifact regenerated by npm run sync, not a hand-maintained tracked copy. This doesn't affect correctness (the commit doesn't need to touch it, and doesn't), just a small overstatement in the commit's own accounting of "five checked-in copies."

Tests: the regenerated pet/tests/fixtures/v1-*.txt files are exactly the kind of pinned-golden-output data this fix is supposed to correct (previously pinned to the wrong/drifted body); the commit's own reported evidence (./pet/verify.sh --no-swift exits 0, 380/380 checkpoints across 2 expression versions × 4 tapes/modes, npm --prefix pet run check 68/68, npm --prefix pet run sync leaves git diff --exit-code clean) is exactly the right proof shape for a golden-fixture regeneration — a before/after pass count on the specific gate this PR is fixing, not a broad unrelated suite. I did not have npm/toolchain execution approved in this session to independently re-run verify.sh, so this rests on the reported command output plus the static verification above (canonical-file identity, CI guard correctness, no missed consumer).

No provider/model/route safety, reuse/dead-code, or security issues in this increment — it's confined to the pet subsystem's data/CI surface, with no interaction with provider/model selection, exec policy, or secrets.
· Branch

@Hmbown
Hmbown merged commit 9cdfa92 into main Sep 15, 2026
39 of 42 checks passed
@Hmbown
Hmbown deleted the v0914-chunk2 branch September 15, 2026 15:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants