JuliusBrussee/caveman · error
engine_changed_bytes_without_recovery
Error message
engine_changed_bytes_without_recovery
What it means
Thrown after running a compression engine when the engine's output bytes differ from the input but it returned no recovery_handle. The contract is strict: the engine may either pass bytes through unchanged, or change them AND provide a handle so cave_retrieve can recover the original. Changing bytes without a handle is unrecoverable and fails immediately.
Source
Thrown at packages/agent/src/runtime.ts:2777
async function engineCompress(
input: Uint8Array,
engineBin: string | undefined,
contentType = "text",
signal?: AbortSignal,
): Promise<{ output: Uint8Array; handle?: string; tokensBefore?: number; tokensAfter?: number }> {
const result = await runEngine(engineBin, ["compress", "--type", contentType], input, signal);
const lastLine = result.stderr.trim().split("\n").pop() ?? "{}";
const report = JSON.parse(lastLine) as {
recovery_handle?: unknown;
tokens_before?: unknown;
tokens_after?: unknown;
};
const handle = typeof report.recovery_handle === "string" && report.recovery_handle.length > 0
? report.recovery_handle
: undefined;
if (!handle && !bytesEqual(result.stdout, input)) {
throw new Error("engine_changed_bytes_without_recovery");
}
const tokensBefore = validEngineTokenCount(report.tokens_before);
const tokensAfter = validEngineTokenCount(report.tokens_after);
return {
output: result.stdout,
...(handle === undefined ? {} : { handle }),
...(tokensBefore === undefined ? {} : { tokensBefore }),
...(tokensAfter === undefined ? {} : { tokensAfter }),
};
}
function engineContentType(transformID: string): string {
const match = /^caveman\.engine\.([a-z0-9-]+)\.v1$/.exec(transformID);
if (!match) throw new Error(`cave_unknown_transform:${transformID}`);
return match[1]!;
}
View on GitHub (pinned to 27d5a3981a)
Solutions
- Verify the engine binary version matches the runtime version (engineBin path)
- Reproduce with a direct engine invocation: run `<engineBin> compress --type text` and inspect the last stderr JSON line for recovery_handle
- If using a custom engine, ensure it either outputs identical bytes or emits a valid recovery_handle in the report
- Update both runtime and engine together from the same release
Defensive patterns
Strategy: try-catch
Validate before calling
// Smoke-test the engine before the run:
const input = new TextEncoder().encode("probe");
const result = await runEngine(engineBin, ["compress", "--type", "text"], input);
const report = JSON.parse(result.stderr.trim().split("\n").pop() ?? "{}");
const changed = !bytesEqual(result.stdout, input);
if (changed && !report.recovery_handle) { /* engine broken: fail fast */ } Type guard
function isEngineChangedWithoutRecovery(e: unknown): e is Error {
return e instanceof Error && e.message === "engine_changed_bytes_without_recovery";
} Try / catch
try {
await run(engineBin, ["compress", "--type", contentType], input, signal);
} catch (e) {
if (isEngineChangedWithoutRecovery(e)) {
// fall back to the original bytes (fail open) or abort the run
} else throw e;
} Prevention
- Pin runtime and engine binary to the same release
- Smoke-test compress+recover round-trips at startup
- Never swap in a custom engine that omits recovery_handle reporting
When it happens
Trigger: runEngine('compress') returns stdout that differs from input, and the stderr JSON report has no non-empty recovery_handle string.
Common situations: A buggy or modified caveman engine binary; version mismatch between runtime expectations and engine report format (recovery_handle field renamed/dropped); an engine crash producing partial output; someone swapping in a custom engine that compresses without registering recovery state.
Related errors
- registry output has no valid registry_sha256
- registry output has no capabilities
- registry output has invalid capability
- cave_transform_registry_unavailable: run caveman setup or se
- caveman-code: no tool output recorded yet to prove recovery
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/3529ae550e75178c.
Report an issue: GitHub.