JuliusBrussee/caveman · error · Error

Invalid caveman_cache_epoch

Error message

Invalid caveman_cache_epoch

What it means

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.

Solutions

  1. Stringify the epoch: configurable: { caveman_cache_epoch: String(epoch) }.
  2. Omit caveman_cache_epoch to use the default '0'.
  3. Validate with typeof before invoking when the epoch is computed at runtime.
  4. Standardize epoch generation to produce strings across the codebase.

Example fix

// before
{ configurable: { thread_id: 't1', caveman_cache_epoch: 2 } } // throws
// after
{ configurable: { thread_id: 't1', caveman_cache_epoch: '2' } }
Defensive patterns

Strategy: validation

Validate before calling

function assertCacheEpoch(v: unknown): string | undefined {
  if (v === undefined) return undefined;
  if (typeof v !== 'string') throw new Error('caveman_cache_epoch must be a string');
  return v;
}

Type guard

const isCacheEpoch = (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_cache_epoch') {
    const { caveman_cache_epoch, ...rest } = config.configurable ?? {};
    await graph.invoke(input, { ...config, configurable: { ...rest, caveman_cache_epoch: String(caveman_cache_epoch) } });
  } else throw e;
}

Prevention

When it happens

Trigger: Calling with config.configurable.caveman_cache_epoch !== undefined and typeof it !== 'string' — e.g. the number 2 instead of '2'.

Common situations: 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.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/2c79c9dbbc879531. Report an issue: GitHub.

Appendix: source

Thrown at packages/middleware/typescript/src/langchain.ts:25

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};
  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});

View on GitHub (pinned to 3ee70a1026)