Repository navigation
fix(wr-agent): replay the busy state to a reattaching client (#359) - #364
Conversation
A program reports busy with OSC 9;4 once, at the start of its work, so a client that reattached mid-turn (an app relaunch) always showed idle. The shadow terminal now keeps the last report and the replay re-emits it, or an explicit REMOVE when there is none, before the parser continuation. The on-disk record never carries it: it outlives the program. A program killed mid-turn never sends its REMOVE, so the shadow notes the pty's foreground process group when a report arrives, and a live attach or a hand-off freeze replays idle while another group owns the pty. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The core reattachment integration test finishes its reporting command before detaching, so it does not exercise the stated mid-turn regression.
Review effort: Balanced
Findings: 1
Open (3)
What changed in this PR
Replays OSC 9;4 progress state when clients reattach to live wr-agent sessions.
Changes:
- Tracks and replays progress reports with foreground-process ownership.
- Excludes progress state from persisted screens.
- Adds reattach, hand-off, ownership, and restoration tests and documentation.
| File | Description |
|---|---|
vcs/crates/wr-agent/src/terminal.rs |
Captures and replays progress reports. |
vcs/crates/wr-agent/src/shadow.rs |
Tracks report ownership by process group. |
vcs/crates/wr-agent/src/session.rs |
Applies live replay during attach and hand-off. |
vcs/crates/wr-agent/src/screens.rs |
Keeps persisted screens idle. |
vcs/crates/wr-agent/src/handoff.rs |
Updates hand-off documentation. |
vcs/crates/wr-agent/tests/attach_survives_detach.rs |
Adds reattachment integration coverage. |
docs/designs/remote-workrooms.md |
Documents progress-state durability. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| let seen = replay_after( | ||
| "busy", | ||
| "5a5a5a5a-6b6b-7c7c-8d8d-9e9e9e9e9e9e", | ||
| "printf '\\033]9;4;3;\\033\\\\'; echo WORK''ING\n", |
There was a problem hiding this comment.
🤖 Left by Claude Code: Fixed in 42fa384. The reporter is now a foreground /bin/sh child that is still running (sleep 30) when the client reattaches, as in the issue repro, so the replay is busy because its live sender owns the pty. Confirmed it goes red when the owner check in Shadow::replay_for is inverted.
| /// A session that ended with its host reports busy only while it runs: its record is written | ||
| /// from the live shadow and rendered long after the program is gone, so a restored pane must | ||
| /// never be told it is busy (#359). Driven through the real writer, `flush`. | ||
| /// Value: protects=a restored pane's repaint carries no OSC 9;4 for a session that reported busy; fails_when=changed_screens() takes replay() instead of record(), pinning the restored pane's spinner on; why_new=no test took a record from a live session through flush and render; seam=none |
There was a problem hiding this comment.
🤖 Left by Claude Code: Fixed in 42fa384. The doc and value card now say the restored repaint carries no busy report (a REMOVE at most); the assertion already checks only for ESC]9;4;3.
|
|
||
| /// A hand-off keeps the busy indicator: the screen a session is frozen with, adopted by the | ||
| /// next program, replays its last OSC 9;4 report, so a client that attaches afterwards is told | ||
| /// the session is busy (#359). Unlike the on-disk record, a hand-off's screen is `replay()`. |
There was a problem hiding this comment.
🤖 Left by Claude Code: Fixed in 42fa384. The doc and value card now name the live replay (live_replay -> Shadow::replay_for) that frozen() captures.
Copilot: - The busy reattach test now reports from a foreground program still running at reattach, as an agent mid-turn is, rather than from shell built-ins whose report the long-lived shell owns. - The restored-pane test's doc says no busy report, not no OSC 9;4: the render's replay may carry a REMOVE. - The adopted-session test's doc names the live replay a hand-off freezes, not replay(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>


Fixes #359
Summary
The busy indicator now survives a reattach. A program reports busy with OSC 9;4 once, at the start of its work, so a pane reattaching mid-turn (an app relaunch, local or remote) always showed idle. wr-agent's shadow terminal now registers
GHOSTTY_TERMINAL_OPT_PROGRESS_REPORT, keeps the last report, and the replay re-emits it after the screen and before the parser continuation. The app's existing 9;4 path then restores the spinner and its safety timer; no Swift change.ESC ] 9;4;0 ST), so a client that kept its view across a reconnect drops a busy state the program has since cleared. A fresh client treats it as a no-op.record()(the--screensrecord) carries no 9;4: a record outlives its program, so a restored pane is painted idle.fgis busy again to the next client.Box::into_rawslot freed inDropafterghostty_terminal_free, so a move ofShadowTerminalnever aliases the pointer the terminal holds.Known limits, documented in code and
docs/designs/remote-workrooms.md:ponytail:comment inshadow.rs).Test Coverage
Tests: 29 → 29 test files (new tests are in-source
#[cfg(test)]modules and an existing integration file)Coverage: 83% value-weighted at the gate (15/18 paths); the later review cycles added owner-tracking tests on top.
Test value: 11 tests written, 0 rejected by the authoring gate, 0 existing tests extended, 3 paths weakly covered (two unreachable FFI guards, rejected as needing a seam;
clone_via_snapshot's untracked slot).New tests, each shown to fail when the code it guards is broken:
a_reattaching_client_is_told_the_session_is_busy(integration, real agent + socket): red before the fix.a_reattaching_client_is_not_told_a_cleared_report: red when REMOVE stops clearing, or when idle sends no REMOVE.only_the_replay_carries_the_progress_report(table, incl. percent 0 and the explicit REMOVE): red when the report moves intorecord()or idle sends nothing.the_progress_report_does_not_split_the_continuation: red when the report follows the continuation.a_restored_pane_is_never_told_the_session_was_busy(realflush→Screens::render): red when the writer usesreplay().an_adopted_session_replays_the_progress_report_it_was_frozen_with: hand-off round trip.a_report_from_a_program_that_has_gone_is_not_replayed,an_adopted_report_is_dropped_once_its_sender_goes,a_hand_off_after_the_reporter_has_gone_is_frozen_idle(real ptys,set -m, file-gated reporter): red when the foreground check, adopt's owner note, or the live freeze is removed.the_progress_report_follows_the_program_that_sent_it,a_new_report_belongs_to_its_own_sender(closure-driven, no pty): red when the owner comparison inverts or the owner is noted only once.Pre-Landing Review
Three review cycles (the skill's cap) with testing, maintainability, security, performance, simplification and Red Team specialists, then the adversarial pass. All findings were INFORMATIONAL; none critical.
fg. Fixed (read-onlyreplay_for). Folded write + owner note intowrite_from_pty; new closure-driven and adopt tests.live_replayhelper), a test that could not fail (replaced), hand-off docs. Continued past the cap on the maintainer's explicit approval, on tests + mutation checks rather than a fourth full review.live_replay) with a mutation-verified test. Other items are the documented limits above.hastest helper is repeated across three test modules.Outside review (Codex): structured review completed, gate PASS (two P2s, both the documented owner-sampling and hand-off-owner limits). Codex adversarial challenge: unavailable (timed out after 9 minutes; missing coverage, not a pass).
Review binding note: the last fixes landed after the final full review pass, under the approval above.
Exploratory QA
Not run as a separate pass: this is an agent-internal change with no UI. Behaviour was exercised through the real agent binary over a socket and through real ptys in the tests above. Reset behaviour was probed directly: a full reset (
ESC c) clears the stored report; alt-screen switches and DECSTR keep it, as the app's live Ghostty does.Design Review
No frontend files changed — design review skipped.
Eval Results
No prompt-related files changed — evals skipped.
Scope Drift
Scope Check: CLEAN against #359 (all three Fix items and both Tests items). Added beyond the issue, on the maintainer's choice: the explicit idle REMOVE and the stale-owner drop.
Plan Completion
Plan completion audit: not run (no plan is bound to this branch and no docs/designs/ file matches). Fix: add "Plan: " to the PR body, or run /autoplan.
TODOS
No TODO items completed in this PR.
Documentation
Status: updated. One authored doc was corrected and extended to match #359 (wr-agent replays the last OSC 9;4 progress report, or an explicit REMOVE when idle, to reattaching clients).
Scope: audited the 6 unstaged wr-agent files (
handoff.rs,screens.rs,session.rs,shadow.rs,terminal.rs,tests/attach_survives_detach.rs) against base 5d5f033 (no committed, staged or untracked content), plus the discovered docs (README, CONTRIBUTING, AGENTS, macapp/AGENTS, docs/agents/, docs/designs/, docs/repository-notes.md).Documentation health:
docs/designs/remote-workrooms.md: updated. Hand-off screen item now says the outgoing agent sends the replay a reattaching client would get (Shadow::replay_for), so the busy state crosses a hand-off. New paragraph under Terminal-state durability describes the busy state on reattach: re-emitted report or explicit REMOVE, report before the parser continuation, idle once the sending process group no longer owns the pty (live attach and hand-off freeze), report kept for ^Z/fg, the sample-after-read limit and the two integration tests. The Stop-and-reboot screen restoration #232 record bullet now saysrecord()also omits the progress report and a restored pane is painted idle.docs/designs/oq8-cross-machine-reattach.md: current. The 'Live display state' row is still accurate for disk persistence.docs/repository-notes.md,CONTRIBUTING.md,README.md,AGENTS.md,macapp/AGENTS.md: current.Coverage debt: reference coverage for the new behaviour lives only in the design doc, which is appropriate for an agent-internal behaviour with no new flags, wire fields or user-facing surface. No how-to or tutorial gaps. Minor wording debt: Success Criteria and the Distribution Plan still describe the on-disk record as
replay()'s VT bytes; the #232 bullet now holds the precise definition.Diagram drift: none; the audited docs contain no diagrams naming the changed entities.
Test plan
WR_AGENT_HAS_TERMINAL_STATE=1 cargo test --manifest-path vcs/Cargo.toml -p wr-agent --features terminal-state: 514 passed, 0 failed (6 ignored, pre-existing)cargo test --manifest-path vcs/Cargo.toml -p wr-agent: 479 passed, 0 failedcargo clippy -p wr-agent --all-targets -- -D warnings, with and without--features terminal-state: cleancargo fmt -p wr-agent -- --check: clean🤖 Generated with Claude Code