JuliusBrussee/caveman · error · Error

cave_harness_wire_contract_invalid

cave_harness_wire_contract_invalid

Error message

cave_harness_wire_contract_invalid

What it means

`canonicalRecord` round-trips a wire payload through `stableStringify` to produce a canonical form for hashing and freezing. If `stableStringify` throws — classically because the value contains a circular reference or a value JSON cannot represent (function, symbol, BigInt, undefined inside arrays) — the adapter throws `cave_harness_wire_contract_invalid`: the payload does not satisfy the serializable wire contract the harness evidence chain requires. The payload must be plain JSON data.

Source

Thrown at packages/agent/src/adapters.ts:601

  // longer authorize execution.
  const installed = installedPackageVersion(pkg);
  if (installed === undefined) {
    throw new Error(`cave_${harness}_upstream_version_unresolvable`);
  }
  if (installed !== expected) {
    throw new Error(`cave_${harness}_upstream_version_unsupported`);
  }
  if (identity.upstreamVersion !== installed) {
    throw new Error(`cave_${harness}_upstream_version_mismatch`);
  }
}

function canonicalRecord(value: Readonly<Record<string, unknown>>): Readonly<Record<string, unknown>> {
  let encoded: string;
  try {
    encoded = stableStringify(value);
  } catch {
    throw new Error("cave_harness_wire_contract_invalid");
  }
  const decoded = JSON.parse(encoded) as unknown;
  if (!isRecord(decoded)) throw new Error("cave_harness_wire_contract_invalid");
  return decoded;
}

function deepFreeze<T>(value: T): T {
  if (value !== null && typeof value === "object") {
    Object.freeze(value);
    for (const child of Object.values(value)) deepFreeze(child);
  }
  return value;
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return value !== null && typeof value === "object" && !Array.isArray(value);
}

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Strip non-serializable values from the record before passing it: keep only strings, numbers, booleans, null, arrays, and plain objects.
  2. Break circular references (delete parent/back-pointers) or map entities to plain DTOs.
  3. If you must carry a live object, keep it outside the record — like the adapter itself does with `AbortSignal` in `snapshotRequest`.

Example fix

// before
const payload = { prompt: "hi", client: someLiveClient }; // not serializable

// after
const { client, ...payload } = config; // client stays outside the wire record
const wirePayload = payload; // plain JSON data only
Defensive patterns

Strategy: validation

Validate before calling

function isJsonSerializable(value: unknown, seen = new Set()): boolean {
  if (value === null || ["string", "number", "boolean"].includes(typeof value)) return true;
  if (typeof value === "object") {
    if (seen.has(value)) return false; // circular
    seen.add(value);
    return Object.values(value).every((v) => isJsonSerializable(v, seen));
  }
  return false; // function, symbol, bigint, undefined
}
if (!isJsonSerializable(record)) throw new Error("strip non-serializable values first");

Try / catch

try {
  await adapter.run(request);
} catch (err) {
  if (err instanceof Error && err.message === "cave_harness_wire_contract_invalid") {
    // log which record failed, strip live objects, retry with plain data
  }
}

Prevention

When it happens

Trigger: Passing a harness request/record containing a circular object graph; embedding a function, class instance, symbol, or BigInt in what should be a JSON-serializable record; a computed property getter that throws during stringification.

Common situations: Attaching live objects (loggers, clients, DOM nodes) into request metadata; reusing domain entities with parent pointers as wire records; a refactor that moved a closure into a previously-plain config object.

Related errors


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