Repository navigation
fix(daemon): refresh a live session's copied bearer when its rolling member rotates - #146
koushikraj-s wants to merge 6 commits into
Conversation
uwuclxdy
left a comment
There was a problem hiding this comment.
Requesting changes. Findings are against your three commits (12a20c68..02965ce2), so they carry over to the split.
Design
- major:
copied_bearer_behind_sidecarcalls an item stale when it holds a refreshless bearer that differs from the sidecar, which never shows it's the member's own revoked bearer. A static mint on a member later flipped torolling_tokengets replaced. So does a session whose swap leftcurrent_memberon A after its row update failed: every 15 s its item moves back to the account the chain moved it off. Match the item's bearer against one clauth wrote for that member, as the Keychain mirror's recognition does, or test it against the chain with #144'srolling_bearer_behind_chain. - major: the pass polls every 15 s, but the sidecar changes on one event,
stamp_rolling_tokeninland_pair_with. Between a rotation and the next pass the session holds a revoked bearer. Drive the re-point off the stamp, or say in the PR why the poll is enough. - minor: a sidecar that is behind its chain still counts as the truth, so the pass writes a bearer the rotation already revoked and logs
re-pointed. Skip whilerolling_sidecar_behind_chainholds. - minor: the item side reads
refreshToken: ""as refreshless, wheresidecar_kind_ofcalls the same shape mis-filled, and the pass re-derivessidecar_summaryfrom the line above it. Reuse both.
Coverage
- minor: a background session's item is Claude Code-created and unowned, and
mcp_sync_may_adoptrefusesbackground, so--bgsessions keep the revoked copy while the wiki says every running session. - minor: an unreadable profile turns into a silent skip on every pass. Log it, as #144 did for the unreadable
rolling_tokenflag. - minor: only the pure predicate is tested. The rolling gate, the adopt check and the background exclusion have no seam, so deleting any of them stays green.
- minor: each live rolling session can cost an item read, a flock wait and a 20 s leg budget in series, about 55 s worst case, with no bound across sessions. That delays the MCP sync behind it.
- minor:
copied_bearer_behind_sidecarandrefresh_copied_bearerare only called from macOS code, so-D warningsfails linux and windows. Gate them likesession_item_carry_gate.
Docs and copy
- minor:
wiki/Auto-Switch.mdsays Claude Code never stores a refreshless bearer in the item, which this PR's premise contradicts. Fix that sentence alongside the new paragraph. - nit:
'{}''srenders as'cld0''s, and "its copy was one the member's rotation had revoked" claims more than the predicate checks. - nit: the mechanism is explained three times in doc comments and once more in the wiki. Once is enough.
The split onto mommy
What the pass uses from #143, none of it on mommy: the clauth-mcp-sync thread with its 15 s throttle, live_session_runtime_dirs and LiveRuntime, mcp_sync_may_adopt, authorize_owned_write, authorize_live_write and failed_on_locked_keychain. Plus the wiki's reference to the sync MCP logins setting. owned_keychain_leg, the Keychain primitives and sidecar_kind_of are already there.
- the item write (
refresh_config_dir_item_bearer) and the two pure functions move over as they are. - the cleanest split carries the two write witnesses over; #143 then rebases onto them. Using
mommy'sauthorize_writeinstead takes the row over, which #143 deliberately keeps apart from adopting. - on
mommytheno_item_leg_folds_in_a_carry_or_a_flockcount becomes 7, and its message drops the MCP sync clause. - if the pass moves onto the rotation stamp (second point above), the throttle and the thread aren't needed at all.
02965ce to
1dab4c0
Compare
|
Done. This now sits on Verified live on macOS with this build: Replies to the review: Design
Coverage
Docs and copy
|
ea2855a to
531e352
Compare
|
Rebased onto current mommy (10b7b77) to clear the conflict. The only overlap was test blocks appended at the end of |
|
Pushed I can't test on macos, could you run your rotation check once more on this head? |
|
Ran the rotation check on Setup: macOS 27.2 (arm64), Claude Code 2.1.316, Result: the copied bearer is re-pointed within a second, and the session keeps working
Gap: an item holding an older revoked bearer is never re-pointedThe queue only knows the bearer that the last stamp in the same process replaced. A session whose item copied an earlier bearer stays on a dead bearer for the rest of its life. Live example from the same run: session 11207-0's item holds This affects:
I'm writing a follow-up: a daemon sweep, at boot and every few minutes, that finds such items and feeds them into this PR's queue, so the write and all its checks stay as they are here. It recognizes a stale copy by where it came from rather than by "differs". The session's row launched on this member's Smaller notes
cc: @uwuclxdy |
uwuclxdy
left a comment
There was a problem hiding this comment.
Approving on 1f3a41cd: def90718 rebased onto mommy 2e528ce8, plus one wiki sentence that now says the item copy can land seconds after a plain start instead of naming a sign-in or a refresh as the trigger, which is what harrisrobin's run showed. No line of the PR's own diff changed in the rebase.
Two tradeoffs accepted for this release:
- the retry queue lives in memory per process: a daemon restart or an exiting stamper drops a pending pair, and that session keeps the revoked bearer until a later stamp queues it again. A durable record beside
session-token.jsonwas rejected as a second copy both sides would have to keep honest. - a non-locked item read that spends its full timeout settles that session, as a swap drops
SwapItemLeg::Failedon one, since a Claude Code-created item can raise an access dialog on every read.
The older-bearer gap ships as its own sweep, stacked on this branch, for 0.18.1.
Landing it as a rebase merge.
|
The branch conflicts with I rebased it and gated it: Could you put it on your branch? Or rebase it yourself, as long as |
|
Stacked on this branch is right, and it goes into 0.18.1: the release session takes it from there once it is open. Thanks for the 401/200 pair, that is the piece the earlier runs did not show. Your run covers the re-point itself: the branch is now |
…s member's stamp revokes it
…tnesses' refusals
A locked keychain, a failed write or a sidecar not ready yet keeps the pair queued on the swap retry backoff; a denied or unparseable item read settles. Also gives state_lock_held and armed_budget_remaining their own doc comments.
def9071 to
9c194ce
Compare
|
Correcting my last comment: the force push was blocked by git-lfs's lock check on my side, not by your fork's permissions. The branch now carries the rebase, head |
Summary
Stacked on #143. This branch carries #143's commits plus 3 of its own. Please review it after #143; I'll rebase once #143 merges.
On macOS, the first time Claude Code saves credentials inside a
clauth startsession (an MCP sign-in or refresh, for example), it writes the whole blob into the session's per-session Keychain item. That blob includes a copy of the session'sclaudeAiOauthbearer, read from the symlinked.credentials.json, and the runtime's.credentials.jsonis gone afterwards. From then on the session reads only that copy. For a member on a rolling token, the copy is a refreshless bearer. When the member's chain rotates, clauth re-stampssession-token.jsoncorrectly, but the session never sees it, and the rotation has revoked the bearer it holds. The session fails withOAuth token revoked.What I saw on 2026-10-27 (fingerprints only):
developersrotated at 12:48:31Z, and its sidecar andcredentials.jsonmoved to the new bearer straight away.OAuth token revokedat 12:49:29Z..credentials.jsongone, and the item holdingclaudeAiOauthplusmcpOAuth.I think this also explains the misses described in #144: there the sidecar was re-stamped, but sessions that had detached like this never read it.
Fix. A daemon pass, macOS only, runs on the existing MCP-sync thread at most every 15 s. It covers live clauth sessions whose member has
rolling_tokenset. When a session's item holds a refreshless bearer that differs from the member's live rolling sidecar (copied_bearer_behind_sidecar), it writes the sidecar'sclaudeAiOauthinto the item and keeps every other key,mcpOAuthincluded.sync MCP loginsis set to.Verified live (macOS, Claude Code 2.1.310, built from this branch):
koushikrajrotated at about 13:30:06Z with two detached sessions on it. The daemon loggedre-pointed session 18075-0's per-session Keychain item at 'koushikraj@procteye.com''s current rolling bearer …at 13:30:07Z, and the same for 18891-0 at 13:30:08Z. Neither session hitOAuth token revokedafterwards. A second member,getintouch, rotated at 13:35:17Z, also cleanly.Checklist
sk-ant-*, no~/.claude/~/.clauthcontents)cargo fmt --all -- --check,cargo clippy --locked --all-targets --all-features -- -D warningsandcargo test --locked --all-featurespass locallyOn this branch:
cargo fmt --all -- --checkandcargo clippy --locked --all-targets --all-features -- -D warningsare clean.copied_bearer,mcp_,sync_mcp_logins,hand_off,rolling,restamp,no_item_leg_folds) pass: 158 tests.a_copied_bearer_behind_a_live_rolling_sidecar_is_refresheda_copied_bearer_is_left_when_nothing_proves_it_stale(covers equal bearers, a real login, a dead sidecar, a mint, a mis-fill and garbage shapes)no_item_leg_folds_in_a_carry_or_a_flocknow counts 8 owned legs, with the new site named. The new call has no carry, flock or row retouch inside it.AI