{"record":{"id":"edbba1276f795ae6","repo":"JuliusBrussee/caveman","slug":"caveman-agent-value-is-not-canonically-serializab","errorCode":null,"errorMessage":"caveman agent: value is not canonically serializable","messagePattern":"caveman agent: value is not canonically serializable","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/agent/src/context-ir.ts","lineNumber":337,"sourceCode":"async function sourceBytes(source: string | FileSource, rootDir: string): Promise<Uint8Array> {\n  if (typeof source === \"string\") return new TextEncoder().encode(source);\n  const fullPath = resolve(rootDir, source.path);\n  const relativePath = relative(rootDir, fullPath);\n  if (relativePath === \"..\" || relativePath.startsWith(\"../\") ||\n      relativePath.startsWith(\"..\\\\\") || isAbsolute(relativePath)) {\n    throw new Error(\"caveman agent: file source escapes project root\");\n  }\n  return new Uint8Array(await readFile(fullPath));\n}\n\nfunction encodeCanonical(value: unknown): Uint8Array {\n  return new TextEncoder().encode(stableStringify(value));\n}\n\nexport function stableStringify(value: unknown): string {\n  if (value === null || typeof value !== \"object\") {\n    const encoded = JSON.stringify(value);\n    if (encoded === undefined) throw new Error(\"caveman agent: value is not canonically serializable\");\n    return encoded;\n  }\n  if (Array.isArray(value)) return `[${value.map(stableStringify).join(\",\")}]`;\n  const object = value as Record<string, unknown>;\n  return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(object[key])}`).join(\",\")}}`;\n}\n\nexport function sha256(value: Uint8Array | string): string {\n  return createHash(\"sha256\").update(value).digest(\"hex\");\n}\n\nexport function opaquePayload(input: Uint8Array): boolean {\n  const value = new TextDecoder().decode(input).trim();\n  if (/^[-]{5}BEGIN (?:PGP SIGNED MESSAGE|[A-Z ]+ SIGNATURE)[-]{5}/.test(value) ||\n      /^[A-Za-z0-9_-]{16,}\\.[A-Za-z0-9_-]{16,}\\.[A-Za-z0-9_-]{16,}$/.test(value) ||\n      /\\b(?:x-amz-signature|signature)=([0-9a-f]{32,}|[A-Za-z0-9_-]{32,})\\b/i.test(value)) {\n    return true;\n  }","sourceCodeStart":319,"sourceCodeEnd":355,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/context-ir.ts#L319-L355","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\naddSegment({ body: { userId: 9007199254740993n } }); // bigint\n\n// after\naddSegment({ body: { userId: \"9007199254740993\" } }); // string","handlingStrategy":"validation","validationCode":"function isCanonicallySerializable(v: unknown): boolean {\n  if (v === undefined || typeof v === \"function\" || typeof v === \"symbol\" || typeof v === \"bigint\") return false;\n  if (v === null || typeof v !== \"object\") return true;\n  if (Array.isArray(v)) return v.every(isCanonicallySerializable);\n  return Object.values(v).every(isCanonicallySerializable);\n}","typeGuard":"function isPlainJSON(v: unknown): boolean {\n  if (typeof v === \"bigint\" || typeof v === \"function\" || typeof v === \"symbol\" || v === undefined) return false;\n  if (v === null || typeof v !== \"object\") return true;\n  if (Array.isArray(v)) return v.every(isPlainJSON);\n  const proto = Object.getPrototypeOf(v);\n  if (proto !== Object.prototype && proto !== null) return false;\n  return Object.entries(v).every(([k, val]) => typeof k === \"string\" && isPlainJSON(val));\n}","tryCatchPattern":null,"preventionTips":["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"],"tags":["serialization","bigint","provenance","validation"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}