JuliusBrussee/caveman · error · Error

caveman agent: value is not canonically serializable

Error message

caveman agent: value is not canonically serializable

What it means

stableStringify builds a deterministic JSON form (sorted object keys, recursive) used for provenance hashing. JSON.stringify returns undefined for non-serializable top-level values — bigint, function, symbol, or undefined — and those make canonical serialization impossible, so the builder throws rather than emit a non-deterministic digest.

Source

Thrown at packages/agent/src/context-ir.ts:337

async function sourceBytes(source: string | FileSource, rootDir: string): Promise<Uint8Array> {
  if (typeof source === "string") return new TextEncoder().encode(source);
  const fullPath = resolve(rootDir, source.path);
  const relativePath = relative(rootDir, fullPath);
  if (relativePath === ".." || relativePath.startsWith("../") ||
      relativePath.startsWith("..\\") || isAbsolute(relativePath)) {
    throw new Error("caveman agent: file source escapes project root");
  }
  return new Uint8Array(await readFile(fullPath));
}

function encodeCanonical(value: unknown): Uint8Array {
  return new TextEncoder().encode(stableStringify(value));
}

export function stableStringify(value: unknown): string {
  if (value === null || typeof value !== "object") {
    const encoded = JSON.stringify(value);
    if (encoded === undefined) throw new Error("caveman agent: value is not canonically serializable");
    return encoded;
  }
  if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
  const object = value as Record<string, unknown>;
  return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(object[key])}`).join(",")}}`;
}

export function sha256(value: Uint8Array | string): string {
  return createHash("sha256").update(value).digest("hex");
}

export function opaquePayload(input: Uint8Array): boolean {
  const value = new TextDecoder().decode(input).trim();
  if (/^[-]{5}BEGIN (?:PGP SIGNED MESSAGE|[A-Z ]+ SIGNATURE)[-]{5}/.test(value) ||
      /^[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}$/.test(value) ||
      /\b(?:x-amz-signature|signature)=([0-9a-f]{32,}|[A-Za-z0-9_-]{32,})\b/i.test(value)) {
    return true;
  }

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Convert bigints to strings (or numbers when safe) before building segments: String(row.id)
  2. Strip functions/symbols/undefined fields from payloads (a JSON.parse(JSON.stringify(x)) round-trip drops them all)
  3. Keep IR bodies plain-JSON data only: no class instances, no Map/Set unless pre-serialized to arrays

Example fix

// before
addSegment({ body: { userId: 9007199254740993n } }); // bigint

// after
addSegment({ body: { userId: "9007199254740993" } }); // string
Defensive patterns

Strategy: validation

Validate before calling

function isCanonicallySerializable(v: unknown): boolean {
  if (v === undefined || typeof v === "function" || typeof v === "symbol" || typeof v === "bigint") return false;
  if (v === null || typeof v !== "object") return true;
  if (Array.isArray(v)) return v.every(isCanonicallySerializable);
  return Object.values(v).every(isCanonicallySerializable);
}

Type guard

function isPlainJSON(v: unknown): boolean {
  if (typeof v === "bigint" || typeof v === "function" || typeof v === "symbol" || v === undefined) return false;
  if (v === null || typeof v !== "object") return true;
  if (Array.isArray(v)) return v.every(isPlainJSON);
  const proto = Object.getPrototypeOf(v);
  if (proto !== Object.prototype && proto !== null) return false;
  return Object.entries(v).every(([k, val]) => typeof k === "string" && isPlainJSON(val));
}

Prevention

When it happens

Trigger: Passing a segment payload containing a bigint (e.g. 123n from a DB id), a function, a symbol, or a top-level undefined into content that gets canonically encoded for provenance_digest computation.

Common situations: DB rows with bigint ids (Postgres/SQLite drivers); Date objects work but custom class instances carrying symbol keys; API payloads coerced through libraries that introduce bigint for 64-bit ints.

Related errors


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