JuliusBrussee/caveman · error

cave_sandbox_failure_not_serializable

cave_sandbox_failure_not_serializable

Error message

cave_sandbox_failure_not_serializable

What it means

The tool worker writes exactly one result frame to fd 3; if serializing a FAILURE payload ({ ok: false, ... }) with JSON.stringify throws (e.g. a non-serializable property like BigInt or a circular reference on the error), writeResult throws cave_sandbox_failure_not_serializable — it cannot even produce a fallback failure frame. This is a last-resort invariant: terminal state is only marked after serialization succeeds so a bad value can never corrupt the protocol into an opaque invalid-output.

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 27d5a3981a)

Solutions

  1. Sanitize thrown errors in tool code: keep message a string and drop or String() non-serializable custom properties.
  2. If you attach context to errors, attach primitives or serializable summaries only.
  3. Reproduce locally by running the tool in-process and inspecting the thrown error's own enumerable properties.

Example fix

// before
throw Object.assign(new Error("bad value"), { value: 10n });

// after
throw new Error(`bad value: ${10n}`);
Defensive patterns

Strategy: fallback

Validate before calling

function serializableError(err: unknown): boolean {
  try { JSON.stringify({ m: (err as Error)?.message, c: (err as Error)?.cause }); return true; }
  catch { return false; }
}

Type guard

function isSerializable(value: unknown): boolean {
  try { JSON.stringify(value); return true; } catch { return false; }
}

Try / catch

// In tool code — sanitize before throwing from a sandboxed worker:
try {
  risky();
} catch (error) {
  throw new Error(error instanceof Error ? String(error.message) : String(error));
}

Prevention

When it happens

Trigger: A sandboxed tool throws an Error whose own properties (message, cause, custom fields) contain BigInt, circular structures, or other JSON-hostile values carried onto the failure payload.

Common situations: Custom error classes attaching the offending value itself (e.g. error.value = someBigInt); rethrowing provider/tool objects with cycles; almost never hit because success payloads convert to cave_sandbox_result_not_serializable instead.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/a497b723a93beb3c. Report an issue: GitHub.