Skip to content

Add /claude-prime: start 5h quota windows right after reset - #127

Open
iceteaSA wants to merge 2 commits into
cortexkit:mainfrom
iceteaSA:feat/prime
Open

Add /claude-prime: start 5h quota windows right after reset#127
iceteaSA wants to merge 2 commits into
cortexkit:mainfrom
iceteaSA:feat/prime

Conversation

@iceteaSA

@iceteaSA iceteaSA commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

An OAuth account's five-hour quota window only starts counting when a request fires. After a reset, an idle account's window sits unstarted — the first real prompt starts it late, pushing every subsequent reset later. /claude-prime (opt-in, default off) fires one minimal request per account ~1 minute after its 5h window resets, so the window starts immediately.

What it does

  • Scope: main + every enabled OAuth fallback, each tracked independently against its own five_hour.resetsAt.
  • The prime request: claude-haiku-4-5, max_tokens: 1, system Reply with 1 when you receive 0., user 0 — no streaming/thinking/tools/cache_control. ~20 input + 1 output tokens. Sent direct (not relayed) through rewriteUrl() with full OAuth identity headers.
  • Exactly one request per account per reset cycle across any number of concurrent opencode processes: atomic claim markers (writeFile with wx) keyed <accountId>-<resetsAtEpochMs> under $TMPDIR/opencode-anthropic-auth/prime/, swept after 6h by mtime.
  • Redundancy guard: before firing, the account's quota is force-refreshed; freshness is derived from the snapshot itself (checkedAt advanced vs a pre-call baseline), so cached returns (429 backoff, cross-process quota-lock) can never fire against an already-started window. A future resetsAt records "window active" and skips without claiming.
  • Catch-up on boot: a reset that elapsed while no process ran fires on the first tick (unless a real request already started the window).
  • Eligibility: OAuth accounts only, enabled, no permanent refresh error, killswitch-passing. Main/fallback tokens are refreshed through the existing paths before both the fresh-check and the fire.
  • Cumulative accounting: per-account counters (count, inputTokens, outputTokens, since) in runtime state (never in config); cost estimate derived at display time from Haiku 4.5 pricing constants.

Surfaces

  • /claude-prime on|off|status — registered command + TUI modal (Enable / Disable / Status / Back; Status shows per-account rows: next prime time, primed ✓, window active, error), Pi display-only.
  • Expanded-sidebar Prime row (next-due / primed / error tone); nothing in the collapsed view. Toggling publishes sidebar state immediately.
  • New prime log channel: fire success info, fire/token-refresh failures warn (distinct events), skips debug, lifecycle trace; setting changes info on commands. Payloads carry labels and values only — never tokens or bodies.

Lifecycle

PrimeManager (core) mirrors CacheKeepManager: unref'd 60s tick, idempotent start/stop, plugin-level singleton guard (a reloaded plugin stops the previous instance), and the persisted opt-in + stop state are re-checked after every await boundary so /claude-prime off prevents any in-flight fire.

Known bounded limitation, documented in-code: cross-process counter increments use the in-process save mutex, so two processes priming different accounts in the same tick can lose one cosmetic counter increment. Auth data is unaffected.

Verification

  • 1104 tests green (core 78, opencode 847, pi 53 — all new logic test-first with red-first evidence), typecheck + biome clean.
  • Live TUI verification: palette registration, modal open/Enable/Disable/Status/Back navigation, config round-trip, sidebar row appearing on enable and clearing on disable — all exercised in a real opencode -s session against an isolated state dir.
  • Concurrency: two managers sharing a marker dir produce exactly one fire (wx atomicity test); claim-then-fire semantics; marker sweep by mtime.

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Starts each OAuth account’s five-hour quota window immediately after reset by sending one minimal claude-haiku-4-5 request, opt-in via /claude-prime. Previously the window started on the first real request; now it starts ~60s post‑reset to keep reset times aligned at negligible cost.

  • Exactly one prime per account per reset across processes via atomic wx markers under $TMPDIR/opencode-anthropic-auth/prime/<storage-fp>/<auth-lineage>/, swept after 6h; catches up on boot; 5‑min throttle; model‑aware killswitch blocks Haiku by display name if modelId is absent.
  • Requires a fresh quota snapshot; skips cached/backoff results or already‑active windows. QuotaManager.refresh* returns { quota, fetched } to distinguish network fetches.
  • Introduces a stable OAuth auth‑lineage id minted at login and threaded through refresh; marker namespace includes the lineage. @opencode adopts a single PrimeManager per storage fingerprint and rebinds on path changes.
  • Measures usage from response accounting; persists per‑account counters in runtime state; displays projected cost using CLAUDE_HAIKU_4_5_PRICING (exports CLAUDE_HAIKU_4_5_MODEL_ID and CLAUDE_HAIKU_4_5_PRICING from core).
  • Surfaces: @opencode adds /claude-prime on|off|status and a sidebar section; @pi exposes Status only. Docs updated (architecture, structure, readmes).
  • Required action: none. Off by default; enable with /claude-prime on.

