JuliusBrussee/caveman · error · Error

cave_execution_plan_selection_mismatch

cave_execution_plan_selection_mismatch

Error message

cave_execution_plan_selection_mismatch

What it means

validatePlanSelection asserts that the provider/model/reasoning a runtime actually selected is exactly the locked plan's selection, by comparing `${provider}/${model}` to plan.model and reasoning to plan.reasoning (with plan value "none" normalized to runtime value "off"). It is the execution kernel's guard that locked builds never silently run a different model or effort level than the compiler selected. The string comparison means provider and model identity, not just the concatenation, must match byte-for-byte.

Source

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

  readonly plan: CavePlan;
  readonly planSHA256: string;
  readonly contextIRSHA256: string;
  readonly provider: string;
  readonly model: string;
}

export function validatePlanSelection(
  plan: CavePlan,
  selected: {
    provider: string;
    model: string;
    reasoning: "off" | "minimal" | "low" | "medium" | "high";
  },
): void {
  const expectedReasoning = plan.reasoning === "none" ? "off" : plan.reasoning;
  if (`${selected.provider}/${selected.model}` !== plan.model ||
      selected.reasoning !== expectedReasoning) {
    throw new Error("cave_execution_plan_selection_mismatch");
  }
}

export function prepareLockedHarnessExecution(input: {
  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");
  }

View on GitHub (pinned to 766dce6b13)

Solutions

  1. Derive provider and model from plan.model itself (split on "/" once, rejoin the remainder) instead of using an independent config value
  2. Map reasoning exactly: selected.reasoning = plan.reasoning === "none" ? "off" : plan.reasoning
  3. If the mismatch is intentional (you want a different model), recompile the plan/build rather than bypassing the check
  4. Log both `${provider}/${model}` and plan.model next to the two reasoning values to see which half diverged

Example fix

// before
validatePlanSelection(plan, {
  provider: config.provider,            // e.g. "openai"
  model: config.model,                  // e.g. "gpt-4o" — may differ from plan
  reasoning: "low",                     // hardcoded, plan locked "medium"
});

// after — derive everything from the locked plan
const [provider, ...modelParts] = plan.model.split("/");
validatePlanSelection(plan, {
  provider,
  model: modelParts.join("/"),
  reasoning: plan.reasoning === "none" ? "off" : plan.reasoning,
});
Defensive patterns

Strategy: validation

Validate before calling

import type { CavePlan } from "@caveman-ai/agent";

function selectionFromPlan(plan: CavePlan): {
  provider: string;
  model: string;
  reasoning: "off" | "minimal" | "low" | "medium" | "high";
} {
  const [provider, ...modelParts] = plan.model.split("/");
  return {
    provider,
    model: modelParts.join("/"),
    reasoning: plan.reasoning === "none" ? "off" : plan.reasoning,
  };
}

// construct `selected` exclusively via selectionFromPlan(plan) — never from app config

Type guard

const isRuntimeReasoning = (value: unknown): value is "off" | "minimal" | "low" | "medium" | "high" =>
  ["off", "minimal", "low", "medium", "high"].includes(value as string);

Try / catch

try {
  validatePlanSelection(plan, selected);
} catch (error) {
  if (error instanceof Error && error.message === "cave_execution_plan_selection_mismatch") {
    const expectedReasoning = plan.reasoning === "none" ? "off" : plan.reasoning;
    throw new Error(
      `runtime selected ${selected.provider}/${selected.model}/${selected.reasoning} ` +
      `but plan locked ${plan.model}/${expectedReasoning}`,
      { cause: error },
    );
  }
  throw error;
}

Prevention

When it happens

Trigger: Passing selected.reasoning "none" (plan vocabulary) instead of "off" (runtime vocabulary); building selected from your app config's default model instead of the plan's model string; a provider or model string containing an extra slash so the concatenation shifts (e.g. model "openrouter/anthropic/claude-..."); validating against a different CavePlan than the one locked.

Common situations: Adapters wiring a user-configurable model alongside a locked build; framework upgrades renaming reasoning levels; copy-pasted selection code that hardcodes reasoning "low" while the plan locked "medium"; splitting plan.model on the wrong delimiter before recombining.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@766dce6b13 (2026-08-18). Data as JSON: /api/errors/935675a62240d9a6. Report an issue: GitHub.