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
- Convert bigints to strings (or numbers when safe) before building segments: String(row.id)
- Strip functions/symbols/undefined fields from payloads (a JSON.parse(JSON.stringify(x)) round-trip drops them all)
- 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
- Convert bigints to strings at API boundaries (DB drivers commonly emit them)
- Round-trip dynamic payloads through JSON.parse(JSON.stringify(x)) to drop unserializable values
- Keep IR bodies to plain JSON data — no class instances, Maps, or Sets
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
- cave_harness_wire_contract_invalid
- cave_context_ir_invalid
- {label}: verified_at is in the future
- {label}: verified_at is older than 120 days
- {label}: unreviewed sync proposal marker on line(s) {', '.jo
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/edbba1276f795ae6.
Report an issue: GitHub.