{"record":{"id":"50a074b701bdd514","repo":"JuliusBrussee/caveman","slug":"invalid-caveman-branch-id","errorCode":null,"errorMessage":"Invalid caveman_branch_id","messagePattern":"Invalid caveman_branch_id","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/middleware/typescript/src/langchain.ts","lineNumber":24,"sourceCode":"import { BaseDocumentCompressor } from '@langchain/core/retrievers/document_compressors';\nimport { 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};","sourceCodeStart":6,"sourceCodeEnd":42,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/middleware/typescript/src/langchain.ts#L6-L42","documentation":"scopeFromConfig validates the optional configurable.caveman_branch_id override: it must be a string if provided. Any other type (number, boolean, object) causes this throw, since branch ids are used verbatim in cache scoping keys.","triggerScenarios":"Calling with config.configurable.caveman_branch_id !== undefined and typeof it !== 'string' — e.g. a number or object passed as the branch id.","commonSituations":"Numeric branch identifiers from other systems passed unconverted, programmatically-built config objects with wrong types, or upstream frameworks stuffing non-string values into configurable.","solutions":["Convert the branch id to a string: configurable: { caveman_branch_id: String(branchId) }.","Omit caveman_branch_id entirely to use the default 'main' branch.","Validate the type before invoking when building config dynamically.","Check upstream code that copies fields into configurable and coerces them."],"exampleFix":"// before\n{ configurable: { thread_id: 't1', caveman_branch_id: 7 } } // throws\n// after\n{ configurable: { thread_id: 't1', caveman_branch_id: 'feature-branch-7' } }","handlingStrategy":"validation","validationCode":"function assertBranchId(v: unknown): string | undefined {\n  if (v === undefined) return undefined;\n  if (typeof v !== 'string') throw new Error('caveman_branch_id must be a string');\n  return v;\n}","typeGuard":"const isBranchId = (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_branch_id') {\n    const { caveman_branch_id, ...rest } = config.configurable ?? {};\n    await graph.invoke(input, { ...config, configurable: { ...rest, caveman_branch_id: String(caveman_branch_id) } });\n  } else throw e;\n}","preventionTips":["Always store branch ids as strings","Coerce numeric branch ids with String() when building config","Validate configurable fields at config-construction time","Omit caveman_branch_id unless overriding the default 'main' branch"],"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"}