JuliusBrussee/caveman · error · Error
Invalid caveman_branch_id
Error message
Invalid caveman_branch_id
What it means
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.
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.
Example fix
// before
{ configurable: { thread_id: 't1', caveman_branch_id: 7 } } // throws
// after
{ configurable: { thread_id: 't1', caveman_branch_id: 'feature-branch-7' } } Defensive patterns
Strategy: validation
Validate before calling
function assertBranchId(v: unknown): string | undefined {
if (v === undefined) return undefined;
if (typeof v !== 'string') throw new Error('caveman_branch_id must be a string');
return v;
} Type guard
const isBranchId = (v: unknown): v is string | undefined => v === undefined || typeof v === 'string';
Try / catch
try {
await graph.invoke(input, config);
} catch (e) {
if (e instanceof Error && e.message === 'Invalid caveman_branch_id') {
const { caveman_branch_id, ...rest } = config.configurable ?? {};
await graph.invoke(input, { ...config, configurable: { ...rest, caveman_branch_id: String(caveman_branch_id) } });
} else throw e;
} Prevention
- 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
When it happens
Trigger: Calling with config.configurable.caveman_branch_id !== undefined and typeof it !== 'string' — e.g. a number or object passed as the branch id.
Common situations: Numeric branch identifiers from other systems passed unconverted, programmatically-built config objects with wrong types, or upstream frameworks stuffing non-string values into configurable.
Related errors
- Invalid caveman_cache_epoch
- cave_breaker_retry_backoff_invalid
- cave_breaker_retry_requires_budget
- cave_breaker_retry_spend_invalid
- cave_breaker_threshold_invalid
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/50a074b701bdd514.
Report an issue: GitHub.
Appendix: source
Thrown at packages/middleware/typescript/src/langchain.ts:24
import { BaseDocumentCompressor } from '@langchain/core/retrievers/document_compressors';
import { Document, type DocumentInterface } from '@langchain/core/documents';
import { MiddlewareRuntime, recoveryInputSchema, recoveryToolDescription, type Candidate, type RecoveryBinding, type RetrieveArgs, type Scope, type Usage } from '@caveman-ai/sdk/middleware';
import { currentOwner, manifest, observe, plain, withOwner, type Attempt } from './common.js';
import { adapterCompatible, frameworkVersion } from './compatibility.js';
export type LangChainScope = Scope | ((config: RunnableConfig) => Scope);
export interface LangChainOptions { runtime: MiddlewareRuntime; scope: LangChainScope }
export interface LangChainDocumentOptions extends LangChainOptions {
/** The runtime-owned reader already registered by the application for this scope. */
sourceExpansion?: RecoveryBinding;
}
export const langChainAdapter = { id:'langchain', version:'0.1.0', framework_version:frameworkVersion('langchain')??'unknown', serialization_revision:'langchain-message-v1' };
export const langChainSupported=(_runtime:MiddlewareRuntime)=>adapterCompatible('langchain');
export function scopeFromConfig(config:RunnableConfig, namespace:string):Scope{
const c=config.configurable??{};
if(typeof c.thread_id!=='string'||!c.thread_id)throw new Error('LangGraph middleware requires configurable.thread_id');
if(c.caveman_branch_id!==undefined&&typeof c.caveman_branch_id!=='string')throw new Error('Invalid caveman_branch_id');
if(c.caveman_cache_epoch!==undefined&&typeof c.caveman_cache_epoch!=='string')throw new Error('Invalid caveman_cache_epoch');
return {namespace,session_id:c.thread_id,branch_id:c.caveman_branch_id??'main',cache_epoch:c.caveman_cache_epoch??'0'};
}
export function resolveLangChainScope(source:LangChainScope,config?:RunnableConfig):Scope{
return typeof source==='function'?source(ensureConfig(config)):source;
}
const number=(value:unknown):number|null=>typeof value==='number'&&Number.isSafeInteger(value)&&value>=0?value:null;
export function langChainUsage(value:UsageMetadata|undefined):Usage|null{
if(!value)return null;
const input=number(value.input_tokens),output=number(value.output_tokens);
return {provenance:'client_observed_sdk',complete:input!==null&&output!==null,input_tokens:input,output_tokens:output,
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)};
}
/** Clone only native ToolMessage text. Other message classes stay identical. */
export async function prepareLangChain(messages:BaseMessage[], options:LangChainOptions, config?:RunnableConfig, binding?:RecoveryBinding|null, prefix:BaseMessage[]=[]):Promise<{messages:BaseMessage[];attempt:Attempt|null}>{
const signal=config?.signal;signal?.throwIfAborted();
if(currentOwner())return {messages,attempt:null};View on GitHub (pinned to 3ee70a1026)