Skip to content

fix(daemon): let the sweepers prove a Claude seat that has no launch record - #1554

Merged
mvschwarz merged 6 commits into
mvschwarz:mainfrom
AdamWhitehurst:fix/sweeper-identity-proof
Oct 20, 2026
Merged

mvschwarz merged 6 commits into
mvschwarz:mainfrom
AdamWhitehurst:fix/sweeper-identity-proof

Conversation

@AdamWhitehurst

@AdamWhitehurst AdamWhitehurst commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

What a user gets

Before: after a daemon restart, a Claude seat left in attention_required (or failed) on an auto-mode screen with no "Claude Code v" banner in the captured text stayed there until someone ran rig seat clear-attention. The sweepers (the context monitor's 30 s tick and the session transport's reconcile on a successful send or capture) call checkReady with a stand-in binding that has no pane. The adapter proves such a screen only from its in-memory launch record, which a restart discards, so the sweepers could never reach "ready".

After: the sweepers pass a Claude seat's real binding (pane, session, current occupant) and its recorded resume token. With no launch record for the agent, the Claude adapter proves the seat the way clear-attention does, and the existing status flip to ready then happens on the next tick.

  • The proof. The pane's one foreground Claude names exactly the recorded conversation (or, for a seat whose Claude continued that conversation under a new id, the conversation named by the rotation recorded with it, A resumed Claude seat whose session id changes can no longer be proved, so it reads `attention_required` #1077, accepted only from the process that recorded it), the same process in two samples, with no frozen executable (the observation clear-attention uses, with the rotation it passes), in the session's only pane, whose PID is the same after the observation.
  • When it applies. Only when there is no in-memory launch record for the agent. If a record exists for a different launch, the result is what it is today.
  • When it doesn't prove. No token, a wrong token, two panes, another pane, a changed process or pane PID, an unreadable or empty process list: each keeps today's result (not ready, claude_auto_identity_required, still flagged). Missing evidence is not failure and a mismatch stays flagged.
  • Writes. None beyond the status flip the sweepers already make. No identity verdict and no seat.attention_cleared event. A full-restore outcome stays with the restore reconciliation that clear-attention goes through.

Refs #1477 (section 2). Section 1, the wrapper-script launch, is #1540 and independent of this PR.

What the proof does and does not establish

The sweeper proves that the pane's process names the conversation the seat's own record names, as clear-attention does. That record is sessions.resume_token, which the SessionStart hook writes from whichever conversation it reports, so a record the hook has moved and the process agree.

So the monitor also checks the request, in the shape the maintainer suggested on #1477. node.startup_failed.requestedResumeToken is written by the daemon, the hook never touches it, and it survives a restart. When the agent's latest node.startup_failed for the session carries a non-null request, the monitor promotes only if hasClaudeResumeIdentityWork(...) is false for it: the stored token equals the request, or the stored rotation started from it (so a resume Claude continued under a new id still passes). This is added to the stored-token proof, not a replacement: a process keeps naming its launch token after an in-process /resume, so the request alone would pass an agent that has switched conversations.

  • Unchanged when there is no request to compare: no such event, a null request (a fresh launch), or an event from before the fields existed. The monitor does not take the post-send reconcile's default of holding the agent when the event is missing: that would hold agents the monitor promotes today.
  • The post-send reconcile already makes this comparison (hasClaudeIdentityWork, with its hold-when-missing default) and is unchanged by it.
  • Applies only to promotions this proof makes. The adapter marks a ready result the no-launch process proof produced (ReadinessResult.provedWithoutLaunch), and the monitor applies the comparison only to such a result. A banner-recognised seat whose request and record disagree is promoted by the monitor as it is today. Holding those too would be a reasonable follow-up, as the reconcile already holds them after any send; this PR does not do it.
  • The limit: if the agent's record already named the wrong conversation before the launch, the request names it too, and this cannot tell.

The callers

Changed, for Claude seats only (Codex and other runtimes keep the stand-in binding):

  • ContextMonitor.normalizeStartupStatus (context-monitor.ts): passes the session's binding id, pane and current generation, the recorded token, and the rotation recorded with it (the session query now selects the rotation columns); and holds a promotion this proof made when the latest node.startup_failed request differs from the record (above).
  • SessionTransport.doReconcileStartupReadiness (session-transport.ts): passes the pane and occupant generation from its snapshot, the recorded token and its rotation (the snapshot query now selects the rotation columns). Its existing gating is unchanged: a failure that isn't recoverable, or a resume the registry doesn't certify, still doesn't become ready.

