JuliusBrussee/caveman · error

cave_sandbox_failure_not_serializable

cave_sandbox_failure_not_serializable

Error message

cave_sandbox_failure_not_serializable

What it means

Inside the sandboxed tool worker, writeResult serializes the result frame with JSON.stringify. If the FAILURE payload itself cannot be serialized (the failure object's fields — e.g. error message/code — are fine, but a nested value like a BigInt or circular reference sneaks into the payload), and it is not an ok result, the worker throws `cave_sandbox_failure_not_serializable` instead of writing a terminal frame. Successful results get a fallback `cave_sandbox_result_not_serializable` frame; failure payloads have no safe fallback, so this error surfaces.

Solutions

  1. Ensure thrown errors in sandboxed tools only carry string fields (message plus optional string `resource`); strip or stringify BigInt/circular values before throwing.
  2. Convert BigInt values with String()/Number() and break circular references before rejecting.
  3. Wrap tool logic so errors are re-thrown as plain `new Error(msg)` without extra properties.
  4. Check whether a recent tool change introduced exotic error metadata and revert it.

Example fix

// before
throw Object.assign(new Error("quota exceeded"), { used: 9007199254740993n });

// after
throw new Error(`quota exceeded (used=${String(9007199254740993n)})`);
Defensive patterns

Strategy: try-catch

Validate before calling

function isJsonSafe(v: unknown, seen = new Set()): boolean {
  if (v === null || typeof v !== "object") return typeof v !== "bigint";
  if (seen.has(v)) return false;
  seen.add(v);
  return Object.values(v).every((x) => isJsonSafe(x, seen));
}
// guard: isJsonSafe(maybeErrorMetadata) before throwing enriched errors

Type guard

const isPlainError = (e: unknown): e is Error =>
  e instanceof Error &&
  Object.getOwnPropertyNames(e).every((k) => k === "message" || typeof (e as any)[k] === "string");

Try / catch

try {
  await runSandboxedTool(args);
} catch (err) {
  // re-throw as a plain serializable error so the worker failure frame can be written
  throw new Error(err instanceof Error ? err.message : String(err));
}

Prevention

When it happens

Trigger: A sandboxed tool throws an error whose failure-frame payload contains a JSON-incompatible value (BigInt, circular structure, function) so JSON.stringify throws while writing the failure result before resultWritten is set.

Common situations: A tool error object carries a BigInt counter or circular `cause` chain; a custom Error subclass attaches non-serializable `resource` metadata; a library rejection embeds Node Buffer-circular internals.

Understand the failure class

Background: "JSON serialization failed", "not JSON serializable", "Failed to serialize": why JSON marshaling errors happen and how to fix them — this error's family across 46 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/a497b723a93beb3c. Report an issue: GitHub.

Appendix: source

Thrown at packages/agent/src/tool-worker.ts:26

import type { AgentDefinition } from "./index.js";
import { installNetworkDeny } from "./sandbox-network.js";

// The result travels on a DEDICATED fd 3, length-prefixed — never stdout
// Any console.log in the tool's own import graph writes to stdout
// (fd 1) and the parent ignores it, so it can no longer collide with the result
// JSON and corrupt it into cave_sandbox_invalid_output.
let resultWritten = false;
function writeResult(payload: { ok: boolean; value?: unknown; code?: string }): void {
  if (resultWritten) return;
  let encoded: string;
  try {
    encoded = JSON.stringify(payload);
  } catch {
    if (payload.ok) {
      writeResult({ ok: false, code: "cave_sandbox_result_not_serializable" });
      return;
    }
    throw new Error("cave_sandbox_failure_not_serializable");
  }
  // Mark terminal only after serialization succeeds. Otherwise a BigInt or
  // circular tool value suppresses the failure frame and becomes an opaque
  // cave_sandbox_invalid_output at the parent.
  resultWritten = true;
  const body = Buffer.from(encoded, "utf8");
  const header = Buffer.allocUnsafe(4);
  header.writeUInt32BE(body.byteLength, 0);
  const frame = Buffer.concat([header, body]);
  let offset = 0;
  while (offset < frame.byteLength) {
    offset += writeSync(3, frame, offset, frame.byteLength - offset);
  }
}

function failureCode(error: unknown): string {
  return error instanceof Error
    ? [

View on GitHub (pinned to 3ee70a1026)