fix(anthropic-subscription): report a same-turn retry on another seat as a seat switch, with the serving seat in every continuity line
Part of code-yeongyu/oh-my-openagent#10304. Related: #2081 (persisted slot blocks; the other half of the same cascade).
Problem
Prompt caches never cross Anthropic organizations (prompt caching docs: "Different organizations never share caches, even if they use identical prompts"). Each subscription seat is its own organization, so the first request on a new seat writes the whole prefix.
decideFromBinding lets account drift on a shared-root lane fall through to the retry checkpoint (#1432), and that branch labelled the result fork / timeout_retry, the same label as a same-seat retry that reads its cache. In the reported incident (5.1.33, four seats) every timeout_retry record read only the 29,319-token system and tool prefix and wrote 0.8-0.9M tokens. The logs could not tell a seat switch from a same-seat retry, or say which seat served a request.
Change
session-continuity.ts:retryCheckpointDecisiontakes the label. A retry on another seat isaccount_changed; a same-seat retry staystimeout_retry. The fork boundary and offset are unchanged, so the intent of #1973 and #1974 (fork at a mapped boundary inside the proven prefix) is kept and their tests stay green.session-observability.ts,session-stream.ts: every continuity observation andclaude_sdk_oauth_session_continuitylog line carriesseat(first 8 hex digits of SHA-256 of the slot name, never the name) andseatChanged(the attempt runs on another seat than the lineage's last one).
This PR changes no request bytes: only labels and log fields. It does not change failover policy (when to rotate seats, a per-turn cold-write budget); that is a separate decision tracked on #10304.
Evidence
- RED at
f755b7d815(main), using the new assertions: 3 failures.anthropic-subscription-continuity-retry-checkpoint: a failover fork was labelledtimeout_retry.1432-anthropic-subscription-failover-reattach: noaccount_changedfork was observed.723-anthropic-subscription-timeout-abort-retry-continuity: the observation carried no seat.
- GREEN at this head: the whole
anthropic-subscriptiontest cluster passes: 112 files / 764 tests passed, 4 skipped.compaction/lane-policyandsession-log(which also read these observations) pass 25/25. Two more tests that pinned the old output exactly were updated:anthropic-subscription-diagnostic-renderand1432-anthropic-subscription-observation-emit-once,tsc --noEmitpasses, andbiome checkreports nothing. - The same-seat retry reading its cache is the behaviour #723 shipped and measured live (
changes.md: "five 429s, thenfork / timeout_retrywith a full cache read"); regression 723 still pins the delta-only resend and now alsoseatChanged: false. - No slot name reaches an observation (asserted in 1432).
Thanks to 욜로개발조아 for the measured incident report.
Summary by cubic
Fixes the Anthropic Subscription continuity logs so a same-turn retry that fails over to another seat is labeled as a seat switch, not a same-seat timeout retry.
- Retry checkpoint decisions keep the same fork boundary and offset but now carry
account_changedfor a seat switch andtimeout_retryfor a same-seat retry, since a new seat writes the whole prompt cache. - Continuity observations and
claude_sdk_oauth_session_continuitylog lines now include aseatdigest (first 8 hex digits of an HMAC-SHA-256 of the slot name keyed by a per-install salt, never the name) and aseatChangedflag; only decision observations carry a seat, not the terminalfailedor non-residentdisabledones. - The per-install salt (32 bytes, mode 0600) is created atomically via a staged write plus hard-link, so concurrent first uses converge on one salt and digests stay unguessable without it.
- The tracker entry notes that the old
timeout_retrylabels in the #1432 and #723 entries are superseded, leaving dated entries append-only. - Regression tests pin that no slot name, access token, or refresh token reaches an observation or log line.
- No request bytes or failover policy change; only labels and log fields.