Both pass the options argument only when the session records a token (and the rotation only when one is recorded), so every existing call has the shape it had.

Left alone:

  • The startup waits (startup-orchestrator.ts, before content and at completion) and the successor launcher: they run in the daemon that just launched the seat, with its launch record, so they already take the launch path.
  • continueStartup (seat-lifecycle-service.ts): it passes a real binding, but it then delivers saved startup context into the pane. The maintainer's section 2 names the sweepers, and sending text is a different consequence from flipping a status, so I did not widen it. A seat that needs it after a restart still has clear-attention; say if you want the token passed there too.

What it costs

  • checkReady(binding, options?) gets an optional second argument on the runtime adapter interface; other adapters ignore it.
  • Observation cost. For a Claude seat that is flagged on a banner-less auto-mode screen and has no launch record, each sweeper check now makes two process listings (the two samples) and two pane listings, where it made none. It runs on the 30 s tick for such seats only: a healthy seat, a seat whose screen is recognised without proof, and any other runtime take the same path as before. A flagged seat that is not promoted repeats this each tick.
  • No new OS reads. The proof uses the lister as it is (no OS executable path).

How you verified it

Branch fix/sweeper-identity-proof, four commits (5fd950e9; 72302eb6, which passes the stored rotation, from review; 826e9a4f, the requested-token anchor, from the maintainer's comment; 6f10f4f7, which limits the anchor to promotions this proof makes) on upstream/main befd941e, with upstream/main merged in (7b2b6d28, after #1540 landed; merged, not rebased). #1448 touches a nearby file; if it lands first I'll merge main in.

Tests, written first. Run against befd941e's sources, 12 fail (3 adapter, 5 monitor, 4 transport): the ready cases with the real binding and token, without a frozen executable and for a rotated seat, the monitor's and the transport's promotions and hand-off, and the real-adapter pairings. The refusal cases are guards that pass there by construction, so each was checked the other way: removing, one at a time, the pane-count and pane-identity checks (both together), the pane-PID check, the two-sample verification, and the rotation pass-through (adapter, monitor, transport, each separately) fails a test, and applying the proof when a record exists for another launch fails its test. The banner-seat case (a banner-recognised seat whose request and record disagree is still promoted) fails if the anchor is applied to every promotion, and the adapter test fails if the flag is not returned. For the anchor, the maintainer's first test (the request is the seat's own conversation, the record and process another) fails without it; making the monitor hold when the event or its fields are missing (the reconcile's default) fails five tests; and dropping the rotation from the comparison fails the rotated-seat test.

  • claude-sweeper-identity-proof.test.ts (a fresh adapter, mocked tmux and process list): proved by the real binding and exact token; proved without a frozen executable (an install path no frozen path could name); proved for a rotated seat (record B, process naming A, rotation A recorded by that process) and not without the rotation, with a rotation recorded by another process or at another start time, from a conversation the process does not name, or with a process naming neither conversation; not proved without a token (absent, null, empty), by the stand-in binding with no pane, with a wrong token, a process naming another conversation or none, a Claude outside the foreground group, two panes, a session whose only pane is another, a pane PID that changes, a process that changes between samples, an unreadable or empty process list, or a launch record for a different launch; a recognised screen still needs no token.
  • context-monitor-sweeper-proof.test.ts: a failed and an attention_required seat are promoted on the tick with no event written; the check receives the real binding, token and (when recorded) rotation; a rotated seat is promoted by the real adapter and one whose process is not the rotation's stays flagged; the real adapter promotes the exact token and leaves a wrong-conversation seat flagged; no options when the session records no token.
  • session-transport.test.ts: the same for the transport's reconcile, with no seat.attention_cleared.
  • The requested-token anchor, in both files (the maintainer's four): request X with the record and process on another conversation stays flagged (monitor and reconcile); request X, a recorded rotation to Y and the process naming X is promoted; a fresh launch (null request) is promoted, and with no event or an event without the fields the monitor promotes while the reconcile keeps its existing hold; an in-process /resume (the process names X, the record another) stays flagged. The reconcile already makes this comparison, so its cases pin it beside the proof rather than fail without the change.
  • Shared fixture: test/helpers/claude-sweeper-seat.ts.

packages/daemon, full suite with the standard skips (skipped: tcp-readiness-native, scenario-pipeline, SIGQUIT parent case): 1084 files, 18707 tests passed, 8 skipped, 5 todo. Earlier full runs each had one real-tmux failure in a file this change does not touch (tmux-control-native, seat-activity-retirement-hooks); both pass alone, and this run had none. npm run lint (all four typechecks) exits 0. The packages/cli suite was not run: no CLI source changed.

Live, on an isolated daemon (--no-kernel, own HOME / OPENRIG_HOME / tmux server and port, the separate Claude test login, seat cwds outside the repo), four auto-mode haiku seats pushed past the capture with 90 printed lines each. To get seats into attention I used the section 1 cause (a stand-in claude wrapper first on the daemon's PATH, which this branch does not include a fix for): a simulated reboot (daemon stopped, tmux killed, daemon started, rig up wl --existing) left all four attention_required, partially_restored. Each pane was captured before anything touched it (none showed the banner). Then the daemon alone was restarted (tmux and the seats stay up, so no launch record exists):

Build After the daemon restart
911a9d5d, my earlier branch (its Claude paths match main's before this change) all four stay attention_required for 70 s (two ticks)
this branch seats 1 to 3 ready at about 32 s (the first tick), seat 4 ready by 37 s; no seat.attention_cleared event

Seat 4 is the wrong-conversation case. In the first run it was promoted too, for the reason in "What the proof does and does not establish": its stand-in wrapper resumed seat 3's conversation, and the SessionStart hook recorded that conversation as seat 4's token, so record and process agreed. To test a real mismatch I stopped the daemon, set seat 4's recorded token back to its own in the isolated database, set it back to attention_required, and restarted this branch: it stayed attention_required at 40 s and 70 s, while its process named seat 3's conversation. That database edit is mine, in the isolated database only. My live run predates the rotation pass-through and the requested-token anchor and exercises neither; the verification at 72302eb6 covers the rotation (below), and the anchor is covered by the tests only.

The test login's credentials file did not change during this run. The isolated daemon, its tmux server, the run directories and my test-login project directories are removed.

Not run live: the requested-token anchor. The four cases you listed (a requested token the process and record disagree with, staying flagged; a requested token with a recorded rotation, promoted; a fresh launch and an older event, unchanged; an in-process /resume, flagged) are covered by the tests above only. Each fails without its part of the change (the mutation checks above). The live runs, mine and a separate verification of the whole live matrix at 72302eb6 (promotion at the first monitor tick after a daemon-only restart, no seat.attention_cleared event, main still flagged; a wrong-conversation seat still flagged), predate the anchor. The anchor only ever holds a promotion back, it never adds one.

Not covered: a seat whose Claude really rotated its conversation live (the verification at 72302eb6 staged a rotation record in the isolated database from the real process's pid and start time, and that seat was promoted at the first tick; the hook that writes a real rotation is covered by tests only); macOS; auto mode only; Codex (unchanged by design); a seat that is flagged for a reason other than the missing proof (it is not promoted: the proof only answers the identity question); and the claude continue path, left alone as above.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved startup recovery for Claude Code sessions that resume without a recorded launch by verifying the active process, session binding, and conversation identity.
    • Resume checks now account for valid token rotations and process stability before marking a session ready.
    • Sessions remain flagged when evidence is missing, unstable, inconclusive, or mismatched. Existing checks for sessions with recorded launches remain in place.

AdamWhitehurst and others added 5 commits October 19, 2026 11:46
…record

After a daemon restart no launch record exists, so a Claude seat left in
attention on an auto-mode screen with no banner could never be proved by the
launch path, and the sweepers (which pass a binding with no pane) kept it
there until someone ran `rig seat clear-attention`.

checkReady takes an optional resume token. With no launch record for the agent,
the Claude adapter now proves the seat the way clear-attention does: the pane's
one foreground Claude names exactly that conversation, the same process in two
samples with no frozen executable, in the session's only pane with an unchanged
PID. The context monitor and the session transport pass a Claude seat's real
pane, occupant and recorded token. A record for another launch, a missing token,
a wrong token, two panes, a changed pane PID or an unreadable process list keep
today's result. Nothing is written beyond the status flip the sweepers already
make: no identity verdict and no attention-cleared event.

Refs mvschwarz#1477

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…y proof

clear-attention checks a seat's pane against its recorded conversation and the
rotation recorded with it (mvschwarz#1077), so a Claude that continued conversation A as
B is proved by the process that recorded the rotation. The sweepers' proof
passed the token only, so such a seat was never promoted although
clear-attention would prove it.

ReadinessOptions carries the rotation next to the token. The context monitor
and the session transport read the rotation columns with the session row, build
the rotation with claudeResumeRotation as clear-attention's reader does, and
pass it only when one is recorded. The adapter hands it to the same process
observation. A rotation recorded by another process, at another start time, or
from a conversation the process does not name still proves nothing.

Refs mvschwarz#1477

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… asked for differs

The SessionStart hook rewrites sessions.resume_token from whichever conversation
it reports, so the sweeper proof compares the pane's process with a record the
hook may have moved. node.startup_failed's requestedResumeToken is written by
the daemon and the hook never touches it.

The context monitor now also reads the agent's latest startup_failed event for
the session. When it carries a non-null requested token, the seat is promoted
only if hasClaudeResumeIdentityWork is false for it, the comparison the
post-send reconcile already makes: the stored token equals the request, or the
stored rotation started from it. This adds to the stored-token proof and does
not replace it. With no event, a null request, or an event from before the
fields existed, nothing is held, unlike the reconcile's default, so a seat the
monitor promotes today stays promoted.

Shape from the maintainer's comment on mvschwarz#1477.

Refs mvschwarz#1477

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…the promotions the new proof makes

The hold sat on every Claude promotion, so a banner-recognised seat whose
request and record disagree, which the monitor promotes today, would have been
held. The Claude adapter now marks a ready result that the no-launch process
proof made (ReadinessResult.provedWithoutLaunch), and the monitor applies the
requested-conversation comparison only to such a result. Banner promotions and
every other path are unchanged.

Refs mvschwarz#1477

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: b234f6b1-0bfd-4ee3-a085-9a09286fa94d
📥 Commits

Reviewing files that changed from the base of the PR and between 6f10f4f and 7018571.

📒 Files selected for processing: 5
  • packages/daemon/src/adapters/claude-code-adapter.ts
  • packages/daemon/src/domain/runtime-adapter.ts
  • packages/daemon/src/domain/session-transport.ts
  • packages/daemon/test/claude-sweeper-identity-proof.test.ts
  • packages/daemon/test/session-transport.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.

📝 Walkthrough

Claude readiness checks now verify resumed processes without launch records by using recorded binding, pane, resume token, and rotation data. Reconciliation blocks promotion when the requested conversation does not match the recorded resume identity.

Changes: Claude Resume Readiness

Layer / File(s) Summary
Readiness contract and context wiring
packages/daemon/src/domain/runtime-adapter.ts, packages/daemon/src/domain/session-transport.ts, packages/daemon/src/domain/context-monitor.ts
Readiness checks accept optional resume identity data. Claude reconciliation passes the real binding, pane, generation, token, and rotation; other runtimes retain their existing behavior.
No-launch Claude process proof
packages/daemon/src/adapters/claude-code-adapter.ts, packages/daemon/test/claude-sweeper-identity-proof.test.ts, packages/daemon/test/helpers/claude-sweeper-seat.ts
The adapter verifies a resumed Claude process when no launch record exists. It requires matching process identity and stable pane and PID observations. Tests cover accepted and rejected evidence.
Conversation identity and promotion
packages/daemon/src/domain/context-monitor.ts, packages/daemon/test/context-monitor-sweeper-proof.test.ts, packages/daemon/test/session-transport.test.ts
Reconciliation compares the requested conversation with the recorded resume identity before promoting a no-launch proof. Tests cover rotations, fresh launches, mismatches, and event retention.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant ContextMonitor
  participant ClaudeCodeAdapter
  participant Tmux
  participant verifyClaudePaneProcess
  ContextMonitor->>ClaudeCodeAdapter: Pass binding, resume token, and rotation
  ClaudeCodeAdapter->>Tmux: Read matching pane and PID
  ClaudeCodeAdapter->>verifyClaudePaneProcess: Verify process against resume identity
  verifyClaudePaneProcess-->>ClaudeCodeAdapter: Return process verification result
  ClaudeCodeAdapter->>Tmux: Recheck pane and PID
  ClaudeCodeAdapter-->>ContextMonitor: Return readiness result
  ContextMonitor->>ContextMonitor: Compare requested and recorded conversation identity
Loading

Suggested reviewers: mvschwarz

Merge Risk: ⚪ Minimal · up to 70185

No merge-blocking issue was established in the Claude seat-readiness changes; the PR is mergeable after normal checks.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 8 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: allowing daemon sweepers to prove Claude seats that lack a launch record.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mvschwarz

Copy link
Copy Markdown
Owner

Thanks, @AdamWhitehurst. We've read it at 6f10f4f7, and it looks good to us: it doesn't refuse anything new, it's the sweepers half of #1477, and the requested-token hold went in as discussed.

One ask, a line in the PR description only, with no code change. The description covers restart and recordless recovery. Could you add a sentence on the monitor path too, so the description covers both? For example: "Because the monitor now passes the real binding, a seat in attention_required on the same daemon whose launch record still matches can also be promoted by the monitor through the existing launch-record proof, where before only the startup waits could."

On continueStartup: leave it out of this PR. Delivering saved startup text into the pane is a different consequence from changing a status, so your call was right.

That's all we have.

@mvschwarz

Copy link
Copy Markdown
Owner

Thanks again, @AdamWhitehurst. #1553 has just merged to main, and it changes the same lines in packages/daemon/src/adapters/claude-code-adapter.ts, so this PR now conflicts with main. Could you merge main into the branch (a plain merge, no rebase) and resolve that file so both changes hold?

#1553's "absent" check (a readable process list with no Claude in it) should stay on the fresh in-place path only, and not loosen the exact-token requirement of the no-launch proof.

You can skip the line we asked you to add to the PR description: we'll describe the same-daemon monitor promotion ourselves. Once the merge is pushed, we'll read the resolved lines and the CI results.

@mvschwarz mvschwarz moved this from Todo to In Progress in OpenRig roadmap Oct 20, 2026
…y-proof

# Conflicts:
#	packages/daemon/src/adapters/claude-code-adapter.ts
@AdamWhitehurst

Copy link
Copy Markdown
Contributor Author

Merged main in (a plain merge, 70185710), with claude-code-adapter.ts resolved as you described:

  • both sets of native-process imports are kept;
  • the sweeper options go into the history assessment (assessManagedProbe(..., options)), and Qualify fresh in-place Claude readiness with the current frame #1553's assessFreshInPlaceProbe runs after it, unchanged;
  • the no-launch proof's result still carries provedWithoutLaunch, and the fresh in-place check passes it through when there's no fresh in-place record, so the monitor's requested-token hold still applies only to that proof;
  • Qualify fresh in-place Claude readiness with the current frame #1553's "absent" reading stays inside the fresh in-place path. The no-launch proof still needs the exact token, and a new test covers it: a readable process list with no Claude in it doesn't prove a seat (it fails if the absent reading reaches that path).

One test-only change came with the merge: the transport's "fresh launch / older event" case is split in two, because main's new seat-address rule rejects two seeded seats in one test. What it asserts is unchanged. The PR description is as it was.

@openrig-review openrig-review left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved at 7018571. The context monitor and the send/capture reconcile now pass a Claude seat's real binding and recorded resume token. So a seat left in attention_required or failed on a banner-less auto-mode screen can be proved and promoted without a person clearing attention: after a daemon restart, or with no launch record, by the clear-attention-style process proof; and on the same daemon, by the existing launch proof when its record still matches. A promotion this no-launch proof makes is held when the requested token disagrees. Required CI (8/8) passes here.

— dev60-planner@v-openrig-build

@mvschwarz
mvschwarz merged commit 771f1b0 into mvschwarz:main Oct 20, 2026
10 checks passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in OpenRig roadmap Oct 20, 2026
@mvschwarz

Copy link
Copy Markdown
Owner

Thanks, @AdamWhitehurst. Merged. After a daemon restart, the two sweepers (the context monitor's 30-second check and the reconcile after a send or capture) can now prove a Claude agent on an auto-mode screen with no banner, the way rig seat clear-attention does: from its real pane and its recorded conversation. So the agent moves back to ready on its own. When the agent's last failed start names the conversation that was asked for, a promotion is held if the agent is on another conversation. Thanks for both review rounds, for raising that case before opening the PR, and for the careful merge with main. It's on main and not in a release yet.

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

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants