beta-stabilize: hold anthropic-beta at its first-seen value per session - #340
beta-stabilize: hold anthropic-beta at its first-seen value per session#340deafsquad wants to merge 2 commits into
Conversation
Closes the cache-key churn described in cnighswonger#326: CC toggles the beta set between consecutive turns of one session, and each toggle is a different cache key for an otherwise identical request. Snapshots the set at first sight per session and emits it on every subsequent turn. Deltas CC tries to introduce are reported on ctx.meta and to stderr, never forwarded — first-seen wins, and the extension makes no judgement about which betas are desirable. Opt-in via CACHE_FIX_BETA_STABILIZE=1, default off, matching the discipline cnighswonger#326 asks for: it changes what we send upstream. Order 530, after auto-1m-guard (520). That ordering is load-bearing rather than cosmetic — auto-1m-guard in strip mode removes context-1m from the same header, so snapshotting before it would freeze a token the next stage then removes and the emitted value would differ from the snapshot on every turn. Reuses findBetaHeader / parseBetaTokens / joinBetaTokens from auto-1m-guard and resolveSessionId from cache-telemetry rather than restating them.
session-key-invariants caught this: betaSessionKey returned the bare session id, so two conversations under one session id shared a snapshot. Every subagent of a session runs the same agent prompt under the same session id — the collision that put 39 conversations in one insertion-normalization bucket and that deferred-tool-rewrite inherited. Now the same key shape as resolveToolRewriteSessionKey: s-<sid>-<systemPromptSubKey>-<conversationSubKey>. It matters here even though anthropic-beta is CC-process-global: a coarse key would impose conversation A's first-seen set on conversation B and send B a header nobody asked for. The reverse — more keys than processes — costs nothing in this design, because a new key snapshots on its first turn rather than waiting to promote a baseline. Three tests added for the invariants directly, plus an end-to-end case showing a subagent under the same session id keeps its own set. 22/22 here, session-key-invariants 4/4.
|
CI caught a real one, thank you —
Fixed in a771678 with the same key shape as Worth recording why the invariant holds here even though Added three tests against the invariants directly plus an end-to-end case One correction to my earlier note: I wrote that your full suite had not been run — Claude Opus 5, working with @deafsquad |
Closes the cache-key churn in #326.
What
CC toggles the
anthropic-betaset between consecutive turns of one session,and each toggle is a different cache key for an otherwise identical request.
This snapshots the set at first sight per session and emits it on every
subsequent turn. Deltas CC tries to introduce are reported and not forwarded.
Opt-in via
CACHE_FIX_BETA_STABILIZE=1, default off — it changes what we sendupstream, which is the discipline #326 asks for.
Evidence
The test replays the sequence measured on visits-01 in #326 through one
session, and asserts both halves — the defect and the fix:
Asserting the OFF case matters: without it the test could pass against a
neutered predicate.
node --test test/proxy-beta-stabilize.test.mjs→ 19/19.Design notes
auto-1m-guard(520) — load-bearing, not cosmetic.auto-1m-guardin strip mode removescontext-1mfrom this same header, sosnapshotting before it would freeze a token the next stage removes, and the
emitted value would differ from the snapshot on every turn.
extensions.jsonentry.loadExtensionsresolvescfg?.order ?? ext.order ?? 1000andcfg?.enabled ?? ext.enabled ?? true,so the module-declared order is the default — same as
auto-1m-guard. Saythe word if you would rather it were listed explicitly.
findBetaHeader/parseBetaTokens/joinBetaTokenscome fromauto-1m-guard,resolveSessionIdfromcache-telemetry.sessions would send a set the caller never asked for, which is worse than not
stabilizing.
Non-Functional Requirements
Under the ~300-line production threshold (140 lines), but the checklist is
cheap and #326 is a wire-affecting change:
The decision itself is one exported pure function; everything else is header
plumbing borrowed from
auto-1m-guard.surface, nothing persisted, nothing logged beyond beta token names (already
public identifiers). The snapshot map holds token strings keyed by session
id, in memory only.
module-level
Map, bounded at 500 sessions with oldest-out eviction so along-lived proxy cannot accumulate an entry per session seen.
Anthropic's cache key, so it wants a human look regardless of the size.
Known divergence from our own implementation
We run a variant in a private proxy that pins only when set membership
matches and lets a genuine beta change through, on the reasoning that
suppressing a real change sends Anthropic a header the caller did not ask for.
This PR deliberately implements what #326 specifies — first-seen wins, hold
through the change — rather than substituting our design. Happy to add the
set-match behaviour as a second mode if you want it; it is a few lines on top
of
planStableBetas.Caveat
Your full suite exceeds 10 minutes on this machine and was not run to
completion. Verified: the new tests (19/19),
proxy-auto-1m-guard(23/23, themodule imported from),
proxy-pipeline(15/15, the loader).absence-scanisred on
mainfor an unrelated Windows reason — see #339, which is independentof this PR.
— Claude Opus 5, working with @deafsquad