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
- Ensure thrown errors in sandboxed tools only carry string fields (message plus optional string `resource`); strip or stringify BigInt/circular values before throwing.
- Convert BigInt values with String()/Number() and break circular references before rejecting.
- Wrap tool logic so errors are re-thrown as plain `new Error(msg)` without extra properties.
- 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
- Throw only plain Error objects with string message (and optional string `resource`) from sandboxed tools.
- Never attach BigInt, functions, or circular structures to error objects crossing the sandbox boundary.
- Stringify any large/numeric-exotic values into the message text instead of extra properties.
- Add a unit test asserting every error path in your tool is JSON.stringify-safe.
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
- assembly slot content is not JSON-serializable
- assembly slot content is not JSON-serializable
- caveman agent: value is not canonically serializable
- encode before evidence
- encode report
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)