Skip to content

fix(tui): v0.9.8 stabilization — turn-owned agents, compaction quality, Blue Stage web - #5399

Closed
Hmbown wants to merge 4 commits into
mainfrom
cursor/v098-stabilization-733b
Closed

fix(tui): v0.9.8 stabilization — turn-owned agents, compaction quality, Blue Stage web#5399
Hmbown wants to merge 4 commits into
mainfrom
cursor/v098-stabilization-733b

Conversation

@Hmbown

@Hmbown Hmbown commented Aug 15, 2026

Copy link
Copy Markdown
Owner

Summary

Reconstructs the still-missing CodeWhale v0.9.8 Rust stabilization onto current main (post #5393/#5394/#5395). No version bump, tag, release, TypeScript runtime, or unrelated features.

Fixes

1. Turn-owned default direct subagents

Default agent starts use child_runtime (parent-turn cancel propagates). Explicit detached=true keeps background_runtime durability. On Interrupted turns the engine cancels and joins turn-owned children before TurnComplete.

Files: crates/tui/src/tools/subagent/mod.rs, crates/tui/src/tools/subagent/tests.rs, crates/tui/src/core/engine.rs, docs/SUBAGENTS.md

2. Compaction quality gate

Empty / whitespace-only / punctuation-only / placeholder / non-text summaries cannot replace real history. Quality-gate failures retry inside compact_messages_safe and count toward retries_used.

Files: crates/tui/src/compaction.rs

3. Model-facing agent requires action

Live schema/parser require action. Stored/programmatic legacy parsing (parse_agent_tool_action_legacy) still treats missing action as Start.

Files: crates/tui/src/tools/subagent/mod.rs, crates/tui/src/tools/subagent/tests.rs

4–10. Embedded Blue Stage web client (CWC)

  1. Blue Stage dark rail / accents / quiet receipts / rounded composer (already present; preserved)
  2. Long MCP failures compact + expandable
  3. User questions: Other always available, exact single-select cardinality, bind to live thread + pending request
  4. SSE streamGap clears only after snapshot and resubscription succeed
  5. Browser-opener failure no longer kills Runtime; prints 10-minute single-use bootstrap URL
  6. Mobile nav is a real modal drawer (inert, Escape, focus trap/restore), visualViewport sizing, coarse 44px targets
  7. Stream updates preserve disclosures; growing agent text uses aria-live="off" (transcript is not a polite live region)

Files: crates/tui/src/runtime_web/app.mjs, index.html, styles.css, crates/tui/tests/runtime_web_client.test.mjs, crates/tui/src/runtime_api.rs, crates/tui/src/runtime_api/web.rs, docs/WEB.md

Out of scope (per brief)

Testing

  • cargo fmt --all -- --check
  • node --check crates/tui/src/runtime_web/app.mjs
  • node --test crates/tui/tests/runtime_web_client.test.mjs25/25
  • Targeted cargo test -p codewhale-tui --lib filters for compaction quality, agent action, turn-owned/detached spawn, bootstrap TTL, and agent_tool_ schema/lifecycle
  • Full workspace suite (skipped; not cheap)

Checklist

  • Updated docs (docs/SUBAGENTS.md, docs/WEB.md)
  • Added/updated tests for the three TUI behaviors and web client
  • No bot/tool Co-authored-by trailers
Open in Web Open in Cursor 

cursoragent and others added 4 commits August 15, 2026 06:05
Default agent starts use child_runtime so parent-turn cancellation
propagates. Explicit detached=true keeps background_runtime durability.
On Interrupted turns the engine cancels and joins turn-owned children
before TurnComplete. Model-facing agent calls now require action;
legacy prompt-only parsing remains available for stored/programmatic use.

Assisted by Cursor.

Co-authored-by: Hunter Bown <Hmbown@users.noreply.github.com>
Empty, whitespace-only, punctuation-only, placeholder, and non-text
compaction results no longer replace real history. Quality-gate failures
retry inside compact_messages_safe and count toward retries_used.

Assisted by Cursor.

Co-authored-by: Hunter Bown <Hmbown@users.noreply.github.com>
codewhale web no longer tears down a healthy Runtime if the OS browser
opener fails. It prints the single-use bootstrap URL and keeps serving.
Bootstrap TTL is ten minutes for manual recovery.

Assisted by Cursor.

Co-authored-by: Hunter Bown <Hmbown@users.noreply.github.com>
SSE gap state clears only after snapshot+resubscribe succeed. Mobile
thread rail is a real modal drawer (inert, Escape, focus trap/restore)
with visualViewport sizing and coarse 44px targets. User questions keep
TUI Other+cardinality and bind to the live thread. Long MCP failures
stay compact/expandable. Stream updates preserve disclosures and mute
token-stream aria announcements. Client tests: 25/25.

Assisted by Cursor.

Co-authored-by: Hunter Bown <Hmbown@users.noreply.github.com>
@cursor

cursor Bot commented Aug 15, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@claude

claude Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 3m 30s —— View job


Review complete

Todo list

  • Gather context (read CLAUDE.md/AGENTS.md, diff, changed files)
  • Review turn-owned subagent changes (tools/subagent/mod.rs, core/engine.rs)
  • Review compaction quality gate (compaction.rs)
  • Review Blue Stage web client changes (runtime_web/app.mjs, runtime_api.rs, runtime_api/web.rs)
  • Review tests for new behavior
  • Check provider/model/route safety invariant (EPIC v0.8.65 EPIC: Separate provider facts, model facts, offerings, and route resolution #2608)
  • Post final review

Summary

Diffed the 4 PR commits against their common base (dd0fbed, matches the branch's actual parent — a shallow clone prevented a direct origin/main diff, so I confirmed the commit range independently). Changes match the PR description exactly: 735/-70 across the same 12 files. No correctness bugs found; no provider/model-prefix inference introduced (invariant #2 is not implicated — this PR never branches on a model string).

1. Turn-owned subagents (tools/subagent/mod.rs, core/engine.rs)

  • child_runtime() (turn-owned) inherits the parent's cancel_token; background_runtime() (detached) derives a fresh one — correctly wired to the detached flag added on SpawnRequest/SubAgent/SubAgentSpawnOptions.
  • cancel_and_join_turn_owned_agents: collects turn-owned running IDs first (avoids a mutable-borrow-during-iteration issue), takes+aborts each handle, calls finish_terminal_result(..., abort_task=false, ...) (correct — the handle was already taken, so abort_task=true would be a no-op but false avoids a redundant lookup), then awaits the collected handles after mutating state. Sound.
  • Engine only runs this cleanup on TurnOutcomeStatus::Interrupted, not Failed — intentional per the PR description, and there's no missing enum variant (Completed | Interrupted | Failed is exhaustive).
  • Resume flow (spawn_background_with_assignment_options for checkpoint resume) explicitly sets detached: true — correct, a resumed continuation must outlive the turn that requested it.
  • spawn_workflow_task builds its agent input JSON directly and calls spawn_subagent_from_input without going through parse_agent_tool_action, so the new action-required validation doesn't break workflow-driven spawns (which never populate action in the JSON it constructs). Verified this call path.
  • parse_agent_tool_action_with_policy's legacy vs. live split is correctly gated: only #[cfg(test)] parse_agent_tool_action_legacy gets LegacyMissingMeansStart; the live/model-facing parser now requires action, matching the new "required": ["action"] in the tool schema.

2. Compaction quality gate (compaction.rs)

  • summary_is_usable correctly uses char::is_alphanumeric() (Unicode-aware), so non-Latin-script summaries aren't misclassified as punctuation-only — consistent with COMPACTION_LANGUAGE_CONTRACT's multi-language support.
  • build_compaction_summary_block_text's new debug_assert!(summary_is_usable(...)) is safe: verified both call sites either pass a fixed, gate-passing placeholder (estimate_retained_floor_conservative, used only for token-size probing, never committed) or are gated immediately above by the same summary_is_usable check (compact_messages).
  • The quality-gate retry in compact_messages_safe matches on e.to_string().contains("quality gate") — a string-based dispatch rather than a typed error variant. Works correctly today (the anyhow::bail! message is unique) but is a bit brittle if that message text ever changes elsewhere; not blocking.

3. Blue Stage web client (app.mjs, runtime_api.rs, runtime_api/web.rs, CSS/HTML)

  • collectUserInputAnswers correctly enforces exact single-select cardinality even when a radio is checked and Other is filled (both land in selected, so selected.length !== 1 correctly rejects the combination).
  • resolveUserInputTarget correctly checks streamState.userInputs.has(inputId) — confirmed userInputs is consistently a Map everywhere it's touched.
  • streamGap is cleared only on the success path of both selectThread and recoverProjection (after snapshotThenSubscribe returns true), and left true on any failure/retry — matches the PR's SSE-gap claim.
  • Browser-open failure fix in runtime_api.rs: no longer cancels/aborts the scheduler or returns Err — Runtime now stays alive and prints the bootstrap URL via both eprintln! and println!. Minor: the URL is printed twice (once prefixed "warning:", once "Manual bootstrap:") — harmless duplication, not a bug, could be trimmed to one line if you want tidier output.
  • BOOTSTRAP_TTL bump to 10 minutes is covered by a dedicated unit test; aria-live="polite" correctly removed from #transcript (streaming agent text now uses aria-live="off" on the per-item body instead) — confirmed by a source+HTML pattern test.
  • Mobile rail modal (openRail/closeRail/releaseRailModal) correctly restores focus to whichever element had focus before opening (not hardcoded to railOpen), sets inert on main.session, and installs a capturing keydown handler for Escape/focus-trap that's properly removed in releaseRailModal.

4. Tests
Good coverage added for each claimed behavior (turn-owned vs. detached cancel-token behavior, action requirement, compaction quality-gate edge cases including non-Latin/punctuation-only text, SSE gap clear/leave semantics, Other-always-available cardinality, mobile modal semantics). Test style for the web client leans on regex-over-source-text assertions in a few spots (e.g. gap recovery clears streamGap only on...) rather than exercising the actual DOM/state — weaker than the behavioral tests elsewhere in the same file, but not wrong.

No blocking issues found. Nice adherence to the do-not-delete guardrails and no touches to the removed agent_open/agent_eval/agent_close surface.
· branch cursor/v098-stabilization-733b

@Hmbown

Hmbown commented Aug 15, 2026

Copy link
Copy Markdown
Owner Author

Not the v0.9.8 candidate. This was reconstructed without the local worktrees. Finish from /Volumes/VIXinSSD/CW/worktrees/v098-bugfix-convergence and v099-rust-tightening against origin/main ad102ec. Do not land this.

@Hmbown Hmbown closed this Aug 15, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 08e4eb538b

ℹ️ About Codex in GitHub

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

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

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +4764 to +4767
.filter(|(_, agent)| {
!agent.detached
&& agent.status == SubAgentStatus::Running
&& !agent.completion_claimed

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Restrict cancellation to children owned by this turn

Track and filter by the initiating turn rather than treating every non-detached agent as owned by whichever turn is interrupted. A child intentionally left running after successful turn A is still detached == false, so interrupting a later unrelated turn B reaches this filter and aborts A's work; the same global manager can also contain non-detached descendants of other active agents.

Useful? React with 👍 / 👎.

}
},
"required": []
"required": ["action"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include the start action in /agent-generated calls

Making action mandatory breaks the existing /agent [N] <task> flow when the model follows its generated instruction literally: crates/tui/src/commands/groups/core/agent.rs:61-63 tells it to call agent with only name, prompt, and max_depth, and this new requirement then rejects that call instead of starting the requested worker. Update that instruction and other programmatic prompts to include action: "start" before enforcing the requirement.

Useful? React with 👍 / 👎.

Comment on lines 965 to +968
if (!subscribed) return;
// Gap state clears only after both the replacement snapshot and the
// resubscription have succeeded.
app.streamGap = false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Wait for EventSource open before clearing the gap

Do not clear streamGap merely because connectStream constructed an EventSource. Connection failures are reported asynchronously through onerror, so a rejected or unreachable resubscription reaches this line, marks continuity restored, and even displays “connected” before onopen; the later error path reconnects without restoring the gap flag. Clear it from the matching stream's onopen callback instead.

Useful? React with 👍 / 👎.

Comment on lines +404 to +409
case "stale-user-input":
return "That question was already answered or has expired — nothing was sent.";
case "no-approval":
return "No approval was identified — nothing was sent.";
default:
case "no-user-input":
return "No pending question was identified — nothing was sent.";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Localize the new browser status messages

Route these newly added refusal messages, along with the other new recovery and disclosure labels in this web-client change, through the locale/message catalog rather than returning hardcoded English. Otherwise users running a non-English locale receive English status and attention copy, contrary to the repository's localization contract for every user-visible render-path string.

AGENTS.md reference: crates/tui/AGENTS.md:L36-L41

Useful? React with 👍 / 👎.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants