Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ This repo is a CortexKit-maintained Anthropic auth monorepo for OpenCode and Pi.

## Unreleased

- Capture Anthropic cache diagnostics in versioned `MC-CACHE-DIAG ` records, preserve provider response IDs across requests and cachekeep prewarms, and write response/request dump artifacts without response content. Document the beta states and known fingerprint, organization, workspace, and beta-set limitations.

## 1.19.1

### Patch Changes
Expand Down
84 changes: 73 additions & 11 deletions packages/core/src/cachekeep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { AccountStorage } from './accounts.ts'
import type { CacheKeepTrackedSession } from './cachekeep-registry.ts'
import { signRequestBody } from './cch.ts'
import { orderClaudeCodeBody } from './claude-code.ts'
import { dumpDirectRequest, dumpResponseArtifact } from './dump.ts'
import { logger } from './logger.ts'

export const CLAUDE_CACHE_KEEP_COMMAND_NAME = 'claude-cachekeep'
Expand Down Expand Up @@ -328,6 +329,7 @@ export type CacheKeepTarget = {
cacheExpiresAt: number
dayKey: string
oauthAccountId?: string
isSubagent: boolean
}

export type CacheKeepPrewarmResult =
Expand Down Expand Up @@ -360,6 +362,17 @@ export class CacheKeepManager {
onTrackedSessionsChanged?: (
sessions: readonly CacheKeepTrackedSession[],
) => Promise<void> | void
prepareBody?: (
bodyText: string,
target: CacheKeepTarget,
) => string | Promise<string>
onResponse?: (input: {
target: CacheKeepTarget
bodyText: string
status: number
data: unknown
receivedAt: number
}) => void | Promise<void>
},
) {}