Written for commit 8be0644. Summary will update on new commits.

Review in cubic

Greptile Summary

This PR adds opt-in priming for OAuth five-hour quota windows. The main changes are:

  • A per-account scheduler that sends one minimal Haiku request after each reset.
  • Atomic cross-process claims and stable authentication-lineage markers.
  • Fresh quota checks and model-aware killswitch enforcement before sending.
  • Persistent usage counters, commands, TUI status, and sidebar state.
  • Explicit metadata that separates network quota fetches from cached results.

Confidence Score: 5/5

This looks safe to merge.

  • Cached quota results no longer pass the fresh-check gate.
  • The latest quota snapshot is checked against the Haiku-scoped killswitch before claiming.
  • No blocking issue remains in the reviewed fixes.

Important Files Changed

Filename Overview
packages/core/src/prime.ts Adds scheduling, eligibility checks, atomic claims, request sending, accounting, and lifecycle handling.
packages/core/src/quota-manager.ts Adds metadata that distinguishes network quota fetches from cached results.
packages/core/src/accounts.ts Adds prime settings, runtime counters, authentication lineage, and scoped-model matching.
packages/opencode/src/index.ts Connects the scheduler to OpenCode authentication, quota refresh, commands, requests, and sidebar state.
packages/opencode/src/prime-manager-registry.ts Shares schedulers by account-storage identity across plugin instances.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Prime tick] --> B{Account eligible and reset due?}
  B -- No --> Z[Skip]
  B -- Yes --> C[Refresh token and quota]
  C --> D{Quota fetched from network?}
  D -- No --> Z
  D -- Yes --> E{Haiku killswitch passes?}
  E -- No --> Z
  E -- Yes --> F{Window already active?}
  F -- Yes --> Z
  F -- No --> G[Claim account and reset marker]
  G --> H{Claim won?}
  H -- No --> Z
  H -- Yes --> I[Send minimal Haiku request]
  I --> J[Persist usage and refresh quota later]
Loading

Reviews (19): Last reviewed commit: "docs: document /claude-prime architectur..." | Re-trigger Greptile

Context used:

  • Context used - AGENTS.md (source)
  • Context used - captures/AGENTS.md (source)

Comment thread packages/core/src/prime.ts Outdated
Comment thread packages/opencode/src/index.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 17 files

