From 69139c07e2f8efd29f6fe07a95a8c15d910fdb9f Mon Sep 17 00:00:00 2001 From: iceteaSA <171169159+iceteaSA@users.noreply.github.com> Date: Thu, 3 Sep 2026 07:41:46 +0200 Subject: [PATCH 1/4] fix(prime): send with the vault credential for a vault-served fallback --- packages/core/src/prime.ts | 17 +- packages/opencode/src/index.ts | 179 +++++---- packages/opencode/src/tests/index.test.ts | 424 ++++++++++++++++++++++ 3 files changed, 539 insertions(+), 81 deletions(-) diff --git a/packages/core/src/prime.ts b/packages/core/src/prime.ts index 3947e8e9..0cbeb0a9 100644 --- a/packages/core/src/prime.ts +++ b/packages/core/src/prime.ts @@ -70,13 +70,9 @@ export type PrimeSendResult = status?: number ms?: number error: string - // Discriminates the failure kind so the manager can emit the - // spec Logging table's two distinct warn events: `prime token - // refresh failed` (warn · prime · { account, error }) for - // token-refresh failures, `prime fire failed` (warn · prime · - // { account, status?, error }) for HTTP / fetch / identity - // failures during the request itself. - reason?: 'token-refresh' | 'send' + // `vault-cold` is an expected off-path cache warm state, not evidence + // of a failed provider request, so it must not emit the fire-failure warn. + reason?: 'token-refresh' | 'vault-cold' | 'send' } /** @@ -946,7 +942,7 @@ export class PrimeManager { if (this.stopped) return if (!result.ok) { - // Spec Logging table: two distinct warn events. + // `vault-cold` is a cache-warm skip; it is not a provider failure. // - `prime token refresh failed` — token-refresh failure // before the request fires (reason: 'token-refresh'). // - `prime fire failed` — HTTP error / fetch throw / @@ -956,6 +952,11 @@ export class PrimeManager { account: evaluation.label, error: result.error, }) + } else if (result.reason === 'vault-cold') { + logger.debug('prime', 'prime vault credential cold', { + account: evaluation.label, + reason: result.reason, + }) } else { logger.warn('prime', 'prime fire failed', { account: evaluation.label, diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 5822c771..dd0aead8 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -1928,6 +1928,65 @@ const anthropicAuthPlugin = async ( } } + async function reportCapturedClaustrumAuthFailure( + served: { + accountId: string + handle: string + recordVersion: number + }, + reporterSource: ClaustrumReporterSource = 'direct', + options?: { preserveServedVersion?: boolean }, + ): Promise { + const cache = claustrumCredentialCache + if (!cache) return + if ( + served.recordVersion <= + (claustrumLastReportedVersion.get(served.handle) ?? -1) + ) { + return + } + if (!options?.preserveServedVersion) { + const current = cache.peek(served.handle) + // Version match makes reports single-shot per served version. Accepted + // tradeoff: an unrelated cache eviction also suppresses a genuine + // report (worst case one delayed cycle until the next served 401). + if (!current || current.recordVersion !== served.recordVersion) return + } + const key = `${served.handle}\0${served.recordVersion}` + const pending = claustrumAuthFailureReports.get(key) + if (pending) { + await pending + return + } + const report = (async () => { + try { + await cache.reportAuthFailure( + served.handle, + 401, + { + recordVersion: served.recordVersion, + }, + reporterSource, + ) + claustrumLastReportedVersion.set(served.handle, served.recordVersion) + } catch (error) { + handleClaustrumCredentialError(served.accountId, error, served.handle) + logger.warn('claustrum', 'failed to report credential failure', { + accountId: served.accountId, + error: error instanceof Error ? error.message : String(error), + }) + } + })() + claustrumAuthFailureReports.set(key, report) + try { + await report + } finally { + if (claustrumAuthFailureReports.get(key) === report) { + claustrumAuthFailureReports.delete(key) + } + } + } + async function ensureClaustrumCredentialCache(): Promise { if (claustrumCredentialCache) return claustrumCredentialCache const now = claustrumNow() @@ -2564,6 +2623,7 @@ const anthropicAuthPlugin = async ( const start = performance.now() let accessToken: string | undefined let resolvedModel: string | undefined + let servedClaustrumCredential: ClaustrumAccessResolution['served'] try { if (accountId === 'main') { // Use the same refresh path the fresh-check uses, so a missing or @@ -2598,22 +2658,41 @@ const anthropicAuthPlugin = async ( error: `prime: OAuth account ${accountId} is unavailable`, } } - let current = account - try { - // R2: the fire path refreshes ONLY the token, not the quota. - // The fresh-check already performed the single usage-API - // call and persisted the result; the fire path reuses the - // in-memory quota via the headers / URL contract. This keeps - // the cycle at exactly one quota API call per account. - current = await fallbackManager.refreshAccount(account, storage) - } catch (error) { - return { - ok: false, - reason: 'token-refresh', - error: error instanceof Error ? error.message : String(error), + const vaultEnabled = + Boolean(account.claustrumHandle) && + isClaustrumEnabledForAccount(storage, account.id) && + !claustrumBlockedAccounts.has(account.id) + if (vaultEnabled) { + const resolved = resolveClaustrumAccess(account, storage, { + warm: false, + }) + if (!resolved.accessToken || !resolved.served) { + return { + ok: false, + reason: 'vault-cold', + error: 'prime: vault credential is unavailable', + } + } + accessToken = resolved.accessToken + servedClaustrumCredential = resolved.served + } else { + let current = account + try { + // R2: the fire path refreshes ONLY the token, not the quota. + // The fresh-check already performed the single usage-API + // call and persisted the result; the fire path reuses the + // in-memory quota via the headers / URL contract. This keeps + // the cycle at exactly one quota API call per account. + current = await fallbackManager.refreshAccount(account, storage) + } catch (error) { + return { + ok: false, + reason: 'token-refresh', + error: error instanceof Error ? error.message : String(error), + } } + accessToken = current.access } - accessToken = current.access resolvedModel = CLAUDE_HAIKU_4_5_MODEL_ID } @@ -2657,6 +2736,13 @@ const anthropicAuthPlugin = async ( if (!response.ok) { const reason = (await response.text().catch(() => '')) || `HTTP ${response.status}` + if (response.status === 401 && servedClaustrumCredential) { + await reportCapturedClaustrumAuthFailure( + servedClaustrumCredential, + 'direct', + { preserveServedVersion: true }, + ) + } return { ok: false, status: response.status, ms, error: reason } } const data = (await response.json().catch(() => null)) as Record< @@ -5326,66 +5412,13 @@ const anthropicAuthPlugin = async ( recordVersion: number }, reporterSource: ClaustrumReporterSource = 'direct', + options?: { preserveServedVersion?: boolean }, ): Promise { - const cache = claustrumCredentialCache - if (!cache) return - if ( - served.recordVersion <= - (claustrumLastReportedVersion.get(served.handle) ?? -1) - ) { - return - } - const current = cache.peek(served.handle) - // Version match makes reports single-shot per served version. Accepted - // tradeoff: an unrelated cache eviction also suppresses a genuine - // report (worst case one delayed cycle until the next served 401). - if (!current || current.recordVersion !== served.recordVersion) - return - const key = `${served.handle}\0${served.recordVersion}` - const pending = claustrumAuthFailureReports.get(key) - if (pending) { - await pending - return - } - const report = (async () => { - try { - await cache.reportAuthFailure( - served.handle, - 401, - { - recordVersion: served.recordVersion, - }, - reporterSource, - ) - claustrumLastReportedVersion.set( - served.handle, - served.recordVersion, - ) - } catch (error) { - handleClaustrumCredentialError( - served.accountId, - error, - served.handle, - ) - logger.warn( - 'claustrum', - 'failed to report credential failure', - { - accountId: served.accountId, - error: - error instanceof Error ? error.message : String(error), - }, - ) - } - })() - claustrumAuthFailureReports.set(key, report) - try { - await report - } finally { - if (claustrumAuthFailureReports.get(key) === report) { - claustrumAuthFailureReports.delete(key) - } - } + return reportCapturedClaustrumAuthFailure( + served, + reporterSource, + options, + ) } async function sendWithAccessToken( diff --git a/packages/opencode/src/tests/index.test.ts b/packages/opencode/src/tests/index.test.ts index 6a2f0e6e..011992f0 100644 --- a/packages/opencode/src/tests/index.test.ts +++ b/packages/opencode/src/tests/index.test.ts @@ -19086,6 +19086,39 @@ describe('claude-prime direct request', () => { globalThis.setInterval = originalSetInterval }) + function primeCredentialResponse( + accessToken: string, + recordVersion: number, + expiresAtMs = Date.now() + 60_000, + ) { + return { + result: { + payload: Array.from( + new TextEncoder().encode( + JSON.stringify({ access_token: accessToken }), + ), + ), + expires_at_ms: expiresAtMs, + record_version: recordVersion, + }, + } + } + + function primeConnector( + calls: Array<{ method: string; params: Record }>, + handler: (method: string, params: Record) => unknown, + ) { + return async () => + ({ + call: async (_moduleId: string, method: string, params: unknown) => { + const normalized = (params ?? {}) as Record + calls.push({ method, params: normalized }) + return handler(method, normalized) + }, + close: () => {}, + }) as never + } + test('main prime fires a direct messages request with the documented body shape', async () => { const now = Date.now() - 60_000 const past = now - 120_000 @@ -19682,6 +19715,397 @@ describe('claude-prime direct request', () => { expect(primeCall?.auth).toContain('fb-access') }) + test('vault-served fallback prime sends the resident vault credential without refreshing the sidecar', async () => { + const now = Date.now() - 60_000 + const past = now - 120_000 + await useTempAccountFile( + createFallbackStorage({ + accounts: [ + { + id: 'work-alt', + type: 'oauth', + access: 'sidecar-prime-canary', + refresh: 'sidecar-prime-refresh', + expires: Date.now() + 5 * 60 * 60_000, + claustrumHandle: 'prime-vault-handle', + quota: { + five_hour: { + usedPercent: 0, + remainingPercent: 100, + resetsAt: new Date(past).toISOString(), + checkedAt: Date.now() - 60_000, + }, + }, + }, + ], + quota: { + enabled: true, + checkIntervalMinutes: 5, + minimumRemaining: { five_hour: 10, seven_day: 20 }, + failClosedOnUnknownQuota: true, + }, + claustrum: { accounts: { 'work-alt': { enabled: true } } }, + prime: { enabled: true }, + }), + ) + + const credentialCalls: Array<{ + method: string + params: Record + }> = [] + const connector = primeConnector(credentialCalls, (method) => { + if (method === 'credential.get') { + return primeCredentialResponse('vault-prime-access', 101) + } + return { result: {} } + }) + const requestAuthorizations: string[] = [] + let tokenEndpointCalls = 0 + globalThis.fetch = mock((input: unknown, init?: RequestInit) => { + const url = extractUrl(input as string | URL | Request) + const authorization = + new Headers(init?.headers).get('authorization') ?? '' + if (url.includes('/v1/messages')) { + requestAuthorizations.push(authorization) + return Promise.resolve( + new Response( + JSON.stringify({ usage: { input_tokens: 20, output_tokens: 1 } }), + { status: 200, headers: { 'content-type': 'application/json' } }, + ), + ) + } + if (url.includes('/api/oauth/usage')) { + requestAuthorizations.push(authorization) + return freshPrimeQuotaResponse({ + five_hour: { + utilization: 0, + resets_at: new Date(now - 1_000).toISOString(), + }, + }) + } + if (url.includes('/v1/oauth/token')) tokenEndpointCalls += 1 + return Promise.resolve(new Response('not-mocked', { status: 599 })) + }) as unknown as typeof fetch + + const plugin = await getPlugin(undefined, undefined, { + claustrumConnector: connector, + }) + await plugin.auth.loader( + () => + Promise.resolve({ + type: 'oauth', + access: 'main-access', + refresh: 'main-refresh', + expires: Date.now() + 100_000, + }), + { models: {} }, + ) + const mgr = (plugin as any).__primeManager + await mgr.tick() + + expect(requestAuthorizations).toContain('Bearer vault-prime-access') + expect(requestAuthorizations).not.toContain('Bearer sidecar-prime-canary') + expect(tokenEndpointCalls).toBe(0) + await plugin.dispose?.() + }) + + test('a cold vault fallback skips prime without a fire-failed warning', async () => { + const now = Date.now() - 60_000 + const past = now - 120_000 + const dueQuota = { + five_hour: { + usedPercent: 0, + remainingPercent: 100, + resetsAt: new Date(past).toISOString(), + checkedAt: Date.now(), + }, + } + await useTempAccountFile( + createFallbackStorage({ + accounts: [ + { + id: 'work-alt', + type: 'oauth', + access: 'sidecar-cold-canary', + refresh: 'sidecar-cold-refresh', + expires: Date.now() + 5 * 60 * 60_000, + claustrumHandle: 'prime-cold-handle', + quota: dueQuota, + }, + ], + quota: { + enabled: true, + checkIntervalMinutes: 5, + minimumRemaining: { five_hour: 10, seven_day: 20 }, + failClosedOnUnknownQuota: true, + }, + claustrum: { accounts: { 'work-alt': { enabled: true } } }, + prime: { enabled: true }, + }), + ) + + const credentialCalls: Array<{ + method: string + params: Record + }> = [] + const connector = primeConnector(credentialCalls, (method) => { + if (method === 'credential.get') { + return primeCredentialResponse( + 'expired-vault-prime-access', + 102, + Date.now() - 1, + ) + } + return { result: {} } + }) + let sidecarPrimeRequests = 0 + globalThis.fetch = mock((input: unknown, init?: RequestInit) => { + if ( + extractUrl(input as string | URL | Request).includes('/v1/messages') + ) { + const authorization = new Headers(init?.headers).get('authorization') + if (authorization === 'Bearer sidecar-cold-canary') { + sidecarPrimeRequests += 1 + } + } + return Promise.resolve(new Response('{}', { status: 200 })) + }) as unknown as typeof fetch + + const plugin = await getPlugin(undefined, undefined, { + claustrumConnector: connector, + }) + await plugin.auth.loader( + () => + Promise.resolve({ + type: 'oauth', + access: 'main-access', + refresh: 'main-refresh', + expires: Date.now() + 100_000, + }), + { models: {} }, + ) + const mgr = (plugin as any).__primeManager + const directResult = await mgr.options.sendPrime('work-alt') + expect(directResult).toMatchObject({ ok: false, reason: 'vault-cold' }) + expect(sidecarPrimeRequests).toBe(0) + + const records: any[] = [] + const { __setLogTestSink, setLogLevel } = await import( + '@cortexkit/anthropic-auth-core' + ) + setLogLevel('debug') + __setLogTestSink((record: any) => records.push(record)) + mgr.options.refreshQuota = async () => ({ quota: dueQuota, fresh: true }) + await mgr.tick() + await mgr.tick() + __setLogTestSink(null) + setLogLevel('info') + + expect(sidecarPrimeRequests).toBe(0) + expect( + records.filter((record) => record.message === 'prime fire failed'), + ).toHaveLength(0) + const coldRecords = records.filter( + (record) => + record.channel === 'prime' && record.payload?.account === 'work-alt', + ) + expect(coldRecords).toHaveLength(1) + expect(coldRecords[0]?.level).toBe('debug') + await plugin.dispose?.() + }) + + test('a vault prime 401 reports the credential version sent before a cache rotation', async () => { + const now = Date.now() - 60_000 + const past = now - 120_000 + const dueQuota = { + five_hour: { + usedPercent: 0, + remainingPercent: 100, + resetsAt: new Date(past).toISOString(), + checkedAt: Date.now(), + }, + } + await useTempAccountFile( + createFallbackStorage({ + accounts: [ + { + id: 'work-alt', + type: 'oauth', + access: 'sidecar-401-canary', + refresh: 'sidecar-401-refresh', + expires: Date.now() + 5 * 60 * 60_000, + claustrumHandle: 'prime-401-handle', + quota: dueQuota, + }, + ], + quota: { + enabled: true, + checkIntervalMinutes: 5, + minimumRemaining: { five_hour: 10, seven_day: 20 }, + failClosedOnUnknownQuota: true, + }, + claustrum: { accounts: { 'work-alt': { enabled: true } } }, + prime: { enabled: true }, + }), + ) + + const credentialCalls: Array<{ + method: string + params: Record + }> = [] + let rotated = false + const connector = primeConnector(credentialCalls, (method) => { + if (method !== 'credential.get') return { result: {} } + return primeCredentialResponse( + rotated ? 'vault-prime-new-access' : 'vault-prime-401-access', + rotated ? 104 : 103, + ) + }) + const requestEntered = deferred() + const releaseResponse = deferred() + let sentAuthorization: string | undefined + globalThis.fetch = mock(async (input: unknown, init?: RequestInit) => { + if ( + extractUrl(input as string | URL | Request).includes('/v1/messages') + ) { + const authorization = new Headers(init?.headers).get('authorization') + if (authorization === 'Bearer vault-prime-401-access') { + sentAuthorization = authorization + requestEntered.resolve() + await releaseResponse.promise + return new Response('{}', { status: 401 }) + } + return new Response('{}', { status: 200 }) + } + return new Response('not-mocked', { status: 599 }) + }) as unknown as typeof fetch + + const plugin = await getPlugin(undefined, undefined, { + claustrumConnector: connector, + }) + await plugin.auth.loader( + () => + Promise.resolve({ + type: 'oauth', + access: 'main-access', + refresh: 'main-refresh', + expires: Date.now() + 100_000, + }), + { models: {} }, + ) + const mgr = (plugin as any).__primeManager + mgr.options.refreshQuota = async () => ({ quota: dueQuota, fresh: true }) + const tick = mgr.tick() + await requestEntered.promise + const cache = plugin.__claustrumCredentialCache + cache.invalidate('prime-401-handle', 103) + rotated = true + await cache.get('prime-401-handle') + releaseResponse.resolve() + await tick + + expect(sentAuthorization).toBe('Bearer vault-prime-401-access') + expect(credentialCalls).toContainEqual({ + method: 'credential.report_auth_failure', + params: { + handle: 'prime-401-handle', + provider_status: 401, + record_version: 103, + reporter_source: 'direct', + }, + }) + await plugin.dispose?.() + }) + + test('a sidecar prime 401 is not reported as a vault credential failure', async () => { + const past = Date.now() - 120_000 + await useTempAccountFile( + createFallbackStorage({ + accounts: [ + { + id: 'work-alt', + type: 'oauth', + access: 'sidecar-401-access', + refresh: 'sidecar-401-refresh', + expires: Date.now() + 5 * 60 * 60_000, + claustrumHandle: 'sidecar-401-handle', + quota: { + five_hour: { + usedPercent: 0, + remainingPercent: 100, + resetsAt: new Date(past).toISOString(), + checkedAt: Date.now(), + }, + }, + }, + { + id: 'vault-witness', + type: 'oauth', + access: 'witness-sidecar-access', + refresh: 'witness-sidecar-refresh', + expires: Date.now() + 5 * 60 * 60_000, + claustrumHandle: 'vault-witness-handle', + }, + ], + quota: { + enabled: true, + checkIntervalMinutes: 5, + minimumRemaining: { five_hour: 10, seven_day: 20 }, + failClosedOnUnknownQuota: true, + }, + claustrum: { accounts: { 'vault-witness': { enabled: true } } }, + prime: { enabled: true }, + }), + ) + + const credentialCalls: Array<{ + method: string + params: Record + }> = [] + const connector = primeConnector(credentialCalls, (method) => { + if (method === 'credential.get') { + return primeCredentialResponse('vault-witness-access', 105) + } + return { result: {} } + }) + let authorization: string | undefined + globalThis.fetch = mock((input: unknown, init?: RequestInit) => { + if ( + extractUrl(input as string | URL | Request).includes('/v1/messages') + ) { + authorization = + new Headers(init?.headers).get('authorization') ?? undefined + return Promise.resolve(new Response('{}', { status: 401 })) + } + return Promise.resolve(new Response('not-mocked', { status: 599 })) + }) as unknown as typeof fetch + + const plugin = await getPlugin(undefined, undefined, { + claustrumConnector: connector, + }) + await plugin.auth.loader( + () => + Promise.resolve({ + type: 'oauth', + access: 'main-access', + refresh: 'main-refresh', + expires: Date.now() + 100_000, + }), + { models: {} }, + ) + const mgr = (plugin as any).__primeManager + const result = await mgr.options.sendPrime('work-alt') + + expect(result).toMatchObject({ ok: false, status: 401 }) + expect(authorization).toBe('Bearer sidecar-401-access') + expect( + credentialCalls.some( + (call) => call.method === 'credential.report_auth_failure', + ), + ).toBe(false) + await plugin.dispose?.() + }) + test('fallback refresh-token rotation keeps one prime claim per reset window', async () => { const now = Date.now() - 60_000 const past = now - 120_000 From add0649b8d0e408bee7cbc4af27902735f41fe1f Mon Sep 17 00:00:00 2001 From: iceteaSA <171169159+iceteaSA@users.noreply.github.com> Date: Fri, 4 Sep 2026 01:03:03 +0200 Subject: [PATCH 2/4] fix(cachekeep): prewarm a vault-served fallback with its vault credential --- packages/core/src/cachekeep.ts | 9 +- packages/opencode/src/index.ts | 49 +++++++-- packages/opencode/src/tests/index.test.ts | 122 ++++++++++++++++++++++ 3 files changed, 170 insertions(+), 10 deletions(-) diff --git a/packages/core/src/cachekeep.ts b/packages/core/src/cachekeep.ts index bdb15ff7..bb251f79 100644 --- a/packages/core/src/cachekeep.ts +++ b/packages/core/src/cachekeep.ts @@ -379,7 +379,7 @@ export class CacheKeepManager { prepareHeaders?: ( headers: Headers, target: CacheKeepTarget, - ) => Promise | Headers + ) => Promise | Headers | undefined onTrackedSessionsChanged?: ( sessions: readonly CacheKeepTrackedSession[], ) => Promise | void @@ -662,6 +662,13 @@ export class CacheKeepManager { prewarmTarget, ) : new Headers(target.headers) + if (!headers) { + return { + ok: false, + reason: 'OAuth cache prewarm credential is unavailable', + transient: true, + } + } headers.delete('content-length') headers.delete('transfer-encoding') let response: Response diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index dd0aead8..27c505f8 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -2377,6 +2377,10 @@ const anthropicAuthPlugin = async ( let aggregateCacheKeepSessions: ReturnType< CacheKeepManager['trackedSessions'] > = [] + const cacheKeepServedClaustrumCredentials = new Map< + string, + ClaustrumAccessResolution['served'] + >() const cacheKeepManager = new CacheKeepManager({ loadStorage: () => loadAccounts(accountStoragePath), setIntervalImpl: runtimeTimers.setInterval, @@ -2418,7 +2422,14 @@ const anthropicAuthPlugin = async ( return bodyText } }, - onResponse: ({ target, bodyText, status, data, receivedAt }) => { + onResponse: async ({ target, bodyText, status, data, receivedAt }) => { + const served = cacheKeepServedClaustrumCredentials.get(target.id) + cacheKeepServedClaustrumCredentials.delete(target.id) + if (status === 401 && served) { + await reportCapturedClaustrumAuthFailure(served, 'direct', { + preserveServedVersion: true, + }) + } const prepared = cacheKeepDiagnosticsRequests.get(target.id) if (!prepared?.betasHash || !prepared.betas) { cacheKeepDiagnosticsRequests.delete(target.id) @@ -2463,6 +2474,7 @@ const anthropicAuthPlugin = async ( }, prepareHeaders: async (headers, target) => { let accessToken: string | undefined + let servedClaustrumCredential: ClaustrumAccessResolution['served'] const accountId = target.oauthAccountId if (accountId && accountId !== 'main') { const storage = await loadAccounts(accountStoragePath) @@ -2477,16 +2489,29 @@ const anthropicAuthPlugin = async ( `OAuth account ${accountId} is unavailable for cache prewarm`, ) } - let current = account - try { - current = await fallbackManager.refreshAccount(account, storage) - } catch (error) { - logger.warn('cachekeep', 'fallback token refresh failed', { - accountId, - error: error instanceof Error ? error.message : String(error), + const vaultEnabled = + Boolean(account.claustrumHandle) && + isClaustrumEnabledForAccount(storage, account.id) && + !claustrumBlockedAccounts.has(account.id) + if (vaultEnabled) { + const resolved = resolveClaustrumAccess(account, storage, { + warm: false, }) + if (!resolved.accessToken || !resolved.served) return undefined + accessToken = resolved.accessToken + servedClaustrumCredential = resolved.served + } else { + let current = account + try { + current = await fallbackManager.refreshAccount(account, storage) + } catch (error) { + logger.warn('cachekeep', 'fallback token refresh failed', { + accountId, + error: error instanceof Error ? error.message : String(error), + }) + } + accessToken = current.access } - accessToken = current.access if (!accessToken) { throw new Error( `OAuth account ${accountId} has no access token for cache prewarm`, @@ -2534,6 +2559,12 @@ const anthropicAuthPlugin = async ( } catch { setOAuthHeaders(headers, accessToken) } + if (servedClaustrumCredential) { + cacheKeepServedClaustrumCredentials.set( + target.id, + servedClaustrumCredential, + ) + } return headers }, }) diff --git a/packages/opencode/src/tests/index.test.ts b/packages/opencode/src/tests/index.test.ts index 011992f0..c771061c 100644 --- a/packages/opencode/src/tests/index.test.ts +++ b/packages/opencode/src/tests/index.test.ts @@ -2052,6 +2052,128 @@ describe('fallback Claustrum credential resolution', () => { return { authorizations, plugin, result } } + test('CacheKeep prewarm serves and reports a resident vault credential', async () => { + const originalNow = Date.now + const originalRuntimeOverrides = pluginRuntimeOverrides + let now = 1_000 + const intervals: Array<{ callback: () => unknown; ms: number }> = [] + const calls: CredentialCall[] = [] + const prewarmStarted = deferred() + let prewarmUsedVault = false + let prewarmUsedSidecar = false + let bootstrapUsedSidecar = false + let tokenRequests = 0 + try { + Date.now = mock(() => now) as unknown as typeof Date.now + resetCache1hState() + pluginRuntimeOverrides = { + setInterval: mock((callback: () => unknown, ms: number) => { + intervals.push({ callback, ms }) + return { unref() {} } as unknown as ReturnType + }) as unknown as typeof setInterval, + clearInterval: mock(() => {}) as unknown as typeof clearInterval, + } + process.env.OPENCODE_ANTHROPIC_AUTH_DISABLE_PROFILE_HYDRATION = '1' + const storage = fallbackWithClaustrum({ + claustrumHandle: 'handle-cachekeep-prewarm', + claustrum: { accounts: { 'fallback-1': { enabled: true } } }, + } as never) + storage.claudeCache = { enabled: true, mode: 'hybrid' } + storage.cacheKeep = { enabled: true, always: true, subagents: true } + storage.quota = { enabled: false, failClosedOnUnknownQuota: false } + await useTempAccountFile(storage) + const connector = connectorFor(calls, (method) => { + if (method === 'credential.get') + return credentialResponse( + 'vault-cachekeep-access', + 47, + now + 2 * 60 * 60_000, + ) + return { result: {} } + }) + globalThis.fetch = mock((input: unknown, init?: RequestInit) => { + const url = extractUrl(input as string | URL | Request) + const authorization = + new Headers(init?.headers).get('authorization') ?? '' + if (url.includes('/v1/oauth/token')) { + tokenRequests += 1 + return Promise.resolve(new Response('{}', { status: 500 })) + } + if (url.includes('/claude_cli/bootstrap')) { + bootstrapUsedSidecar ||= authorization.includes('stored-fallback') + return Promise.resolve( + Response.json({ oauth_account: { account_uuid: 'fallback-1' } }), + ) + } + const body = JSON.parse(String(init?.body)) as { max_tokens?: number } + if (body.max_tokens === 0) { + prewarmUsedVault = authorization.includes('vault-cachekeep') + prewarmUsedSidecar = authorization.includes('stored-fallback') + prewarmStarted.resolve() + return Promise.resolve(new Response('expired', { status: 401 })) + } + return Promise.resolve( + new Response( + 'event: message_start\ndata: {"type":"message_start","message":{"id":"provider-real","model":"claude-opus-4-8","usage":{}}}\n\nevent: message_stop\ndata: {"type":"message_stop"}\n\n', + { status: 200 }, + ), + ) + }) as unknown as typeof fetch + + const plugin = await getPlugin(undefined, undefined, { + claustrumConnector: connector, + }) + const result = await plugin.auth.loader( + () => + Promise.resolve({ + type: 'oauth' as const, + access: 'main-access', + refresh: 'main-refresh', + expires: now + 100_000, + }), + { models: {} }, + ) + await ( + await result.fetch(MESSAGES_URL, { + method: 'POST', + headers: { 'x-session-affinity': 'ses-cachekeep-vault' }, + body: JSON.stringify({ + model: 'claude-opus-4-8', + stream: true, + messages: [{ role: 'user', content: 'hello' }], + }), + }) + ).text() + now += 55 * 60_000 + const cacheKeepTick = intervals.at(-1) + if (!cacheKeepTick) throw new Error('missing cachekeep interval') + cacheKeepTick.callback() + await Bun.sleep(20) + await withDeadlockGuard( + prewarmStarted.promise, + 500, + 'cachekeep prewarm did not run', + ) + + expect(prewarmUsedVault).toBe(true) + expect(prewarmUsedSidecar).toBe(false) + expect(bootstrapUsedSidecar).toBe(false) + expect(tokenRequests).toBe(0) + expect( + calls.filter( + (call) => call.method === 'credential.report_auth_failure', + ), + ).toHaveLength(1) + expect(calls.some((call) => call.params.record_version === 47)).toBe(true) + await plugin.dispose?.() + } finally { + Date.now = originalNow + pluginRuntimeOverrides = originalRuntimeOverrides + delete process.env.OPENCODE_ANTHROPIC_AUTH_DISABLE_PROFILE_HYDRATION + resetCache1hState() + } + }) + test('disabled gate does not connect and uses the stored token', async () => { const calls: CredentialCall[] = [] let connectorCalls = 0 From ba23c7de0da8f0f4e9ecdb387dca8aeac7725466 Mon Sep 17 00:00:00 2001 From: iceteaSA <171169159+iceteaSA@users.noreply.github.com> Date: Fri, 4 Sep 2026 01:07:22 +0200 Subject: [PATCH 3/4] fix(profile): hydrate a vault-served fallback's profile with its vault credential; pin the token use-site census --- packages/opencode/src/index.ts | 13 +- packages/opencode/src/tests/cachekeep.test.ts | 39 ++ .../tests/fallback-token-use-sites.test.ts | 525 ++++++++++++++++++ packages/opencode/src/tests/index.test.ts | 68 +++ 4 files changed, 643 insertions(+), 2 deletions(-) create mode 100644 packages/opencode/src/tests/fallback-token-use-sites.test.ts diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 27c505f8..caf204c7 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -1366,8 +1366,17 @@ const anthropicAuthPlugin = async ( for (const account of storage.accounts) { if (signal?.aborted) break - if (!isOAuthAccount(account) || !account.access) continue - const accessToken = account.access + if (!isOAuthAccount(account)) continue + const vaultEnabled = + Boolean(account.claustrumHandle) && + isClaustrumEnabledForAccount(storage, account.id) && + !claustrumBlockedAccounts.has(account.id) + const resolved = vaultEnabled + ? resolveClaustrumAccess(account, storage) + : undefined + if (vaultEnabled && (!resolved?.accessToken || !resolved.served)) continue + const accessToken = resolved?.accessToken ?? account.access + if (!accessToken) continue if ( account.profile && !oauthProfileMatchesIdentity(account.profile, account.id) diff --git a/packages/opencode/src/tests/cachekeep.test.ts b/packages/opencode/src/tests/cachekeep.test.ts index 37eb686b..40e6663d 100644 --- a/packages/opencode/src/tests/cachekeep.test.ts +++ b/packages/opencode/src/tests/cachekeep.test.ts @@ -331,6 +331,45 @@ describe('CacheKeepManager', () => { manager.stop() }) + test('retains a tracked target when its prewarm credential is unavailable', async () => { + let now = new Date('2026-05-18T10:00:00').getTime() + const fetchImpl = mock(() => + Promise.resolve(new Response('{}', { status: 200 })), + ) as unknown as typeof fetch + const manager = new CacheKeepManager({ + loadStorage: () => Promise.resolve(hybridStorage()), + fetchImpl, + now: () => now, + prepareHeaders: () => undefined, + }) + await manager.track({ + sessionId: 'ses_cold_vault', + url: 'https://api.anthropic.com/v1/messages?beta=true', + headers: new Headers({ authorization: 'Bearer sidecar-canary-cold' }), + bodyText: JSON.stringify({ + system: [ + { + type: 'text', + text: 'stable', + cache_control: { type: 'ephemeral', ttl: '1h' }, + }, + ], + messages: [{ role: 'user', content: 'hello' }], + }), + storage: hybridStorage(), + cacheMode: 'hybrid', + }) + + now += 55 * 60_000 + await manager.tick() + + expect(fetchImpl).not.toHaveBeenCalled() + expect(manager.trackedSessions().map((session) => session.id)).toEqual([ + 'ses_cold_vault', + ]) + manager.stop() + }) + test('always mode keeps targets alive and prewarms across local midnight', async () => { let now = new Date('2026-05-18T23:58:00').getTime() const windowStorage: AccountStorage = { diff --git a/packages/opencode/src/tests/fallback-token-use-sites.test.ts b/packages/opencode/src/tests/fallback-token-use-sites.test.ts new file mode 100644 index 00000000..f163508a --- /dev/null +++ b/packages/opencode/src/tests/fallback-token-use-sites.test.ts @@ -0,0 +1,525 @@ +import { afterEach, describe, expect, mock, test } from 'bun:test' +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { + type AccountStorage, + resetCache1hState, + resetClaudeCodeIdentityCachesForTest, + saveAccountState, + saveAccounts, + tokenFingerprint, +} from '@cortexkit/anthropic-auth-core' +import { AnthropicAuthPlugin } from '../index' +import { LANE_START_REQUEST_HEADER } from '../lane-start' +import { extractUrl, MESSAGES_URL } from './test-fetch' + +type Site = 'request' | 'quota' | 'prime' | 'cachekeep' | 'recovery' | 'profile' + +type OutboundRecord = { + url: string + authorization: string + bodyHasCanary: boolean + headersHaveCanary: boolean + body: string +} + +type IntervalRecord = { callback: () => unknown; ms: number } + +const originalFetch = globalThis.fetch +const originalNow = Date.now +const activePlugins = new Set<{ dispose?: () => Promise | void }>() +const tempDirs = new Set() + +afterEach(async () => { + for (const plugin of activePlugins) await plugin.dispose?.() + activePlugins.clear() + await Promise.all( + [...tempDirs].map((directory) => + rm(directory, { recursive: true, force: true }), + ), + ) + tempDirs.clear() + globalThis.fetch = originalFetch + Date.now = originalNow + delete process.env.OPENCODE_ANTHROPIC_AUTH_DISABLE_PROFILE_HYDRATION + resetCache1hState() + resetClaudeCodeIdentityCachesForTest() +}) + +function quota(now: number, fableRemaining = 90) { + return { + checkedAt: now, + five_hour: { + usedPercent: 10, + remainingPercent: 90, + checkedAt: now, + resetsAt: new Date(now + 60 * 60_000).toISOString(), + }, + seven_day: { + usedPercent: 10, + remainingPercent: 90, + checkedAt: now, + resetsAt: new Date(now + 24 * 60 * 60_000).toISOString(), + }, + scoped: [ + { + id: 'claude-weekly-scoped-fable', + title: 'Fable only', + modelName: 'Fable', + usedPercent: 100 - fableRemaining, + remainingPercent: fableRemaining, + checkedAt: now, + resetsAt: new Date(now + 24 * 60 * 60_000).toISOString(), + }, + ], + } +} + +function vaultToken(site: Site) { + return `sk-ant-oat01-vault-${site}` +} + +function sidecarToken(site: Site) { + return `sk-ant-oat01-sidecar-canary-${site}` +} + +function expectOnlyVaultToken(records: OutboundRecord[], site: Site) { + expect(records.length, `${site}: no outbound requests`).toBeGreaterThan(0) + for (const record of records) { + expect(record.authorization, `${site}: ${record.url}`).toBe( + `Bearer ${vaultToken(site)}`, + ) + expect(record.headersHaveCanary, `${site}: canary in headers`).toBe(false) + expect(record.bodyHasCanary, `${site}: canary in body`).toBe(false) + } +} + +async function waitFor( + predicate: () => boolean, + message: string, + attempts = 50, +) { + for (let attempt = 0; attempt < attempts; attempt++) { + if (predicate()) return + await Bun.sleep(10) + } + throw new Error(message) +} + +async function createFixture( + site: Site, + options: { + now?: number + quotaEnabled?: boolean + quotaSnapshot?: ReturnType + prime?: boolean + cachekeep?: boolean + recovery?: boolean + profile?: boolean + captureIntervals?: boolean + } = {}, +) { + const now = options.now ?? Date.now() + const canary = `sidecar-canary-${site}` + const vault = vaultToken(site) + const accountId = `fallback-${site}` + const handle = `handle-${site}` + const intervals: IntervalRecord[] = [] + const records: OutboundRecord[] = [] + let refusalPending = options.recovery === true + + if (options.now !== undefined) { + let clock = now + Date.now = mock(() => clock) as unknown as typeof Date.now + Object.defineProperty(intervals, 'clock', { + value: (next: number) => { + clock = next + }, + }) + } + + const storage: AccountStorage = { + version: 1, + main: { + type: 'opencode', + provider: 'anthropic', + ...(options.profile + ? { + profile: { + tier: 'default_claude_max_5x', + orgType: 'claude_team', + checkedAt: now, + }, + } + : {}), + }, + fallbackOn: [401, 403, 429], + routing: { mode: options.recovery ? 'sticky-balanced' : 'fallback-first' }, + refresh: { + enabled: true, + intervalMinutes: 10, + refreshBeforeExpiryMinutes: 30, + }, + quota: options.quotaEnabled + ? { + enabled: true, + checkIntervalMinutes: 5, + minimumRemaining: { five_hour: 1, seven_day: 1 }, + failClosedOnUnknownQuota: true, + ...(options.recovery || options.prime + ? { + mainQuota: quota(now, options.recovery ? 0 : 90), + mainQuotaCheckedAt: now, + mainQuotaToken: tokenFingerprint('main-access'), + } + : {}), + } + : { enabled: false, failClosedOnUnknownQuota: false }, + claustrum: { accounts: { [accountId]: { enabled: true } } }, + ...(options.prime ? { prime: { enabled: true } } : {}), + ...(options.cachekeep || options.recovery + ? { + claudeCache: { enabled: true, mode: 'hybrid' }, + cacheKeep: { enabled: true, always: true, subagents: true }, + } + : {}), + accounts: [ + { + id: accountId, + type: 'oauth', + access: sidecarToken(site), + refresh: `refresh-${site}`, + expires: now + 8 * 60 * 60_000, + claustrumHandle: handle, + ...(options.quotaSnapshot ? { quota: options.quotaSnapshot } : {}), + }, + ], + } + + const directory = await mkdtemp(join(tmpdir(), `fallback-census-${site}-`)) + tempDirs.add(directory) + const accountFile = join(directory, 'anthropic-auth.json') + process.env.OPENCODE_ANTHROPIC_AUTH_FILE = accountFile + process.env.OPENCODE_ANTHROPIC_AUTH_SIDEBAR_STATE_FILE = join( + directory, + 'sidebar.json', + ) + process.env.OPENCODE_ANTHROPIC_AUTH_CACHEKEEP_REGISTRY_DIR = join( + directory, + 'cachekeep', + ) + process.env.OPENCODE_ANTHROPIC_AUTH_QUOTA_FEED_DIR = join(directory, 'quota') + if (!options.profile) { + process.env.OPENCODE_ANTHROPIC_AUTH_DISABLE_PROFILE_HYDRATION = '1' + } + await saveAccounts(storage, accountFile) + if (storage.main?.profile) { + await saveAccountState(storage, accountFile, { mainProfile: true }) + } + + const connector = async () => + ({ + call: async (_moduleId: string, method: string) => { + if (method !== 'credential.get') return { result: {} } + return { + result: { + payload: Array.from( + new TextEncoder().encode(JSON.stringify({ access_token: vault })), + ), + expires_at_ms: now + 12 * 60 * 60_000, + record_version: 103, + }, + } + }, + close() {}, + }) as never + + const refusalSse = [ + 'event: message_start\ndata: {"type":"message_start","message":{"id":"msg_filtered"}}\n\n', + 'event: message_delta\ndata: {"type":"message_delta","delta":{"stop_reason":"refusal"},"usage":{"output_tokens":0}}\n\n', + 'event: message_stop\ndata: {"type":"message_stop"}\n\n', + ].join('') + const successSse = [ + 'event: message_start\ndata: {"type":"message_start","message":{"id":"msg_ok","model":"claude-opus-4-8","usage":{}}}\n\n', + 'event: message_delta\ndata: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":1}}\n\n', + 'event: message_stop\ndata: {"type":"message_stop"}\n\n', + ].join('') + + globalThis.fetch = mock((input: unknown, init?: RequestInit) => { + const url = extractUrl(input as string | URL | Request) + const headers = new Headers(init?.headers) + const body = typeof init?.body === 'string' ? init.body : '' + records.push({ + url, + authorization: headers.get('authorization') ?? '', + body, + bodyHasCanary: body.includes(canary), + headersHaveCanary: [...headers.values()].some((value) => + value.includes(canary), + ), + }) + if (url.includes('/claude_cli/bootstrap')) { + return Promise.resolve( + Response.json({ oauth_account: { account_uuid: accountId } }), + ) + } + if (url.includes('/api/oauth/profile')) { + return Promise.resolve( + Response.json({ + organization: { + organization_type: 'claude_team', + rate_limit_tier: 'default_claude_max_5x', + }, + }), + ) + } + if (url.includes('/api/oauth/usage')) { + return Promise.resolve( + Response.json({ + five_hour: { + utilization: 10, + resets_at: new Date(now - 1_000).toISOString(), + }, + seven_day: { utilization: 10 }, + limits: [ + { + kind: 'weekly_scoped', + group: 'weekly', + percent: 10, + scope: { model: { display_name: 'Fable' } }, + }, + ], + }), + ) + } + if (url.includes('/v1/messages')) { + const parsed = body ? (JSON.parse(body) as { max_tokens?: number }) : {} + if (parsed.max_tokens === 0) { + return Promise.resolve( + Response.json({ usage: { input_tokens: 1, output_tokens: 0 } }), + ) + } + if (refusalPending) { + refusalPending = false + return Promise.resolve(new Response(refusalSse, { status: 200 })) + } + if (options.prime) { + return Promise.resolve( + Response.json({ usage: { input_tokens: 20, output_tokens: 1 } }), + ) + } + return Promise.resolve(new Response(successSse, { status: 200 })) + } + return Promise.resolve(new Response('unexpected', { status: 599 })) + }) as unknown as typeof fetch + + const setInterval = options.captureIntervals + ? (mock((callback: () => unknown, ms: number) => { + intervals.push({ callback, ms }) + return { unref() {} } as unknown as ReturnType< + typeof globalThis.setInterval + > + }) as unknown as typeof globalThis.setInterval) + : (mock( + () => + ({ unref() {} }) as unknown as ReturnType< + typeof globalThis.setInterval + >, + ) as unknown as typeof globalThis.setInterval) + const clearInterval = mock( + () => {}, + ) as unknown as typeof globalThis.clearInterval + const plugin = (await ( + AnthropicAuthPlugin as unknown as ( + context: unknown, + runtime: unknown, + ) => Promise + )( + { + client: { + auth: { set: mock(() => Promise.resolve()) }, + session: { promptAsync: mock(() => Promise.resolve()) }, + }, + }, + { claustrumConnector: connector, setInterval, clearInterval }, + )) as any + activePlugins.add(plugin) + const result = await plugin.auth.loader( + () => + Promise.resolve({ + type: 'oauth' as const, + access: 'main-access', + refresh: 'main-refresh', + expires: now + 8 * 60 * 60_000, + }), + { models: {} }, + ) + + return { accountId, intervals, plugin, records, result } +} + +describe('vault-served fallback outbound token census', () => { + test.serial('request and lane-start sends', async () => { + const fixture = await createFixture('request') + fixture.records.length = 0 + const body = JSON.stringify({ + model: 'claude-opus-4-8', + stream: true, + messages: [{ role: 'user', content: 'hello' }], + }) + await ( + await fixture.result.fetch(MESSAGES_URL, { method: 'POST', body }) + ).text() + await ( + await fixture.result.fetch(MESSAGES_URL, { + method: 'POST', + headers: { [LANE_START_REQUEST_HEADER]: '1' }, + body, + }) + ).text() + + expectOnlyVaultToken(fixture.records, 'request') + const bootstrap = fixture.records.filter((record) => + record.url.includes('/claude_cli/bootstrap'), + ) + expect(bootstrap.length).toBeGreaterThan(0) + expect( + fixture.records.filter((record) => record.url.includes('/v1/messages')), + ).toHaveLength(2) + }) + + test.serial('fallback-manager quota poll', async () => { + const fixture = await createFixture('quota', { quotaEnabled: true }) + await fixture.plugin.__fallbackRefreshReady + const usage = fixture.records.filter((record) => + record.url.includes('/api/oauth/usage'), + ) + + expectOnlyVaultToken(usage, 'quota') + }) + + test.serial('prime tick', async () => { + const now = Date.now() - 60_000 + const dueQuota = quota(now) + dueQuota.five_hour.resetsAt = new Date(now - 120_000).toISOString() + const fixture = await createFixture('prime', { + now, + quotaEnabled: true, + quotaSnapshot: dueQuota, + prime: true, + }) + fixture.records.length = 0 + await fixture.plugin.__primeManager.tick() + + const fallbackRecords = fixture.records.filter( + (record) => record.authorization !== 'Bearer main-access', + ) + expectOnlyVaultToken(fallbackRecords, 'prime') + expect( + fallbackRecords.some((record) => record.url.includes('/v1/messages')), + ).toBe(true) + }) + + test.serial('CacheKeep prewarm', async () => { + const now = 1_000 + const fixture = await createFixture('cachekeep', { + now, + cachekeep: true, + captureIntervals: true, + }) + fixture.records.length = 0 + const body = JSON.stringify({ + model: 'claude-opus-4-8', + stream: true, + messages: [{ role: 'user', content: 'hello' }], + }) + await ( + await fixture.result.fetch(MESSAGES_URL, { + method: 'POST', + headers: { 'x-session-affinity': 'cachekeep-census' }, + body, + }) + ).text() + resetClaudeCodeIdentityCachesForTest() + fixture.records.length = 0 + ;( + fixture.intervals as IntervalRecord[] & { clock: (now: number) => void } + ).clock(now + 55 * 60_000) + const cacheKeepTick = fixture.intervals.at(-1) + if (!cacheKeepTick) throw new Error('missing CacheKeep interval') + cacheKeepTick.callback() + await waitFor( + () => + fixture.records.some((record) => { + if (!record.url.includes('/v1/messages')) return false + return ( + (JSON.parse(record.body) as { max_tokens?: number }).max_tokens === + 0 + ) + }), + 'CacheKeep prewarm did not run', + ) + + expectOnlyVaultToken(fixture.records, 'cachekeep') + const bootstrap = fixture.records.filter((record) => + record.url.includes('/claude_cli/bootstrap'), + ) + expect(bootstrap.length).toBeGreaterThan(0) + }) + + test.serial('recovery source-model prewarm', async () => { + // Recovery and ordinary CacheKeep prewarms intentionally share prepareHeaders. + const now = Date.now() + const fixture = await createFixture('recovery', { + now, + quotaEnabled: true, + quotaSnapshot: quota(now, 98), + recovery: true, + }) + fixture.records.length = 0 + const request = { + method: 'POST', + headers: { 'x-session-affinity': 'recovery-census' }, + body: JSON.stringify({ + model: 'claude-fable-5', + max_tokens: 128_000, + stream: true, + system: [{ type: 'text', text: 'stable system' }], + messages: [{ role: 'user', content: 'hello' }], + }), + } + const refused = await fixture.result.fetch(MESSAGES_URL, request) + await expect(refused.text()).rejects.toThrow() + await (await fixture.result.fetch(MESSAGES_URL, request)).text() + await waitFor( + () => + fixture.records.some((record) => { + if (!record.url.includes('/v1/messages')) return false + return ( + (JSON.parse(record.body) as { max_tokens?: number }).max_tokens === + 0 + ) + }), + 'recovery source-model prewarm did not run', + ) + + expectOnlyVaultToken(fixture.records, 'recovery') + }) + + test.serial('profile hydration', async () => { + const fixture = await createFixture('profile', { profile: true }) + await waitFor( + () => + fixture.records.some((record) => + record.url.includes('/api/oauth/profile'), + ), + 'fallback profile hydration did not run', + ) + const profiles = fixture.records.filter((record) => + record.url.includes('/api/oauth/profile'), + ) + + expectOnlyVaultToken(profiles, 'profile') + }) +}) diff --git a/packages/opencode/src/tests/index.test.ts b/packages/opencode/src/tests/index.test.ts index c771061c..c818f195 100644 --- a/packages/opencode/src/tests/index.test.ts +++ b/packages/opencode/src/tests/index.test.ts @@ -2174,6 +2174,74 @@ describe('fallback Claustrum credential resolution', () => { } }) + test('profile hydration serves a resident vault credential', async () => { + const calls: CredentialCall[] = [] + const profileStarted = deferred() + let profileUsedVault = false + let profileUsedSidecar = false + const storage = fallbackWithClaustrum({ + claustrumHandle: 'handle-profile-hydration', + claustrum: { accounts: { 'fallback-1': { enabled: true } } }, + } as never) + await useTempAccountFile(storage) + const connector = connectorFor(calls, (method) => { + if (method === 'credential.get') + return credentialResponse( + 'vault-profile-access', + 53, + Date.now() + 2 * 60 * 60_000, + ) + return { result: {} } + }) + globalThis.fetch = mock((input: unknown, init?: RequestInit) => { + const url = extractUrl(input as string | URL | Request) + const authorization = + new Headers(init?.headers).get('authorization') ?? '' + if (url.includes('/api/oauth/profile')) { + if (authorization.includes('vault-profile')) { + profileUsedVault = true + profileStarted.resolve() + } + if (authorization.includes('stored-fallback')) { + profileUsedSidecar = true + profileStarted.resolve() + } + return Promise.resolve( + Response.json({ + organization: { + organization_type: 'claude_team', + rate_limit_tier: 'default_claude_max_5x', + }, + }), + ) + } + return Promise.resolve(new Response('{}', { status: 200 })) + }) as unknown as typeof fetch + + const plugin = await getPlugin(undefined, undefined, { + claustrumConnector: connector, + }) + await plugin.auth.loader( + () => + Promise.resolve({ + type: 'oauth' as const, + access: 'main-access', + refresh: 'main-refresh', + expires: Date.now() + 100_000, + }), + { models: {} }, + ) + await withDeadlockGuard( + profileStarted.promise, + 500, + 'fallback profile hydration did not run', + ) + + expect(profileUsedVault).toBe(true) + expect(profileUsedSidecar).toBe(false) + await plugin.dispose?.() + }) + test('disabled gate does not connect and uses the stored token', async () => { const calls: CredentialCall[] = [] let connectorCalls = 0 From bd6abcc8ddffde5a4df3bfe5414d450de2fdbd99 Mon Sep 17 00:00:00 2001 From: iceteaSA <171169159+iceteaSA@users.noreply.github.com> Date: Fri, 4 Sep 2026 18:08:04 +0200 Subject: [PATCH 4/4] fix(cachekeep): bind the served vault credential to its prewarm and clear it on every exit; count a cold-vault prime as a skip --- packages/core/src/cachekeep.ts | 212 ++++++++++-------- packages/core/src/prime.ts | 11 +- packages/core/src/tests/prime.test.ts | 41 ++++ packages/opencode/src/index.ts | 22 +- packages/opencode/src/sidebar-state.ts | 11 +- .../src/tests/command-dialogs.test.ts | 14 ++ .../tests/fallback-token-use-sites.test.ts | 20 +- packages/opencode/src/tests/index.test.ts | 195 +++++++++++++++- .../opencode/src/tests/sidebar-state.test.ts | 13 ++ packages/opencode/src/tui/command-dialogs.tsx | 2 + 10 files changed, 429 insertions(+), 112 deletions(-) diff --git a/packages/core/src/cachekeep.ts b/packages/core/src/cachekeep.ts index bb251f79..ebb68dd1 100644 --- a/packages/core/src/cachekeep.ts +++ b/packages/core/src/cachekeep.ts @@ -336,6 +336,10 @@ export type CacheKeepTarget = { isSubagent: boolean } +export type CacheKeepPrewarmAttempt = { + id: number +} + function cacheKeepRetryDelayMs(targetId: string, failureCount: number) { const exponential = Math.min( CACHE_KEEP_RETRY_MAX_MS, @@ -367,6 +371,7 @@ export class CacheKeepManager { private readonly targets = new Map() private timer: ReturnType | null = null private tickPromise: Promise | null = null + private nextPrewarmAttemptId = 0 constructor( private readonly options: { @@ -379,6 +384,7 @@ export class CacheKeepManager { prepareHeaders?: ( headers: Headers, target: CacheKeepTarget, + attempt: CacheKeepPrewarmAttempt, ) => Promise | Headers | undefined onTrackedSessionsChanged?: ( sessions: readonly CacheKeepTrackedSession[], @@ -393,6 +399,11 @@ export class CacheKeepManager { status: number data: unknown receivedAt: number + attempt: CacheKeepPrewarmAttempt + }) => void | Promise + onComplete?: (input: { + target: CacheKeepTarget + attempt: CacheKeepPrewarmAttempt }) => void | Promise }, ) {} @@ -639,110 +650,119 @@ export class CacheKeepManager { private async sendPrewarm( target: CacheKeepTarget, ): Promise { - let bodyText = target.bodyText - if (this.options.prepareBody) { + const attempt = { id: ++this.nextPrewarmAttemptId } + try { + let bodyText = target.bodyText + if (this.options.prepareBody) { + try { + bodyText = await this.options.prepareBody(bodyText, target) + } catch (error) { + logger.warn('cachekeep', 'prepare body failed', { + session: target.id, + error: error instanceof Error ? error.message : String(error), + }) + } + } + const preparedTarget = { ...target, bodyText } + const prewarm = await buildCacheKeepPrewarmBody(bodyText) + if (!prewarm.ok) return prewarm + + const fetchImpl = this.options.fetchImpl ?? fetch + const prewarmTarget = { ...preparedTarget, bodyText: prewarm.bodyText } + const headers = this.options.prepareHeaders + ? await this.options.prepareHeaders( + new Headers(target.headers), + prewarmTarget, + attempt, + ) + : new Headers(target.headers) + if (!headers) { + return { + ok: false, + reason: 'OAuth cache prewarm credential is unavailable', + transient: true, + } + } + headers.delete('content-length') + headers.delete('transfer-encoding') + let response: Response try { - bodyText = await this.options.prepareBody(bodyText, target) + response = await fetchImpl(target.url, { + method: 'POST', + headers, + body: prewarm.bodyText, + signal: AbortSignal.timeout( + this.options.prewarmTimeoutMs ?? CACHE_KEEP_PREWARM_TIMEOUT_MS, + ), + }) + } catch (error) { + return { + ok: false, + reason: error instanceof Error ? error.message : String(error), + transient: true, + } + } + const receivedAt = this.options.now?.() ?? Date.now() + const raw = await response.text().catch(() => '') + let data: unknown = null + try { + data = raw ? JSON.parse(raw) : null + } catch {} + try { + await this.options.onResponse?.({ + target, + bodyText: prewarm.bodyText, + status: response.status, + data, + receivedAt, + attempt, + }) + } catch {} + try { + const dumpHandle = await dumpDirectRequest({ + affinity: target.id, + route: 'cachekeep', + status: response.status, + bodyText: prewarm.bodyText, + url: target.url, + method: 'POST', + headers, + tag: 'cachekeep', + }) + await dumpResponseArtifact(dumpHandle, { + status: response.status, + message: data, + }) } catch (error) { - logger.warn('cachekeep', 'prepare body failed', { + logger.debug('cachekeep', 'dump failed', { session: target.id, error: error instanceof Error ? error.message : String(error), }) } - } - const preparedTarget = { ...target, bodyText } - const prewarm = await buildCacheKeepPrewarmBody(bodyText) - if (!prewarm.ok) return prewarm - - const fetchImpl = this.options.fetchImpl ?? fetch - const prewarmTarget = { ...preparedTarget, bodyText: prewarm.bodyText } - const headers = this.options.prepareHeaders - ? await this.options.prepareHeaders( - new Headers(target.headers), - prewarmTarget, - ) - : new Headers(target.headers) - if (!headers) { - return { - ok: false, - reason: 'OAuth cache prewarm credential is unavailable', - transient: true, - } - } - headers.delete('content-length') - headers.delete('transfer-encoding') - let response: Response - try { - response = await fetchImpl(target.url, { - method: 'POST', - headers, - body: prewarm.bodyText, - signal: AbortSignal.timeout( - this.options.prewarmTimeoutMs ?? CACHE_KEEP_PREWARM_TIMEOUT_MS, - ), - }) - } catch (error) { - return { - ok: false, - reason: error instanceof Error ? error.message : String(error), - transient: true, - } - } - const receivedAt = this.options.now?.() ?? Date.now() - const raw = await response.text().catch(() => '') - let data: unknown = null - try { - data = raw ? JSON.parse(raw) : null - } catch {} - try { - await this.options.onResponse?.({ - target, - bodyText: prewarm.bodyText, - status: response.status, - data, - receivedAt, - }) - } catch {} - try { - const dumpHandle = await dumpDirectRequest({ - affinity: target.id, - route: 'cachekeep', - status: response.status, - bodyText: prewarm.bodyText, - url: target.url, - method: 'POST', - headers, - tag: 'cachekeep', - }) - await dumpResponseArtifact(dumpHandle, { - status: response.status, - message: data, - }) - } catch (error) { - logger.debug('cachekeep', 'dump failed', { - session: target.id, - error: error instanceof Error ? error.message : String(error), - }) - } - if (!response.ok) { - return { - ok: false, - reason: raw || `HTTP ${response.status}`, - status: response.status, + if (!response.ok) { + return { + ok: false, + reason: raw || `HTTP ${response.status}`, + status: response.status, + } } + const objectData = + data && typeof data === 'object' && !Array.isArray(data) + ? (data as Record) + : null + const usage = objectData?.usage as + | { + input_tokens?: number + cache_creation_input_tokens?: number + cache_read_input_tokens?: number + } + | undefined + return { ok: true, ...(usage && { usage }) } + } finally { + try { + await this.options.onComplete?.({ target, attempt }) + } catch {} } - const objectData = - data && typeof data === 'object' && !Array.isArray(data) - ? (data as Record) - : null - const usage = objectData?.usage as - | { - input_tokens?: number - cache_creation_input_tokens?: number - cache_read_input_tokens?: number - } - | undefined - return { ok: true, ...(usage && { usage }) } } private async prewarm(target: CacheKeepTarget, now: number) { diff --git a/packages/core/src/prime.ts b/packages/core/src/prime.ts index 0cbeb0a9..6250536a 100644 --- a/packages/core/src/prime.ts +++ b/packages/core/src/prime.ts @@ -53,7 +53,7 @@ export type PrimeAccountStatus = { label: string nextDueAt?: number lastPrimedAt?: number | null - lastResult?: 'ok' | 'error' + lastResult?: 'ok' | 'error' | 'skipped' usage?: import('./accounts.ts').PrimeUsageCounters estimatedCostUsd?: number } @@ -133,7 +133,7 @@ export function buildPrimeAccountStatuses( now?: number transient?: ReadonlyMap< string, - { lastPrimedAt?: number; lastResult?: 'ok' | 'error' } + { lastPrimedAt?: number; lastResult?: 'ok' | 'error' | 'skipped' } > }, ): PrimeAccountStatus[] { @@ -222,7 +222,7 @@ function formatDue(nextDueAt: number | null | undefined): string { function formatPrimed( lastPrimedAt: number | null | undefined, - lastResult: 'ok' | 'error' | undefined, + lastResult: 'ok' | 'error' | 'skipped' | undefined, ): string { if (typeof lastPrimedAt !== 'number') return '' const time = new Date(lastPrimedAt).toLocaleTimeString([], { @@ -230,6 +230,7 @@ function formatPrimed( minute: '2-digit', }) if (lastResult === 'error') return `primed ${time} err` + if (lastResult === 'skipped') return `primed ${time} skipped` return `primed ${time} \u2713` } @@ -562,7 +563,7 @@ export class PrimeManager { // (M4): skip is not the same as a successful prime. private transient = new Map< string, - { lastPrimedAt: number; lastResult: 'ok' | 'error' } + { lastPrimedAt: number; lastResult: 'ok' | 'error' | 'skipped' } >() // Latest cumulative counters returned by recordSuccess. Overlays the // persisted counters in stats() before the next load persists them. @@ -966,7 +967,7 @@ export class PrimeManager { } this.transient.set(evaluation.id, { lastPrimedAt: now, - lastResult: 'error', + lastResult: result.reason === 'vault-cold' ? 'skipped' : 'error', }) return } diff --git a/packages/core/src/tests/prime.test.ts b/packages/core/src/tests/prime.test.ts index b6d9822a..9f7f69d4 100644 --- a/packages/core/src/tests/prime.test.ts +++ b/packages/core/src/tests/prime.test.ts @@ -1539,6 +1539,47 @@ describe('PrimeManager — send failure', () => { expect(h.sendCalls).toEqual([]) await h.cleanup() }) + + test('cold vault prime is recorded as a skipped attempt, not an error', async () => { + const fixture = makePrimeFixture({ + mainQuota: { + five_hour: { + usedPercent: 0, + remainingPercent: 100, + resetsAt: new Date(500).toISOString(), + checkedAt: 1, + }, + }, + }) + const now = 500 + 120_000 + const h = await makeHarness({ + storage: fixture.storage, + markerDir: markerRoot, + now, + quotaFresh: { + five_hour: { + usedPercent: 0, + remainingPercent: 100, + resetsAt: new Date(now - 1000).toISOString(), + checkedAt: 1, + }, + }, + send: () => ({ + ok: false, + reason: 'vault-cold', + error: 'vault credential is unavailable', + }), + }) + + await h.manager.tick() + + const stats = h.manager.stats() + expect(stats[0]?.lastResult).toBe('skipped') + const summary = buildPrimeStatusSummary({ enabled: true, accounts: stats }) + expect(summary).toContain('skipped') + expect(summary).not.toContain('err') + await h.cleanup() + }) }) describe('PrimeManager — recordSuccess', () => { diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index caf204c7..7c107f89 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -2387,7 +2387,7 @@ const anthropicAuthPlugin = async ( CacheKeepManager['trackedSessions'] > = [] const cacheKeepServedClaustrumCredentials = new Map< - string, + number, ClaustrumAccessResolution['served'] >() const cacheKeepManager = new CacheKeepManager({ @@ -2431,9 +2431,15 @@ const anthropicAuthPlugin = async ( return bodyText } }, - onResponse: async ({ target, bodyText, status, data, receivedAt }) => { - const served = cacheKeepServedClaustrumCredentials.get(target.id) - cacheKeepServedClaustrumCredentials.delete(target.id) + onResponse: async ({ + target, + bodyText, + status, + data, + receivedAt, + attempt, + }) => { + const served = cacheKeepServedClaustrumCredentials.get(attempt.id) if (status === 401 && served) { await reportCapturedClaustrumAuthFailure(served, 'direct', { preserveServedVersion: true, @@ -2481,7 +2487,10 @@ const anthropicAuthPlugin = async ( cacheKeepDiagnosticsRequests.delete(target.id) } }, - prepareHeaders: async (headers, target) => { + onComplete: ({ attempt }) => { + cacheKeepServedClaustrumCredentials.delete(attempt.id) + }, + prepareHeaders: async (headers, target, attempt) => { let accessToken: string | undefined let servedClaustrumCredential: ClaustrumAccessResolution['served'] const accountId = target.oauthAccountId @@ -2570,7 +2579,7 @@ const anthropicAuthPlugin = async ( } if (servedClaustrumCredential) { cacheKeepServedClaustrumCredentials.set( - target.id, + attempt.id, servedClaustrumCredential, ) } @@ -7739,6 +7748,7 @@ const anthropicAuthPlugin = async ( }, __primeManager: primeManager, __quotaManager: quotaManager, + __cacheKeepManager: cacheKeepManager, __persistFallbackQuotaErrorForTest: persistFallbackQuotaError, __fallbackRefreshReady: fallbackRefreshReady, __claustrumCredentialCache: claustrumCredentialCache, diff --git a/packages/opencode/src/sidebar-state.ts b/packages/opencode/src/sidebar-state.ts index e8b0ae1d..38b92320 100644 --- a/packages/opencode/src/sidebar-state.ts +++ b/packages/opencode/src/sidebar-state.ts @@ -59,7 +59,7 @@ export interface PrimeSidebarAccountState { label: string nextDueAt?: number | null lastPrimedAt?: number | null - lastResult?: 'ok' | 'error' + lastResult?: 'ok' | 'error' | 'skipped' usage?: PrimeUsageCounters estimatedCostUsd?: number } @@ -180,7 +180,11 @@ function normalizePrimeAccount( } else if (isFiniteNumber(value.lastPrimedAt)) { account.lastPrimedAt = value.lastPrimedAt } - if (value.lastResult === 'ok' || value.lastResult === 'error') { + if ( + value.lastResult === 'ok' || + value.lastResult === 'error' || + value.lastResult === 'skipped' + ) { account.lastResult = value.lastResult } const usage = normalizePrimeUsage(value.usage) @@ -844,6 +848,9 @@ export function formatPrimeAccountValue(account: PrimeSidebarAccountState): { if (account.lastResult === 'error') { return { text: 'err', hasError: true } } + if (account.lastResult === 'skipped') { + return { text: 'skip', hasError: false } + } if (account.nextDueAt && account.nextDueAt > Date.now()) { return { text: formatPrimeTime(account.nextDueAt), hasError: false } } diff --git a/packages/opencode/src/tests/command-dialogs.test.ts b/packages/opencode/src/tests/command-dialogs.test.ts index 08e17a30..4482608b 100644 --- a/packages/opencode/src/tests/command-dialogs.test.ts +++ b/packages/opencode/src/tests/command-dialogs.test.ts @@ -133,6 +133,20 @@ describe('buildPrimeStatusRows', () => { expect(rows[0]).toContain('primed') expect(rows[0]).toContain('err') }) + + test('cold vault skip renders as a skip, not a successful prime', () => { + const rows = buildPrimeStatusRows([ + { + id: 'work-alt', + label: 'work-alt', + nextDueAt: undefined, + lastPrimedAt: Date.now() - 60_000, + lastResult: 'skipped', + } as PrimeAccountStatus, + ]) + expect(rows[0]).toContain('skip') + expect(rows[0]).not.toContain('\u2713') + }) }) describe('openCommandDialog — claude-prime modal interaction (M6)', () => { diff --git a/packages/opencode/src/tests/fallback-token-use-sites.test.ts b/packages/opencode/src/tests/fallback-token-use-sites.test.ts index f163508a..14d11503 100644 --- a/packages/opencode/src/tests/fallback-token-use-sites.test.ts +++ b/packages/opencode/src/tests/fallback-token-use-sites.test.ts @@ -4,12 +4,14 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { type AccountStorage, + CACHE_KEEP_TICK_MS, resetCache1hState, resetClaudeCodeIdentityCachesForTest, saveAccountState, saveAccounts, tokenFingerprint, } from '@cortexkit/anthropic-auth-core' + import { AnthropicAuthPlugin } from '../index' import { LANE_START_REQUEST_HEADER } from '../lane-start' import { extractUrl, MESSAGES_URL } from './test-fetch' @@ -30,6 +32,14 @@ const originalFetch = globalThis.fetch const originalNow = Date.now const activePlugins = new Set<{ dispose?: () => Promise | void }>() const tempDirs = new Set() +const fixtureEnvKeys = [ + 'OPENCODE_ANTHROPIC_AUTH_FILE', + 'OPENCODE_ANTHROPIC_AUTH_SIDEBAR_STATE_FILE', + 'OPENCODE_ANTHROPIC_AUTH_CACHEKEEP_REGISTRY_DIR', + 'OPENCODE_ANTHROPIC_AUTH_QUOTA_FEED_DIR', + 'OPENCODE_ANTHROPIC_AUTH_DISABLE_PROFILE_HYDRATION', +] as const +const fixtureEnv = new Map(fixtureEnvKeys.map((key) => [key, process.env[key]])) afterEach(async () => { for (const plugin of activePlugins) await plugin.dispose?.() @@ -42,7 +52,11 @@ afterEach(async () => { tempDirs.clear() globalThis.fetch = originalFetch Date.now = originalNow - delete process.env.OPENCODE_ANTHROPIC_AUTH_DISABLE_PROFILE_HYDRATION + for (const key of fixtureEnvKeys) { + const value = fixtureEnv.get(key) + if (value === undefined) delete process.env[key] + else process.env[key] = value + } resetCache1hState() resetClaudeCodeIdentityCachesForTest() }) @@ -446,7 +460,9 @@ describe('vault-served fallback outbound token census', () => { ;( fixture.intervals as IntervalRecord[] & { clock: (now: number) => void } ).clock(now + 55 * 60_000) - const cacheKeepTick = fixture.intervals.at(-1) + const cacheKeepTick = fixture.intervals.find( + (interval) => interval.ms === CACHE_KEEP_TICK_MS, + ) if (!cacheKeepTick) throw new Error('missing CacheKeep interval') cacheKeepTick.callback() await waitFor( diff --git a/packages/opencode/src/tests/index.test.ts b/packages/opencode/src/tests/index.test.ts index c818f195..5adac095 100644 --- a/packages/opencode/src/tests/index.test.ts +++ b/packages/opencode/src/tests/index.test.ts @@ -2145,7 +2145,7 @@ describe('fallback Claustrum credential resolution', () => { }) ).text() now += 55 * 60_000 - const cacheKeepTick = intervals.at(-1) + const cacheKeepTick = intervals.find((interval) => interval.ms === 60_000) if (!cacheKeepTick) throw new Error('missing cachekeep interval') cacheKeepTick.callback() await Bun.sleep(20) @@ -2174,6 +2174,199 @@ describe('fallback Claustrum credential resolution', () => { } }) + test('CacheKeep clears a failed vault attempt before a sidecar 401', async () => { + const calls: CredentialCall[] = [] + const storage = fallbackWithClaustrum({ + claustrumHandle: 'handle-cachekeep-abandon', + claustrum: { accounts: { 'fallback-1': { enabled: true } } }, + } as never) + storage.claudeCache = { enabled: true, mode: 'hybrid' } + storage.cacheKeep = { enabled: true, always: true, subagents: true } + storage.quota = { enabled: false, failClosedOnUnknownQuota: false } + await useTempAccountFile(storage) + const connector = connectorFor(calls, (method) => { + if (method === 'credential.get') { + return credentialResponse('vault-cachekeep-abandon', 47) + } + return { result: {} } + }) + let prewarms = 0 + globalThis.fetch = mock((input: unknown, init?: RequestInit) => { + const url = extractUrl(input as string | URL | Request) + if (url.includes('/claude_cli/bootstrap')) { + return Promise.resolve( + Response.json({ oauth_account: { account_uuid: 'fallback-1' } }), + ) + } + if (url.includes('/v1/messages')) { + prewarms += 1 + if (prewarms === 1) return Promise.reject(new Error('network reset')) + return Promise.resolve(new Response('unauthorized', { status: 401 })) + } + return Promise.resolve(new Response('{}', { status: 200 })) + }) as unknown as typeof fetch + + const plugin = await getPlugin(undefined, undefined, { + claustrumConnector: connector, + }) + await plugin.auth.loader( + () => + Promise.resolve({ + type: 'oauth' as const, + access: 'main-access', + refresh: 'main-refresh', + expires: Date.now() + 100_000, + }), + { models: {} }, + ) + const cacheKeep = plugin.__cacheKeepManager + if (!cacheKeep) throw new Error('missing CacheKeep manager') + const request = { + sessionId: 'ses-cachekeep-abandon', + url: MESSAGES_URL, + headers: new Headers(), + bodyText: JSON.stringify({ + model: 'claude-opus-4-8', + system: [ + { + type: 'text', + text: 'stable', + cache_control: { type: 'ephemeral' }, + }, + ], + messages: [{ role: 'user', content: 'hello' }], + }), + } + await cacheKeep.prewarmNow({ ...request, oauthAccountId: 'fallback-1' }) + await cacheKeep.prewarmNow(request) + + expect( + calls.filter((call) => call.method === 'credential.report_auth_failure'), + ).toHaveLength(0) + await plugin.dispose?.() + }) + + test('overlapping CacheKeep vault 401s report the credential each attempt served', async () => { + const calls: CredentialCall[] = [] + const storage = fallbackWithClaustrum({ + claustrumHandle: 'handle-cachekeep-overlap', + claustrum: { accounts: { 'fallback-1': { enabled: true } } }, + } as never) + storage.claudeCache = { enabled: true, mode: 'hybrid' } + storage.cacheKeep = { enabled: true, always: true, subagents: true } + storage.quota = { enabled: false, failClosedOnUnknownQuota: false } + await useTempAccountFile(storage) + let credentialGets = 0 + const connector = connectorFor(calls, (method) => { + if (method === 'credential.get') { + credentialGets += 1 + return credentialResponse( + `vault-cachekeep-overlap-${credentialGets}`, + 46 + credentialGets, + ) + } + return { result: {} } + }) + let startFirstPrewarm!: () => void + let startSecondPrewarm!: () => void + let releaseFirstPrewarm!: () => void + let releaseSecondPrewarm!: () => void + const firstPrewarmStarted = new Promise((resolve) => { + startFirstPrewarm = resolve + }) + const secondPrewarmStarted = new Promise((resolve) => { + startSecondPrewarm = resolve + }) + const firstPrewarmResponse = new Promise((resolve) => { + releaseFirstPrewarm = () => + resolve(new Response('unauthorized', { status: 401 })) + }) + const secondPrewarmResponse = new Promise((resolve) => { + releaseSecondPrewarm = () => + resolve(new Response('unauthorized', { status: 401 })) + }) + let prewarms = 0 + globalThis.fetch = mock((input: unknown) => { + const url = extractUrl(input as string | URL | Request) + if (url.includes('/claude_cli/bootstrap')) { + return Promise.resolve( + Response.json({ oauth_account: { account_uuid: 'fallback-1' } }), + ) + } + if (url.includes('/v1/messages')) { + prewarms += 1 + if (prewarms === 1) { + startFirstPrewarm() + return firstPrewarmResponse + } + startSecondPrewarm() + return secondPrewarmResponse + } + return Promise.resolve(new Response('{}', { status: 200 })) + }) as unknown as typeof fetch + + const plugin = await getPlugin(undefined, undefined, { + claustrumConnector: connector, + }) + await plugin.auth.loader( + () => + Promise.resolve({ + type: 'oauth' as const, + access: 'main-access', + refresh: 'main-refresh', + expires: Date.now() + 100_000, + }), + { models: {} }, + ) + const cacheKeep = plugin.__cacheKeepManager + if (!cacheKeep) throw new Error('missing CacheKeep manager') + const request = { + sessionId: 'ses-cachekeep-overlap', + url: MESSAGES_URL, + headers: new Headers(), + bodyText: JSON.stringify({ + model: 'claude-opus-4-8', + system: [ + { + type: 'text', + text: 'stable', + cache_control: { type: 'ephemeral' }, + }, + ], + messages: [{ role: 'user', content: 'hello' }], + }), + oauthAccountId: 'fallback-1', + } + const first = cacheKeep.prewarmNow(request) + await withDeadlockGuard( + firstPrewarmStarted, + 500, + `first overlapping CacheKeep prewarm did not start (${JSON.stringify({ credentialGets, prewarms })})`, + ) + plugin.__claustrumCredentialCache.seedForTest('handle-cachekeep-overlap', { + payload: JSON.stringify({ access_token: 'vault-cachekeep-overlap-2' }), + expiresAtMs: Date.now() + 5 * 60 * 60_000, + recordVersion: 48, + }) + const second = cacheKeep.prewarmNow(request) + await withDeadlockGuard( + secondPrewarmStarted, + 500, + `second overlapping CacheKeep prewarm did not start (${JSON.stringify({ credentialGets, prewarms })})`, + ) + releaseFirstPrewarm() + await first + releaseSecondPrewarm() + await second + + expect( + calls + .filter((call) => call.method === 'credential.report_auth_failure') + .map((call) => call.params.record_version), + ).toEqual([47, 48]) + await plugin.dispose?.() + }) + test('profile hydration serves a resident vault credential', async () => { const calls: CredentialCall[] = [] const profileStarted = deferred() diff --git a/packages/opencode/src/tests/sidebar-state.test.ts b/packages/opencode/src/tests/sidebar-state.test.ts index 075139f4..709fb994 100644 --- a/packages/opencode/src/tests/sidebar-state.test.ts +++ b/packages/opencode/src/tests/sidebar-state.test.ts @@ -107,6 +107,19 @@ describe('prime display formatters', () => { lastResult: 'error', }), ).toEqual({ text: 'err', hasError: true }) + const skipped = normalizeSidebarState({ + prime: { + enabled: true, + accounts: [ + { id: 'cold-vault', label: 'cold-vault', lastResult: 'skipped' }, + ], + }, + }).prime?.accounts[0] + expect(skipped).toBeDefined() + expect(formatPrimeAccountValue(skipped!)).toEqual({ + text: 'skip', + hasError: false, + }) expect(formatPrimeAccountValue({ id: 'idle', label: 'idle' })).toEqual({ text: '\u2014', hasError: false, diff --git a/packages/opencode/src/tui/command-dialogs.tsx b/packages/opencode/src/tui/command-dialogs.tsx index 791702d8..11f52811 100644 --- a/packages/opencode/src/tui/command-dialogs.tsx +++ b/packages/opencode/src/tui/command-dialogs.tsx @@ -99,6 +99,8 @@ export function buildPrimeStatusRows(accounts: PrimeAccountStatus[]): string[] { const time = formatPrimeTime(account.lastPrimedAt) if (account.lastResult === 'error') { rows.push(`${account.label} \u00b7 primed ${time} err`) + } else if (account.lastResult === 'skipped') { + rows.push(`${account.label} \u00b7 primed ${time} skipped`) } else { rows.push(`${account.label} \u00b7 primed ${time} \u2713`) }