{"record":{"id":"3529ae550e75178c","repo":"JuliusBrussee/caveman","slug":"engine-changed-bytes-without-recovery","errorCode":null,"errorMessage":"engine_changed_bytes_without_recovery","messagePattern":"engine_changed_bytes_without_recovery","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/runtime.ts","lineNumber":2777,"sourceCode":"\nasync function engineCompress(\n  input: Uint8Array,\n  engineBin: string | undefined,\n  contentType = \"text\",\n  signal?: AbortSignal,\n): Promise<{ output: Uint8Array; handle?: string; tokensBefore?: number; tokensAfter?: number }> {\n  const result = await runEngine(engineBin, [\"compress\", \"--type\", contentType], input, signal);\n  const lastLine = result.stderr.trim().split(\"\\n\").pop() ?? \"{}\";\n  const report = JSON.parse(lastLine) as {\n    recovery_handle?: unknown;\n    tokens_before?: unknown;\n    tokens_after?: unknown;\n  };\n  const handle = typeof report.recovery_handle === \"string\" && report.recovery_handle.length > 0\n    ? report.recovery_handle\n    : undefined;\n  if (!handle && !bytesEqual(result.stdout, input)) {\n    throw new Error(\"engine_changed_bytes_without_recovery\");\n  }\n  const tokensBefore = validEngineTokenCount(report.tokens_before);\n  const tokensAfter = validEngineTokenCount(report.tokens_after);\n  return {\n    output: result.stdout,\n    ...(handle === undefined ? {} : { handle }),\n    ...(tokensBefore === undefined ? {} : { tokensBefore }),\n    ...(tokensAfter === undefined ? {} : { tokensAfter }),\n  };\n}\n\nfunction engineContentType(transformID: string): string {\n  const match = /^caveman\\.engine\\.([a-z0-9-]+)\\.v1$/.exec(transformID);\n  if (!match) throw new Error(`cave_unknown_transform:${transformID}`);\n  return match[1]!;\n}\n\n","sourceCodeStart":2759,"sourceCodeEnd":2795,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/runtime.ts#L2759-L2795","documentation":"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.","triggerScenarios":"runEngine('compress') returns stdout that differs from input, and the stderr JSON report has no non-empty recovery_handle string.","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Smoke-test the engine before the run:\nconst input = new TextEncoder().encode(\"probe\");\nconst result = await runEngine(engineBin, [\"compress\", \"--type\", \"text\"], input);\nconst report = JSON.parse(result.stderr.trim().split(\"\\n\").pop() ?? \"{}\");\nconst changed = !bytesEqual(result.stdout, input);\nif (changed && !report.recovery_handle) { /* engine broken: fail fast */ }","typeGuard":"function isEngineChangedWithoutRecovery(e: unknown): e is Error {\n  return e instanceof Error && e.message === \"engine_changed_bytes_without_recovery\";\n}","tryCatchPattern":"try {\n  await run(engineBin, [\"compress\", \"--type\", contentType], input, signal);\n} catch (e) {\n  if (isEngineChangedWithoutRecovery(e)) {\n    // fall back to the original bytes (fail open) or abort the run\n  } else throw e;\n}","preventionTips":["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"],"tags":["engine","compression","recovery","version-skew"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}