Architecture diagram
sequenceDiagram
    participant User as User (TUI / Pi)
    participant Cmd as Command Handler
    participant PM as PrimeManager
    participant Store as Account Storage (state + config)
    participant QM as QuotaManager / Usage API
    participant Claim as Claim Directory ($TMPDIR/prime/)
    participant API as Anthropic Messages API

    Note over User,API: COMMAND FLOW: /claude-prime on|off|status

    User->>Cmd: /claude-prime on
    Cmd->>Store: setPrimePersistentEnabled(true)
    Cmd->>PM: start (unref'd 60s tick)
    Cmd-->>User: Enabled status summary

    User->>Cmd: /claude-prime off
    Cmd->>Store: setPrimePersistentEnabled(false)
    Cmd->>PM: stop (cancels any in-flight)
    Cmd-->>User: Disabled status summary

    User->>Cmd: /claude-prime status
    Cmd->>Store: load stored counters + quotas
    Cmd-->>User: Per-account status rows

    Note over User,API: Pi is display-only (no toggle / manager)

    Note over PM,API: PRIME MANAGER TICK (every 60s, unref'd)

    loop For each OAuth account (main + enabled fallbacks)
        PM->>PM: Check if opt-in enabled (re-reads config after each await)
        alt Enabled and account eligible (OAuth, no permanent error, killswitch pass)
            PM->>QM: Force-refresh account quota (with fresh access token)
            Note over PM,QM: Capture pre-call checkedAt baseline
            QM-->>PM: Quota snapshot + checkedAt + fresh flag
            alt Snapshot is fresh (checkedAt > pre-call baseline)
                PM->>PM: Compute nextDueAt = resetsAt + 60s
                alt Current time >= nextDueAt (window should be primed)
                    PM->>Claim: try atomic claim: writeFile(accountId-epochMs, 'wx')
                    alt Claim acquired (wx succeeded)
                        PM->>PM: Build minimal request body (model haiku, max_tokens 1, "0")
                        PM->>API: POST /v1/messages with OAuth identity headers
                        Note over PM,API: No streaming, thinking, tools, or cache_control
                        API-->>PM: 200 OK + usage { input_tokens, output_tokens }
                        PM->>Store: incrementPrimeUsagePersistent(counters)
                        PM->>PM: Log success (info · prime)
                    else Claim already exists (wx threw EEXIST)
                        PM->>PM: Skip (another process already primed)
                    end
                else Window not yet due
                    PM->>PM: Skip (too early)
                end
            else Snapshot is cached (checkedAt same as baseline)
                Note over PM,PM: Freshness guard: quota backoff / already started
                PM->>PM: Skip (window assumed already active)
            end
        else Not eligible
            PM->>PM: Skip (log reason at debug/trace)
        end
    end

    Note over PM,API: Claim markers swept after 6h by mtime
Loading

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

Comment thread packages/core/src/prime.ts
Comment thread packages/core/src/prime.ts Outdated
Comment thread packages/core/src/tests/prime.test.ts
Comment thread packages/core/src/accounts.ts Outdated
Comment thread packages/opencode/src/tui.tsx Outdated
Comment thread packages/opencode/src/tests/sidebar-state.test.ts Outdated
Comment thread packages/opencode/src/tests/sidebar-state.test.ts
Comment thread packages/opencode/src/tui.tsx Outdated
Comment thread packages/opencode/src/tests/index.test.ts
Comment thread packages/opencode/src/tests/command-dialogs.test.ts
Comment thread packages/core/src/prime.ts
Comment thread packages/opencode/src/index.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 5 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/core/src/accounts.ts
Comment thread packages/opencode/src/index.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 12 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/core/src/prime.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 3 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/core/src/prime.ts Outdated
Comment thread packages/core/src/prime.ts Outdated
Comment thread packages/core/src/tests/prime.test.ts
Comment thread packages/opencode/src/index.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/core/src/prime.ts Outdated
@iceteaSA
iceteaSA force-pushed the feat/prime branch 2 times, most recently from b62bfbf to 24dd9fa Compare July 17, 2026 22:08
@ualtinok

Copy link
Copy Markdown
Contributor

This is a substantial feature and I am keeping it open, but it needs a dedicated design/rebase pass before merge rather than being treated as a routine command addition.

The main correctness issue I found is marker identity. Bootstrap markers are named from accountId alone (main-bootstrap for the primary account), and reset markers use account ID plus reset time. Separate account files/configurations on the same machine can therefore suppress one another even when they contain different OAuth accounts. The marker namespace needs to include the account-storage identity and a non-secret token/account fingerprint.

The process-wide __anthropicPrimeManager singleton also needs an explicit ownership rule for opencode serve, where multiple project plugin instances load in one process. Replacing the previous manager is only safe if all instances share the same account file and schedule; otherwise one configuration silently disables another.

Before merge, please also:

  • rebase after the current sticky-routing and CacheKeep changes land;
  • add multi-config tests for marker isolation and manager ownership;
  • provide a production-like reset-cycle test proving one prime request per account/reset across concurrent processes and restarts;
  • document clearly that this deliberately consumes a real Haiku request and that Pi's command is status-only.

The existing cross-process marker and quota-refresh tests are a good base, but these ownership boundaries need to be resolved first.

@iceteaSA
iceteaSA force-pushed the feat/prime branch 2 times, most recently from ffc7934 to 6999696 Compare July 19, 2026 15:13
@iceteaSA

Copy link
Copy Markdown
Contributor Author

Rebased onto v1.16.0 (sticky-balanced routing + CacheKeep changes resolved) and completed the design pass — squashed to a single commit (6999696).

Marker namespace / multi-config: all prime markers (fire markers and bootstrap sentinels) now live under prime/<fp12>/, where fp12 is a truncated sha256 of the canonicalized account-storage path (realpathSync, with a resolve() fallback for not-yet-existing files, so symlink and real-path spellings map to one namespace). Two configs on one machine now prime independently; the accountId encoding within a namespace is unchanged. Regression tests cover both directions: different configs both fire, same config still dedupes to exactly one fire. One accepted tradeoff, documented at the namespace helper: markers written by the old un-namespaced layout are not consulted, so the first window after an upgrade can double-fire once (~21 tokens, self-heals).

Singleton ownership (opencode serve multi-project): a module-level registry keys managers by (process × storage fingerprint). A plugin instance whose config resolves to an existing fingerprint adopts the existing manager and rebinds its injected dependencies to the current load (no stale closures from a previous plugin context); an instance with a different config gets an independent manager; a slot whose path changes releases its old manager, which is stopped and evicted once unowned. Lifecycle tests cover adoption identity, closure rebinding (proven by asserting the second load's sender fires, not the first's), different-path eviction, and no duplicate timers.

Reset-cycle test: a production-shaped test walks two full windows with injected clock/quota/send seams — expiry → exactly one fire at reset+60s (asserting the haiku request shape) → new window → no further fires until the next reset → second cycle fires again.

Full gates green: opencode 891, core 113, pi 58, e2e 21, typecheck/lint clean.

@ualtinok

Copy link
Copy Markdown
Contributor

Thanks — the storage-path manager registry and cross-config namespace fix the ownership issue I raised, and the isolated local validation passed (1,062 unit tests, 21 E2E tests, and typecheck). I found four remaining blockers:

  1. The marker namespace still does not identify the OAuth account. primeStorageFingerprint() scopes only by account-storage path, while main-account claims remain main-* and hasAnyClaimMarker('main') accepts any such marker. If the user replaces the main OAuth account in the same config file, markers from the old account can suppress bootstrap/priming for the new account until the marker sweep. Include a non-secret account/token fingerprint in the claim identity as well as the storage fingerprint, and add a regression that switches main OAuth credentials in the same file.

  2. The cumulative usage counter still has a declared lost-update race. incrementPrimeUsagePersistent() performs an unlocked load/modify/save, and its comment says simultaneous successful primes may lose increments. Since the command reports cumulative requests/tokens/cost, these values need to be exact. Please make this a cross-process locked read-modify-write and add a concurrent increment regression.

  3. The actual Anthropic request shape is not production-verified. sendPrime() applies OAuth headers to the minimal body but does not run the normal Claude Code body identity/billing-header/CCH pipeline. The current tests inject the sender or use mocks, so they do not prove that Anthropic accepts this exact wire request. Please either build the request through a shared canonical Claude Code request helper, or add a redacted live wire artifact/test proving the intentionally minimal shape is accepted and document why it differs from normal OAuth messages.

  4. User documentation is still absent. There are no /claude-prime entries in the root, OpenCode, or Pi READMEs. Please document that it deliberately consumes a real Haiku request, explain the schedule/cost, and state clearly that Pi exposes status only and ignores toggle arguments.

Once these are resolved I will rerun the complete gate.

@iceteaSA

Copy link
Copy Markdown
Contributor Author

All four resolved in 1754e4c (squashed).

1 — Claim identity includes the OAuth account. Markers now live under prime/<storage-fp>/<account-fp>/, where the account fingerprint is a truncated hash of the refresh token — stable across routine access-token rotations, distinct across accounts. Fire markers, bootstrap sentinels, the throttle, claim lookup, and the stale-marker sweep are all keyed by this identity; the sweep only ages out markers within an identity and cannot touch another identity's live markers. Regression: switching main OAuth credentials in the same config file previously left the new account suppressed by the old account's markers (1 fire), now both identities prime independently (2 fires), and the old identity's markers age out via the existing sweep.

2 — Usage counters are exact. incrementPrimeUsagePersistent is now a cross-process locked read-modify-write (config → state lock ordering, matching saveAccountsLocked; re-read under the lock, then increment). Regression: two simulated processes × 20 concurrent increments previously lost updates (20/40), now totals 40/40.

3 — Production request shape. sendPrime routes the minimal body through the canonical rewriteRequestBody pipeline — Claude Code identity, billing headers, metadata, and CCH signing over the final serialized body — before OAuth headers are applied, so a prime request is indistinguishable in treatment from a normal OAuth message. The transform is injected into PrimeManager from the OpenCode side, keeping the core package free of opencode imports; Pi remains status-only and never instantiates the manager. A test asserts the outgoing body is byte-identical to the canonical rewritten/signed output.

4 — Documentation. /claude-prime sections added to the root, OpenCode, and Pi READMEs: opt-in default-off, schedule and bootstrap semantics, the explicit real-request cost (one claude-haiku-4-5 call, ~20 input + 1 output tokens per fire), cross-process single-fire, and Pi's status-only contract (toggle args are ignored on Pi; toggling happens in OpenCode).

Full gates: opencode 891, core 116, pi 58, e2e 21, typecheck/lint clean.

@ualtinok

Copy link
Copy Markdown
Contributor

The counter locking, canonical request transformation, and documentation updates are good. One marker-identity blocker remains.

The account fingerprint is derived from the refresh token:

  • main: tokenFingerprint(auth.refresh ?? auth.access ...) at loader time;
  • fallback: tokenFingerprint(account.refresh ?? account.access ...) when resolving the account.

Anthropic rotates refresh tokens during normal OAuth refresh. Therefore the same OAuth account receives a new marker directory after routine refresh (immediately for fallbacks, and for main after a process reload). Existing once-per-reset claims are no longer visible, so the same quota window can be primed again.

I reproduced this directly with PrimeManager: same storage path, same account, same five-hour reset epoch; first tick uses fingerprint A, then a routine token rotation changes it to fingerprint B; the second tick calls sendPrime() again (sends: 2).

Please use a persistent, non-secret OAuth-account identity that survives access- and refresh-token rotation and changes only when the user actually replaces/re-logs the account. A generated auth-lineage ID persisted at login and carried across refresh is one option; an authoritative account UUID is another when reliably available. Add a regression that rotates the refresh token for the same account and proves exactly one send, plus a same-label re-login regression proving a genuinely replaced account gets a new identity.

The documented ~20 input tokens also needs measurement or removal. After the new canonical rewrite, the minimal 130-byte body becomes 345 bytes with three system blocks and 170 characters of system text before Anthropic tokenization. Please use observed usage.input_tokens from a live redacted smoke request for the estimate, or avoid claiming a fixed token count/cost.

PR #128 is now merged, so please rebase this update onto current main before the next gate.

@iceteaSA
iceteaSA force-pushed the feat/prime branch 2 times, most recently from f4ed53e to 7479fee Compare July 19, 2026 23:10
@iceteaSA

Copy link
Copy Markdown
Contributor Author

Fixed the marker-identity blocker and rebased onto current main.

Persistent OAuth lineage identity. Markers are no longer keyed on the rotating refresh-token fingerprint. Each OAuth account carries an authLineageId minted at login (a UUID at the add-oauth-finish handler and the CLI login path) and preserved verbatim across every refresh write-back (refreshClaudeOAuthToken and the main/fallback refresh sites). Pre-existing accounts lazy-migrate on first prime lookup: under the config→state lock, mint-and-persist once, then reuse forever. Two concurrent first-lookups converge on a single id (re-read under the lock, adopt an existing one rather than overwrite). The main account — whose login/refresh we don't own — seeds its lineage once in the state file and reuses it across rotation; a genuine main re-login we cannot detect can re-prime at most once, which is the documented one-time migration.

Your repro is covered red-first: same account, same storage path, same reset epoch, refresh-token rotated between ticks → exactly one sendPrime (was 2). Plus a same-label re-login regression proving a genuinely replaced account gets a distinct identity, a main-rotation regression, a refresh-preservation test, and a concurrent-migration convergence test.

Doc claim. Removed the fixed ~20 input tokens figure from all three READMEs (it was wrong after the canonical rewrite) — replaced with "a minimal request" language, no fixed token/cost claim.

Rebased onto main (post-#128). The rebase reconciled prime's sidebar-section writes with #128's relocated locked routing-preservation path.

Gates green: typecheck · opencode 925 · core 116 · pi 58 · e2e 21 · lint clean.

@ualtinok

Copy link
Copy Markdown
Contributor

The fallback/re-login lineage fixes are good, but the main OpenCode OAuth account replacement case is still unresolved.

I reproduced the remaining path directly against this head:

{"hostAccount":"account-b","firstLineage":"16ffe43b-cf42-4c3d-9cf2-aee5b8a80245","secondLineage":"16ffe43b-cf42-4c3d-9cf2-aee5b8a80245","sameLineage":true,"sends":1,"refreshes":1}

Runtime mechanism:

  1. Main account A primes and writes a marker under the persisted main lineage.
  2. The host Anthropic credential is replaced with account B.
  3. getOrCreatePrimeAuthLineageId("main") receives no current account identity, so it returns A's existing lineage unchanged.
  4. PrimeManager.tick() finds A's marker before quota refresh and suppresses B's prime.

Documenting that a reconnect may wait until the next reset records the limitation, but does not make the marker account-scoped. Please derive or reconcile the main lineage against a stable account identity that survives token rotation but changes on actual account replacement. Claude bootstrap's account UUID looks like the strongest available identity; a persisted equivalent is also fine. Please add a regression that primes account A, replaces the main host credential with account B in the same sidecar, and proves B receives its own prime during the same reset window.

The source/PR wording that still estimates roughly 20–21 input tokens should also be removed or replaced with measured live usage, since canonical request rewriting now adds the Claude Code identity/system blocks and actual accounting already comes from response usage.

@iceteaSA

Copy link
Copy Markdown
Contributor Author

Fixed the main host-account replacement case and rebased onto current main (v1.17.0). New head ebba54d (squashed).

Main lineage reconciled against observed credential identity. The persisted main lineage now carries a bound refresh-token fingerprint:

  • Owned rotation = continuity. Every main refresh performed by the plugin advances the binding — before the rotated credential is published to the host (client.auth.set), with rollback if publication fails. Rotation never changes the lineage, so the once-per-window claim survives routine refreshes.
  • Cross-process adoption reconciles first. Both concurrent-refresh adoption paths (lease adoption and post-wait re-read) reconcile the binding before the adopted token becomes observable to prime lookups, and an active refresh lease suppresses ambiguous handoff lookups — a lookup can no longer land in the adoption window and misclassify a peer's rotation as a replacement.
  • Replacement mints a new lineage. A lookup that observes a refresh token whose fingerprint does not match the binding (and no active lease) is a genuine host-credential replacement: it mints a new lineage under the locked read-modify-write, so account B gets its own marker namespace and its own prime in the same reset window. Concurrent observations converge on one lineage.
  • Fail-safe on missing identity. When no current refresh token is available, the lookup returns the existing lineage (or nothing) without minting or persisting, and the manager skips that account for the tick — no lineage churn.
  • Migration. A legacy persisted lineage with no binding binds once to the current fingerprint, identity unchanged.

Your reproduction is the regression: account A primes → the host credential is replaced with account B in the same sidecar → B mints a distinct lineage and receives its own prime during the same reset window (sends: 2, distinct lineages) — red against the previous head, green now. The inverse guard (rotation keeps sends: 1) is locked alongside it, plus interleaving regressions for both adoption paths and the publish-failure rollback.

Token estimate removed. The fixed "~20–21 input tokens" wording is gone from the source and all three READMEs — cost reporting now defers entirely to measured response usage, which the usage counters already record.

Gates on the rebase: opencode 1019, core 128, pi 58, e2e 23 — all passing, typecheck/lint clean.

rustybret added a commit to rustybret/anthropic-auth that referenced this pull request Jul 25, 2026
Pull upstream PR cortexkit#127 (feat/prime) into main. /claude-prime is an opt-in
(default off) command that fires a minimal claude-haiku-4-5 request ~1min
after each account's 5h quota window resets, so the window starts on time
instead of late. Conflicts resolved additively — kept both the Opus 5
recovery code (formatFallbackModelLabel, recoveryWarmChains) and the prime
manager/formatters that landed adjacent in index.ts and sidebar-state.ts.
@iceteaSA

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (v1.18.0) — clean single commit, new head 7cf6b4e. Reconciled with the Opus 5 recovery-state isolation from ddf5302 (prime's warm-chain block now uses the renamed recoveryWarmChains machinery). Full gates on the rebased tree: typecheck, build, lint, biome check clean; opencode 1052 / core 139 / pi 62 / e2e 24 — all pass.

@iceteaSA

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (v1.19.0) — clean single commit, new head f8e9c27. Auto-merged cleanly against the server-side safety fallback work in 179c5e0 (both touch index.ts/transform.ts/sidebar-state.ts/tui.tsx); typecheck confirms no semantic drift.

Gates on the rebased tree: typecheck, build, lint, biome check clean; opencode 1074 / core 139 / pi 62 all pass.

One e2e test fails, and it is pre-existing on main, not introduced heretool-prefix.test.tsbridges back to a stale Opus cache after more than 20 Fable blocks (line 301) expects "second Opus recovery" but receives the mock's default "ok" response with modelID: "claude-fable-5", i.e. the final recovery does not consume the last scripted Opus response. Verified by running that single test 3× on this branch and 3× on a pristine 41e9aee checkout with no commits of mine: 0/3 pass on both. Happy to open a separate issue for it if useful.

@iceteaSA

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (post-#146, 3a370aa) — clean single commit, new head dbd766c.

Gates on the rebased tree: typecheck, build, lint, biome clean; opencode 1081 / core 139 / pi 62 pass. e2e 26 pass / 1 fail — the pre-existing tool-prefix.test.ts:301 stale-Opus-cache bridge test, unchanged from main and unrelated to this branch.

@iceteaSA

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (v1.19.1, b1d8f8c) — clean single commit, new head f525303.

Reconciled with the injectable plugin timers from 1767881/2755317: that work took a second positional argument on the plugin entry (anthropicAuthPlugin(ctx, timerOverrides)), which this branch's test helper had been using for directory. Merged to getPlugin(client?, directory?, timerOverrides?) and updated the call sites accordingly — six of them, four in regions that merged without conflict, so typecheck rather than git surfaced them.

Gates on the rebased tree: typecheck, build, lint, biome clean; opencode 1086 / core 139 / pi 63 pass. e2e 26 pass / 1 fail — tool-prefix.test.ts:301 "bridges back to a stale Opus cache after more than 20 Fable blocks", still failing identically on main and unrelated to this branch.

@iceteaSA

Copy link
Copy Markdown
Contributor Author

The failing check isn't from this commit — it only touches four markdown files (ARCHITECTURE.md, STRUCTURE.md, and the two changelogs), and the failure is in sidebar-state.test.ts.

Worth a look anyway, because it reads like the test skipping its own scenario rather than a real regression:

expect(written.main.quota.five_hour.usedPercent).toBe(80)
Expected: 80
Received: 10

10 is staleWriter's quota. 80 is successor's — written by the child process that the afterRename hook spawns. The reported duration was 2.29ms. Here that test runs 17.95–21.10 ms across 20 consecutive full-file runs and never once below that; the subprocess spawn accounts for most of it. At 2.29 ms the hook didn't fire, so nothing ever wrote successor, and the test degraded into asserting a plain uncontended write. Its activeId and route assertions then pass for the wrong reason, and only the quota assertion catches it.

Locally at f525303 the full suite is green 3/3 (1086 pass) and this file is 20/20.

I can't tell from the log why the hook didn't run, and I don't have re-run rights here to check whether it repeats. Flagging rather than diagnosing.

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.

2 participants