Expand Down Expand Up @@ -482,6 +495,7 @@ export class CacheKeepManager {
storage: AccountStorage | null
cacheMode: string
oauthAccountId?: string
isSubagent?: boolean
}) {
if (!input.sessionId)
return { tracked: false, reason: 'missing session id' }
Expand Down Expand Up @@ -517,6 +531,7 @@ export class CacheKeepManager {
cacheExpiresAt: now + CACHE_KEEP_TTL_MS,
dayKey: today,
oauthAccountId: input.oauthAccountId,
isSubagent: input.isSubagent ?? false,
})
this.pruneTargets(now, today)
this.publishTrackedSessions()
Expand All @@ -530,6 +545,7 @@ export class CacheKeepManager {
headers: Headers
bodyText: string
oauthAccountId?: string
isSubagent?: boolean
}): Promise<CacheKeepPrewarmResult> {
const headers: Record<string, string> = {}
input.headers.forEach((value, key) => {
Expand All @@ -543,6 +559,7 @@ export class CacheKeepManager {
cacheExpiresAt: this.options.now?.() ?? Date.now(),
dayKey: '',
oauthAccountId: input.oauthAccountId,
isSubagent: input.isSubagent ?? false,
}
return this.sendPrewarm(target)
}
Expand Down Expand Up @@ -584,11 +601,23 @@ export class CacheKeepManager {
private async sendPrewarm(
target: CacheKeepTarget,
): Promise<CacheKeepPrewarmResult> {
const prewarm = await buildCacheKeepPrewarmBody(target.bodyText)
let bodyText = target.bodyText
if (this.options.prepareBody) {
try {
bodyText = await this.options.prepareBody(bodyText, target)
Comment thread
iceteaSA marked this conversation as resolved.
} 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 = { ...target, bodyText: prewarm.bodyText }
const prewarmTarget = { ...preparedTarget, bodyText: prewarm.bodyText }
const headers = this.options.prepareHeaders
? await this.options.prepareHeaders(
new Headers(target.headers),
Expand All @@ -605,21 +634,54 @@ export class CacheKeepManager {
this.options.prewarmTimeoutMs ?? CACHE_KEEP_PREWARM_TIMEOUT_MS,
),
})
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: await response
.text()
.catch(() => '')
.then((body) => body || `HTTP ${response.status}`),
reason: raw || `HTTP ${response.status}`,
status: response.status,
}
}
const data = (await response.json().catch(() => null)) as Record<
string,
unknown
> | null
const usage = data?.usage as
const objectData =
data && typeof data === 'object' && !Array.isArray(data)
? (data as Record<string, unknown>)
: null
const usage = objectData?.usage as
| {
input_tokens?: number
cache_creation_input_tokens?: number
Expand Down
9 changes: 7 additions & 2 deletions packages/core/src/claude-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@ export type ClaudeCodeIdentity = {
const IDENTITY_CACHE_LIMIT = 1_000
const identityCache = new Map<string, ClaudeCodeIdentity>()

function setBounded<K, V>(map: Map<K, V>, key: K, value: V) {
if (!map.has(key) && map.size >= IDENTITY_CACHE_LIMIT) {
export function setBounded<K, V>(
map: Map<K, V>,
key: K,
value: V,
limit = IDENTITY_CACHE_LIMIT,
) {
if (!map.has(key) && map.size >= limit) {
const oldest = map.keys().next().value
if (oldest !== undefined) map.delete(oldest)
}
Expand Down
73 changes: 65 additions & 8 deletions packages/core/src/dump.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ const DEFAULT_DUMP_MAX_BYTES = 512 * 1024 * 1024
const DUMP_SWEEP_INTERVAL_MS = 5 * 60 * 1000
const DUMP_SWEEP_NEWNESS_FLOOR_MS = 60 * 1000
const DUMP_PARTIAL_STALE_MS = 10 * 60 * 1000
const DUMP_ARTIFACT_SUFFIX_PATTERN = /\.(body|meta|relay|request)\.json$/
const DUMP_ARTIFACT_SUFFIX_PATTERN =
/\.(body|meta|relay|request|response)\.json$/
// Earlier builds emitted five-digit counters; current counters grow without truncation.
const DUMP_ARTIFACT_ID_PATTERN =
/^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z-\d{5,}-(.+)$/
Expand All @@ -54,6 +55,13 @@ export type DumpCommandAction =
| { type: 'disable' }
| { type: 'usage' }

export type DumpTag = 'cachekeep'

export type DumpHandle = {
responsePath: string
tag?: DumpTag
}

export function isDumpEnabled() {
return dumpEnabled
}
Expand Down Expand Up @@ -311,6 +319,10 @@ function dumpRequestSegment(input: {
return `direct${route}`
}

function dumpTagSegment(tag: DumpTag | undefined) {
return tag ? `-prewarm-${tag}` : ''
}

function directDumpPreviousKey(input: {
affinity?: string | null
route?: string
Expand Down Expand Up @@ -467,26 +479,29 @@ async function dumpRequest(input: {
previousBodyText?: string
payload?: unknown
relayBytes?: number
tag?: DumpTag
request?: {
url?: string
method?: string
headers?: DumpHeaders
}
}) {
if (!dumpEnabled) return
if (!dumpEnabled) return null
nextDumpId += 1
const affinity = input.affinity?.trim() || 'session-unknown'
const id = `${new Date().toISOString().replace(/[:.]/g, '-')}-${String(nextDumpId).padStart(6, '0')}-${dumpFileSessionSegment(affinity)}-${dumpRequestSegment(input)}`
const id = `${new Date().toISOString().replace(/[:.]/g, '-')}-${String(nextDumpId).padStart(6, '0')}-${dumpFileSessionSegment(affinity)}${dumpTagSegment(input.tag)}-${dumpRequestSegment(input)}`
const dumpDir = getDumpDirectory()
const prefix = join(dumpDir, id)
const files: {
body: string
metadata: string
response: string
relay?: string
request?: string
} = {
body: `${prefix}.body.json`,
metadata: `${prefix}.meta.json`,
response: `${prefix}.response.json`,
}
if (input.payload !== undefined) files.relay = `${prefix}.relay.json`
if (input.request !== undefined) files.request = `${prefix}.request.json`
Expand All @@ -503,6 +518,7 @@ async function dumpRequest(input: {
route: input.route,
status: input.status,
error: input.error,
tag: input.tag,
bodyBytes: input.bodyText.length,
relayBytes: input.relayBytes,
bodyHash: hashText(input.bodyText),
Expand Down Expand Up @@ -551,6 +567,11 @@ async function dumpRequest(input: {
relayLog(
`dump failed: ${error instanceof Error ? error.message : String(error)}`,
)
return null
}
return {
responsePath: files.response,
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
...(input.tag ? { tag: input.tag } : {}),
}
}

Expand All @@ -563,11 +584,12 @@ export async function dumpDirectRequest(input: {
url?: string
method?: string
headers?: DumpHeaders
}) {
if (!dumpEnabled) return
tag?: DumpTag
}): Promise<DumpHandle | null> {
if (!dumpEnabled) return null
const previousKey = directDumpPreviousKey(input)
const previousBodyText = directDumpPreviousBodies.get(previousKey)
await dumpRequest({
const handle = await dumpRequest({
affinity: input.affinity,
transport: 'direct',
route: input.route,
Expand All @@ -580,8 +602,10 @@ export async function dumpDirectRequest(input: {
method: input.method,
headers: input.headers,
},
tag: input.tag,
})
rememberDirectDumpBody(previousKey, input.bodyText)
return handle
}

export async function dumpRelayRequest(input: {
Expand All @@ -594,8 +618,9 @@ export async function dumpRelayRequest(input: {
previousBodyText?: string
payload: unknown
relayBytes: number
}) {
await dumpRequest({
tag?: DumpTag
}): Promise<DumpHandle | null> {
return dumpRequest({
affinity: input.affinity,
transport: input.transport,
protocol: input.protocol,
Expand All @@ -605,5 +630,37 @@ export async function dumpRelayRequest(input: {
previousBodyText: input.previousBodyText,
payload: input.payload,
relayBytes: input.relayBytes,
tag: input.tag,
})
}

export async function dumpResponseArtifact(
handle: DumpHandle | null,
input: { status: number; message: unknown },
): Promise<void> {
if (!handle) return
const message =
input.message != null &&
typeof input.message === 'object' &&
!Array.isArray(input.message)
? (input.message as Record<string, unknown>)
: {}
const artifact: Record<string, unknown> = { status: input.status }
if (typeof message.id === 'string' && message.id.length > 0)
artifact.message_id = message.id
if (typeof message.model === 'string' && message.model.length > 0)
artifact.model = message.model
if (Object.hasOwn(message, 'usage')) artifact.usage = message.usage
if (Object.hasOwn(message, 'diagnostics'))
artifact.diagnostics = message.diagnostics
try {
await writeDumpFile(
handle.responsePath,
`${JSON.stringify(artifact, null, 2)}\n`,
)
} catch (error) {
relayLog(
`dump response failed: ${error instanceof Error ? error.message : String(error)}`,
)
}
}
7 changes: 6 additions & 1 deletion packages/core/src/relay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1109,6 +1109,7 @@ export async function sendViaRelay(options: {
* within an attempt are ignored.
*/
onResponseHeaders?: (headers: Headers) => void
onDumpCreated?: (handle: { responsePath: string; tag?: 'cachekeep' }) => void
setTimeoutImpl?: typeof globalThis.setTimeout
clearTimeoutImpl?: typeof globalThis.clearTimeout
}): Promise<Response> {
Expand All @@ -1122,6 +1123,7 @@ export async function sendViaRelay(options: {
affinity: explicitAffinity,
optimisticResponse,
onResponseHeaders,
onDumpCreated,
setTimeoutImpl = globalThis.setTimeout,
clearTimeoutImpl = globalThis.clearTimeout,
} = options
Expand Down Expand Up @@ -1237,7 +1239,7 @@ export async function sendViaRelay(options: {
`used relay transport=${result.transport} protocol=${result.protocol} mode=${result.payload.mode} status=${result.response.status} session=${shortAffinity(affinity)} bodyBytes=${bodyText.length} relayBytes=${actualPayloadBytes}`,
)
const dumpStart = perfNowMs()
await dumpRelayRequest({
const dumpHandle = await dumpRelayRequest({
affinity,
transport: result.transport,
protocol: result.protocol,
Expand All @@ -1248,6 +1250,9 @@ export async function sendViaRelay(options: {
payload: result.payload,
relayBytes: actualPayloadBytes,
})
try {
if (dumpHandle) onDumpCreated?.(dumpHandle)
} catch {}
relayPerfLog('dump', {
session: shortAffinity(affinity),
ms: formatMs(perfNowMs() - dumpStart),
Expand Down
Loading