Skip to content

fix(succession): re-check a predecessor live only for its quiet time (MUS-93) - #200

Merged
Castor-o7 merged 5 commits into
mainfrom
fix/mus-93-quiet-recheck
Nov 7, 2026
Merged

Castor-o7 merged 5 commits into
mainfrom
fix/mus-93-quiet-recheck

Conversation

@Castor-o7

Copy link
Copy Markdown
Owner

MUS-93: this is the cause of the conformance failure on #194.

What failed. In both of #194's runs (42324052913 and 42330531873), the only cell that failed the suite was claude × unread-mail-follows-a-restart. claude × restart-keeps-identity also failed once, though it's on FLAKY and doesn't fail the suite. Every pi, opencode and prime-agent failure in those runs is already listed as known (MUS-81, MUS-82, MUS-89). In each failed cell the daemon logged:

[identity] <new>: predecessor <old> is Live("it never had a client, and was active 0s ago (quiet time 2s)")

Why. FakeClaude::start runs the session-start hook and then starts the MCP shim, which binds its client asynchronously. A cell that restarts soon after starting can kill the old shim before it binds. The old row then has no client, and verdict() calls it live inside the quiet time. The sweep's Live arm calls check_again only for a live client or waiter (MUS-92), so this verdict is never checked again, even though the same rule calls the row dead seconds later. The new session keeps its fresh nickname, and the cell times out at 20 s. A real Claude session restarted before its MCP shim attaches would lose its nickname the same way.

The fix. A new pure function, succession::quiet_until, gives the time at which such a predecessor would count as dead by the same rule: its last signal plus quiet_ms. It returns None when the predecessor is live for any other reason. The Live arm then calls check_again(min(dead_at + tick, registered + window)). This uses the existing pending check, with no second timer (robust-warbler's ask). Applying the rule again later doesn't loosen it. A sibling that keeps signalling moves its own time on, and keeps its name once the window ends.

Tests.

  • Unit test a_quiet_row_says_when_it_would_count_as_dead: the time it returns is exactly where verdict turns from live to dead. A sibling that signals again moves the time on. A waiter, a bound or dropped client, a Codex row before MUS-77, or no row at all gives None.
  • crates/daemon/tests/identity_quiet.rs: a predecessor that was active inside its quiet time is taken over by a late check once the quiet time passes. A sibling that keeps signalling through the swap window keeps its name. This test fails without the fix: I removed the check_again line and it failed at the takeover assertion.
  • CONFORMANCE_ONLY=restart passes locally, and follows-a-restart passed 3 of 3. Caveat: locally the MCP shim always bound in time, so the takeovers came from an observed client drop and the new path never fired. The local pass shows nothing broke. It doesn't reproduce the race; CI's slower runner is where that shows.
  • cargo fmt --check and clippy -D warnings are clean. Every muster-daemon test passes.

Not done: no new FLAKY entry, per robust-warbler and the watchman. The existing MUS-93 FLAKY cells stay until they pass in CI on three PRs in a row, as MUS-93's done-when says. The claude-tls cells' earlier failures predate any daemon log, so this doesn't show whether they share the cause. docs/roadmap.md and docs/stable-identity.md are updated.

🤖 Generated with Claude Code

Castor-o7 and others added 5 commits November 6, 2026 22:37
…(MUS-93)

A Claude session restarted before its MCP shim bound leaves a row with
no client, which verdict() calls live while it was active inside the
quiet time. The sweep scheduled a late check only for a live client or
waiter (MUS-92), so that verdict was never checked again and the new
session kept its fresh nickname for good, though the same rule calls
the row dead seconds later. That's the line in every failed cell of
#194's conformance runs.

Such a predecessor is now checked again until its quiet time has
passed (succession::quiet_until), a tick after, never past the swap
window, through the same pending check. A sibling that keeps
signalling moves its own time on and keeps its name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…edicate (MUS-93)

From the watchman's read of #200: quiet_until restated verdict's
unobservable branch, so the two could drift apart. Both now decide it
through judged_by_quiet_time, and the Codex guard is one function. No
change in behaviour; the existing verdict tests and the new ones pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…en PRs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…MUS-113)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Castor-o7
Castor-o7 merged commit 83ea351 into main Nov 7, 2026
6 checks passed
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.

1 participant