JuliusBrussee/caveman · error

cave_harness_plan_mismatch

cave_harness_plan_mismatch

Error message

cave_harness_plan_mismatch

What it means

The kernel compares a stable serialization of the runtime CavePlan against the plan embedded in the build lock. It throws when the two plans differ in any serialized field, meaning the execution request's plan is not the plan that was locked at build time.

Solutions

  1. Pass the exact plan stored in build.selected_plan into prepareLockedHarnessExecution.
  2. Rebuild the build lock from the current plan if the plan legitimately changed.
  3. Avoid mutating the plan object between lock creation and execution; treat it as frozen.
  4. Diff stableStringify(plan) against stableStringify(build.selected_plan) to see which field drifted.

Example fix

// before
const plan = { ...locked.selected_plan, reasoning: "high" };
await prepared({ build: lock, plan });
// after
await prepared({ build: lock, plan: lock.selected_plan });
Defensive patterns

Strategy: try-catch

Validate before calling

import { stableStringify } from "./execution-kernel";
if (stableStringify(plan) !== stableStringify(lock.selected_plan)) {
  lock = await buildCaveBuildLock({ plan });
}

Type guard

function planMatchesLock(plan: object, lock: { selected_plan: object }): boolean {
  return stableStringify(plan) === stableStringify(lock.selected_plan);
}

Try / catch

try {
  await prepared({ build: lock, plan });
} catch (err) {
  if (err instanceof Error && err.message === "cave_harness_plan_mismatch") {
    const fresh = await buildCaveBuildLock({ plan });
    return prepared({ build: fresh, plan });
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling prepared or streamAgentInternal with an input.plan whose stableStringify output differs from build.selected_plan — any added/removed/changed plan field (model, reasoning, tools, etc.) triggers it.

Common situations: Mutating the plan after building the lock (adding a tool or changing reasoning); constructing a fresh plan object with defaults differing from the locked one; JSON round-tripping that changes field order only if not using the stable stringify contract; schema version bump changing plan shape.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


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

Appendix: source

Thrown at packages/agent/src/execution-kernel.ts:82

  build: CaveBuildLock;
  harness: string;
  adapterVersion: string;
  upstreamVersion: string;
  agentId?: string;
  contextIR: ContextIR;
  plan: CavePlan;
}): LockedHarnessPreparation {
  const build = parseCaveBuildLock(input.build);
  if (build.harness.id !== input.harness ||
      build.harness.adapter_version !== input.adapterVersion ||
      build.harness.upstream_version !== input.upstreamVersion) {
    throw new Error("cave_harness_build_mismatch");
  }
  if (input.agentId !== undefined && build.agent_id !== input.agentId) {
    throw new Error("cave_harness_agent_mismatch");
  }
  if (stableStringify(input.plan) !== stableStringify(build.selected_plan)) {
    throw new Error("cave_harness_plan_mismatch");
  }
  const planSHA256 = sha256(stableStringify(input.plan));
  if (planSHA256 !== build.plan_sha256) {
    throw new Error("cave_harness_plan_digest_mismatch");
  }
  const contextIRSHA256 = sha256(stableStringify(contextIRToWire(input.contextIR)));
  if (contextIRSHA256 !== build.context_ir_sha256) {
    throw new Error("cave_harness_context_ir_mismatch");
  }
  const [provider, ...modelParts] = input.plan.model.split("/");
  const model = modelParts.join("/");
  if (!provider || !model) throw new Error("cave_harness_model_invalid");
  return Object.freeze({
    build,
    plan: build.selected_plan,
    planSHA256,
    contextIRSHA256,
    provider,

View on GitHub (pinned to 3ee70a1026)