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

  1. Convert the branch id to a string: configurable: { caveman_branch_id: String(branchId) }.
  2. Omit caveman_branch_id entirely to use the default 'main' branch.
  3. Validate the type before invoking when building config dynamically.
  4. 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

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


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)