JuliusBrussee/caveman · error

cave_plan_route_unmatched:${route.segment_id ?? route.segmen

Error message

cave_plan_route_unmatched:${route.segment_id ?? route.segment_kind}

What it means

Thrown when a plan route matches zero segments in the lowered IR. Routes targeting history or tool_result kinds are tolerated (they may legitimately match nothing and 'continue'), but any other unmatched route is an error: the plan does not correspond to this context.

Source

Thrown at packages/agent/src/runtime.ts:2640

  ].map((name) => `caveman.engine.${name}.v1`));
  const evaluated: string[] = [];
  const applied: string[] = [];
  const failures: string[] = [];
  const handles = new Set<string>();
  const trace: MutableTransformTrace[] = [];
  let recoveryResolved = true;
  for (const route of plan.segment_routes) {
    if (!known.has(route.transform_id)) {
      throw new Error(`cave_unknown_transform:${route.transform_id}`);
    }
    const targets = lowered.ir.segments.filter((segment) =>
      segment.kind === route.segment_kind &&
      (route.segment_id === undefined || segment.id === route.segment_id));
    if (targets.length === 0) {
      if (route.segment_kind === "history" || route.segment_kind === "tool_result") {
        continue;
      }
      throw new Error(`cave_plan_route_unmatched:${route.segment_id ?? route.segment_kind}`);
    }
    evaluated.push(route.transform_id);
    let routeApplied = false;
    for (const segment of targets) {
      const original = lowered.bodies.get(segment.bodyHandle);
      if (!original) throw new Error(`cave_context_body_missing:${segment.id}`);
      if (segment.safety !== "S4") throw new Error(`cave_transform_safety_mismatch:${segment.id}`);
      const startedAt = performance.now();
      // beforeTokens/afterTokens are byte-derived (bytes/4) throughout so a
      // delta is always within one basis. beforeTokens counts the ORIGINAL
      // bytes; afterTokens, for an applied transform, counts the FULL provider
      // body actually sent — wrapper included. segment.tokenCount
      // is already estimateTokens(original) = bytes/4.
      const beforeTokens = segment.tokenCount;
      if (segment.opaque) {
        trace.push({
          segmentKind: segment.kind,
          transformID: route.transform_id,

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Regenerate the plan against the current lowered context
  2. If the id in the error message was renamed, update the plan to the new segment id
  3. Invalidate cached plans whenever the agent definition or contexts change
Defensive patterns

Strategy: validation

Validate before calling

function planRoutesMatch(plan: CavePlan, lowered: LoweredContext): boolean {
  return plan.segment_routes.every((route) => {
    const hits = lowered.ir.segments.filter((s) =>
      s.kind === route.segment_kind &&
      (route.segment_id === undefined || s.id === route.segment_id));
    return hits.length > 0 || route.segment_kind === "history" || route.segment_kind === "tool_result";
  });
}

Type guard

function planRouteUnmatched(e: unknown): string | null {
  if (!(e instanceof Error)) return null;
  const m = /^cave_plan_route_unmatched:(.+)$/.exec(e.message);
  return m ? m[1] : null;
}

Try / catch

if (!planRoutesMatch(plan, lowered)) {
  plan = await buildPlan(lowered); // plan and context must come from the same state
}
await applyPlan(plan, lowered);

Prevention

When it happens

Trigger: A route's segment_kind/segment_id filter matches no segments — e.g. the plan was built for a different agent definition, a segment id was renamed, or the context composition changed after the plan was made. Only non-history, non-tool_result kinds throw.

Common situations: Reusing a cached plan across agent definitions or prompt revisions; renamed context segment ids; plan/context version skew.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/443cfa15ce39ed43. Report an issue: GitHub.