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
- Pass the exact plan stored in build.selected_plan into prepareLockedHarnessExecution.
- Rebuild the build lock from the current plan if the plan legitimately changed.
- Avoid mutating the plan object between lock creation and execution; treat it as frozen.
- 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
- Pass lock.selected_plan itself as the plan for locked executions.
- Freeze the plan (Object.freeze) after lock creation to prevent accidental mutation.
- Avoid JSON round-trips of the plan before execution; keep the original object.
- When plan shape evolves, rebuild all outstanding locks rather than patching them.
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
- cave_harness_plan_digest_mismatch
- cave_execution_plan_selection_mismatch
- cave_harness_agent_mismatch
- cave_harness_build_mismatch
- cave_harness_context_ir_mismatch
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)