JuliusBrussee/caveman · error
cave_transform_safety_mismatch
cave_transform_safety_mismatch
Error message
cave_transform_safety_mismatch:${segment.id} What it means
Thrown when a compaction route targets a segment whose safety classification is not 'S4'. Only S4 (lowest-sensitivity, transform-safe) segments may be rewritten by engine transforms; touching S1–S3 segments is refused to protect sensitive content. The segment id is included in the message.
Solutions
- Narrow the plan route with an explicit segment_id that points at an S4 segment.
- Check the safety classification assigned during lowering and correct it if it is wrongly restricted.
- Remove routes that target non-S4 kinds; route only history/tool_result or S4 context.
- If the segment genuinely is safe, update its safety level in the definition/lowering to S4.
Example fix
// before
segment_routes: [{ segment_kind: "context", transform_id: "..." }] // matches S2 segment
// after
segment_routes: [{ segment_kind: "context", segment_id: "docs-s4", transform_id: "..." }] Defensive patterns
Strategy: validation
Validate before calling
for (const route of plan.segment_routes) {
const targets = lowered.ir.segments.filter(s => s.kind === route.segment_kind && (route.segment_id === undefined || s.id === route.segment_id));
const unsafe = targets.filter(s => s.safety !== "S4");
if (unsafe.length) console.warn(`route would touch non-S4 segments: ${unsafe.map(s => s.id).join(",")}`);
} Type guard
function isTransformSafe(seg: { safety: string }): boolean { return seg.safety === "S4"; } Try / catch
try {
await executePlan(plan, lowered);
} catch (err) {
if (err instanceof Error && err.message.startsWith("cave_transform_safety_mismatch:")) {
const id = err.message.split(":")[1];
// retarget the route to an S4 segment or fix the safety label
} else throw err;
} Prevention
- Constrain plan routes with explicit segment_id when targeting context segments.
- Review safety classifications whenever new context sources are added.
- Never widen safety levels to silence this error without a data-sensitivity review.
- Filter route targets to safety === "S4" inside custom plan generators.
When it happens
Trigger: A plan route matches a segment with segment.safety !== "S4" (e.g. instructions or a user-classified context segment) and the runtime attempts to apply the transform to it.
Common situations: Broad segment_kind routes that sweep in higher-safety segments; segments mislabeled S4-eligible during lowering; plans reused across agents where the same kind has different safety labels.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- cave_context_body_missing
- cave_context_segment_missing
- cave_plan_route_unmatched
- cave_unknown_transform
- cave_budget_conflicting_cap
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/09fab7ec9dd853cc.
Report an issue: GitHub.
Appendix: source
Thrown at packages/agent/src/runtime.ts:2647
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,
safetyClass: segment.safety,
beforeTokens,
afterTokens: beforeTokens,
tokensBasis: "byte_derived",
recoveryKind: segment.recovery,
recoveryUsed: false,
latencyMs: Math.round(performance.now() - startedAt),View on GitHub (pinned to 3ee70a1026)