{"record":{"id":"2c79c9dbbc879531","repo":"JuliusBrussee/caveman","slug":"invalid-caveman-cache-epoch","errorCode":null,"errorMessage":"Invalid caveman_cache_epoch","messagePattern":"Invalid caveman_cache_epoch","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/middleware/typescript/src/langchain.ts","lineNumber":25,"sourceCode":"import { Document, type DocumentInterface } from '@langchain/core/documents';\nimport { MiddlewareRuntime, recoveryInputSchema, recoveryToolDescription, type Candidate, type RecoveryBinding, type RetrieveArgs, type Scope, type Usage } from '@caveman-ai/sdk/middleware';\nimport { currentOwner, manifest, observe, plain, withOwner, type Attempt } from './common.js';\nimport { adapterCompatible, frameworkVersion } from './compatibility.js';\n\nexport type LangChainScope = Scope | ((config: RunnableConfig) => Scope);\nexport interface LangChainOptions { runtime: MiddlewareRuntime; scope: LangChainScope }\nexport interface LangChainDocumentOptions extends LangChainOptions {\n  /** The runtime-owned reader already registered by the application for this scope. */\n  sourceExpansion?: RecoveryBinding;\n}\nexport const langChainAdapter = { id:'langchain', version:'0.1.0', framework_version:frameworkVersion('langchain')??'unknown', serialization_revision:'langchain-message-v1' };\nexport const langChainSupported=(_runtime:MiddlewareRuntime)=>adapterCompatible('langchain');\n\nexport function scopeFromConfig(config:RunnableConfig, namespace:string):Scope{\n  const c=config.configurable??{};\n  if(typeof c.thread_id!=='string'||!c.thread_id)throw new Error('LangGraph middleware requires configurable.thread_id');\n  if(c.caveman_branch_id!==undefined&&typeof c.caveman_branch_id!=='string')throw new Error('Invalid caveman_branch_id');\n  if(c.caveman_cache_epoch!==undefined&&typeof c.caveman_cache_epoch!=='string')throw new Error('Invalid caveman_cache_epoch');\n  return {namespace,session_id:c.thread_id,branch_id:c.caveman_branch_id??'main',cache_epoch:c.caveman_cache_epoch??'0'};\n}\nexport function resolveLangChainScope(source:LangChainScope,config?:RunnableConfig):Scope{\n  return typeof source==='function'?source(ensureConfig(config)):source;\n}\nconst number=(value:unknown):number|null=>typeof value==='number'&&Number.isSafeInteger(value)&&value>=0?value:null;\nexport function langChainUsage(value:UsageMetadata|undefined):Usage|null{\n  if(!value)return null;\n  const input=number(value.input_tokens),output=number(value.output_tokens);\n  return {provenance:'client_observed_sdk',complete:input!==null&&output!==null,input_tokens:input,output_tokens:output,\n    cache_read_tokens:number(value.input_token_details?.cache_read),cache_write_tokens:number(value.input_token_details?.cache_creation),reasoning_tokens:number(value.output_token_details?.reasoning)};\n}\n\n/** Clone only native ToolMessage text. Other message classes stay identical. */\nexport async function prepareLangChain(messages:BaseMessage[], options:LangChainOptions, config?:RunnableConfig, binding?:RecoveryBinding|null, prefix:BaseMessage[]=[]):Promise<{messages:BaseMessage[];attempt:Attempt|null}>{\n  const signal=config?.signal;signal?.throwIfAborted();\n  if(currentOwner())return {messages,attempt:null};\n  const passive=(reason:string)=>({messages,attempt:{runtime:options.runtime,scope:{namespace:'caveman-passive',session_id:'report-only',branch_id:'main',cache_epoch:'0'},logicalCallId:crypto.randomUUID(),attemptId:crypto.randomUUID(),optimization:null,wireSHA256:null,passive:true,reason,adapter:'langchain'} satisfies Attempt});","sourceCodeStart":7,"sourceCodeEnd":43,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/middleware/typescript/src/langchain.ts#L7-L43","documentation":"scopeFromConfig validates the optional configurable.caveman_cache_epoch override: it must be a string if provided. The epoch participates in cache keys, so non-string values are rejected to keep scoping deterministic and collision-free.","triggerScenarios":"Calling with config.configurable.caveman_cache_epoch !== undefined and typeof it !== 'string' — e.g. the number 2 instead of '2'.","commonSituations":"Passing a numeric cache epoch (like 2 or Date.now()) instead of a string, or config built by another tool that uses number types for epochs/generations.","solutions":["Stringify the epoch: configurable: { caveman_cache_epoch: String(epoch) }.","Omit caveman_cache_epoch to use the default '0'.","Validate with typeof before invoking when the epoch is computed at runtime.","Standardize epoch generation to produce strings across the codebase."],"exampleFix":"// before\n{ configurable: { thread_id: 't1', caveman_cache_epoch: 2 } } // throws\n// after\n{ configurable: { thread_id: 't1', caveman_cache_epoch: '2' } }","handlingStrategy":"validation","validationCode":"function assertCacheEpoch(v: unknown): string | undefined {\n  if (v === undefined) return undefined;\n  if (typeof v !== 'string') throw new Error('caveman_cache_epoch must be a string');\n  return v;\n}","typeGuard":"const isCacheEpoch = (v: unknown): v is string | undefined =>\n  v === undefined || typeof v === 'string';","tryCatchPattern":"try {\n  await graph.invoke(input, config);\n} catch (e) {\n  if (e instanceof Error && e.message === 'Invalid caveman_cache_epoch') {\n    const { caveman_cache_epoch, ...rest } = config.configurable ?? {};\n    await graph.invoke(input, { ...config, configurable: { ...rest, caveman_cache_epoch: String(caveman_cache_epoch) } });\n  } else throw e;\n}","preventionTips":["Represent cache epochs as strings (e.g. String(Date.now()))","Coerce numbers to strings before placing them in configurable","Validate configurable fields centrally in one config builder","Omit caveman_cache_epoch to use the default '0'"],"tags":["langchain","config","type-mismatch","validation"],"backgroundTag":"config-type-mismatch","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}