{"record":{"id":"4a60c2849a9b9fd9","repo":"JuliusBrussee/caveman","slug":"langgraph-middleware-requires-configurable-thread-id","errorCode":null,"errorMessage":"LangGraph middleware requires configurable.thread_id","messagePattern":"LangGraph middleware requires configurable\\.thread_id","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/middleware/typescript/src/langchain.ts","lineNumber":23,"sourceCode":"import type { JSONSchema } from '@langchain/core/utils/json_schema';\nimport { 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();","sourceCodeStart":5,"sourceCodeEnd":41,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/middleware/typescript/src/langchain.ts#L5-L41","documentation":"scopeFromConfig derives the Caveman Scope (namespace/session/branch/cache epoch) from a LangChain RunnableConfig. LangGraph identifies a conversation thread via configurable.thread_id; without it the middleware cannot scope caching or compression, so it throws. This is a hard requirement for every LangGraph-scoped call.","triggerScenarios":"Calling scopeFromConfig (directly or via resolveLangChainScope/adapters) with config.configurable undefined, or configurable.thread_id not a non-empty string.","commonSituations":"Invoking a LangGraph graph without passing { configurable: { thread_id } }, calling the model standalone (outside a checkpointer-managed graph), or passing thread_id as a number.","solutions":["Pass configurable.thread_id in the invocation config: model.invoke(input, { configurable: { thread_id: 'my-thread' } }).","If using LangGraph, configure a checkpointer (e.g. MemorySaver) so thread_id is threaded through every call.","Coerce numeric/other thread identifiers to non-empty strings before invoking.","If invoking outside a thread context, supply an explicit synthetic thread_id per logical session."],"exampleFix":"// before\nawait graph.invoke({ messages }); // no thread_id\n// after\nawait graph.invoke({ messages }, { configurable: { thread_id: 'session-42' } });","handlingStrategy":"validation","validationCode":"function requireThreadId(config?: RunnableConfig): string {\n  const tid = config?.configurable?.thread_id;\n  if (typeof tid !== 'string' || !tid) throw new Error('configurable.thread_id is required');\n  return tid;\n}","typeGuard":"const hasThreadId = (c?: RunnableConfig): c is RunnableConfig & { configurable: { thread_id: string } } =>\n  typeof c?.configurable?.thread_id === 'string' && c.configurable.thread_id !== '';","tryCatchPattern":"try {\n  await graph.invoke(input, config);\n} catch (e) {\n  if (e instanceof Error && e.message === 'LangGraph middleware requires configurable.thread_id') {\n    await graph.invoke(input, { ...config, configurable: { ...config?.configurable, thread_id: newThreadId() } });\n  } else throw e;\n}","preventionTips":["Always pass { configurable: { thread_id } } on every invoke","Configure a LangGraph checkpointer so thread_id propagates","Generate a stable thread id per logical session before invoking","Type your config helper to require thread_id at compile time"],"tags":["langchain","langgraph","config","missing-argument"],"backgroundTag":"missing-required-config-field","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"}