thedotmack/claude-mem · error · Error
canonical content
Error message
canonical content: ${message} What it means
Internal helper `invalid()` in canonical-content.ts throws this for any violation of the canonical-content rules: non-RFC-8259 JSON values, unsorted object keys, invalid decimal forms, or malformed operation envelopes. Nearly every exported function of the module (canonicalJson, stableDocumentId, wrapCanonicalBody, parseCanonicalOperation, decimal helpers) funnels through it, so the message names the specific invariant that failed.
Solutions
- Read the trailing `${message}` in the error to identify which invariant failed (key order, decimal form, missing field).
- Pass raw data through `canonicalJson()`/`normalizeJson()` before hashing or building operation payloads.
- Represent numeric values as canonical decimal strings (no leading zeros, explicit fraction/exponent rules), never raw JS numbers.
- Validate operation envelopes against the required-field list (`payload_sha256`, etc.) before `parseCanonicalOperation`.
Example fix
// before const id = stableDocumentId(userDoc); // throws: canonical content: keys not sorted // after const canonical = canonicalJson(userDoc); const id = stableDocumentId(JSON.parse(canonical)); // normalized first
Defensive patterns
Strategy: validation
Validate before calling
// validate inputs before calling canonical-content APIs
function assertCanonicalizable(value: unknown): void {
if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
throw new Error('value is not RFC-8259 JSON serializable');
}
if (typeof value === 'number' && (!Number.isFinite(value) || Object.is(value, -0))) {
throw new Error('numbers must be finite and not -0');
}
}
function assertDecimalString(s: string): void {
if (!/^-?(0|[1-9]\d*)(\.\d+)?$/.test(s)) throw new Error(`non-canonical decimal: ${s}`);
} Type guard
function isPlainJsonObject(v: unknown): v is Record<string, unknown> {
return typeof v === 'object' && v !== null && !Array.isArray(v) &&
Object.getPrototypeOf(v) === Object.prototype;
} Try / catch
try {
const canonical = canonicalJson(rawDocument);
} catch (err) {
if (err instanceof Error && err.message.startsWith('canonical content: ')) {
console.error(`document rejected by canonicalizer: ${err.message}`);
// fall back to re-serializing through JSON.parse(JSON.stringify(...)) then retry once
} else throw err;
} Prevention
- Always route external JSON through normalizeJson/canonicalJson before hashing or ID generation.
- Store numbers as canonical decimal strings, never raw JS numbers.
- Round-trip persisted documents through the canonicalizer in a migration test when upgrading the library.
- Unit-test each invariant (key order, decimal form, required fields) with negative cases.
When it happens
Trigger: Calling `canonicalJson` with a value JSON.stringify can't represent deterministically (undefined, functions, cycles via normalizeJson's checks), passing a decimal string with leading zeros or non-canonical exponent form to `assertCanonicalDecimal`/`incrementCanonicalDecimal`, or feeding `parseCanonicalOperation` an object missing required fields like `payload_sha256`.
Common situations: Feeding user-supplied JSON that hasn't been normalized (duplicate/unsorted keys, -0, non-finite numbers); hand-built operations that skip canonicalization before hashing; upgrading the library and old persisted documents no longer satisfy stricter canonical rules; accidentally passing a JS number where a canonical decimal string is expected.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- canonical content
- CLAUDE_MEM_TELEGRAM_WRAPUP_ROUTES must be a JSON object
- cloud sync canonical payload
- Adapter rejected input
- agent_event source_id must belong to project_id and team_id
AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17).
Data as JSON: /api/errors/28a5390ddd6c92da.
Report an issue: GitHub.
Appendix: source
Thrown at workers/sync-hub/src/canonical-content.ts:59
const ENVELOPE_KEYS = [
"body_schema_version",
"deleted",
"deleted_at",
"entity_rev",
"id",
"kind",
"mutation",
"origin_device_id",
"origin_local_id",
"payload",
"payload_schema_version",
"payload_sha256",
] as const;
const encoder = new TextEncoder();
function invalid(message: string): never {
throw new Error(`canonical content: ${message}`);
}
/** RFC-8259 JSON with recursively sorted object keys and preserved array order. */
export function canonicalJson(value: unknown): string {
return JSON.stringify(normalizeJson(value, new Set()));
}
function normalizeJson(value: unknown, seen: Set<object>): unknown {
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
if (typeof value === "number") {
if (!Number.isFinite(value)) invalid("numbers must be finite");
if (!Number.isSafeInteger(value) && Number.isInteger(value)) {
invalid("integers must be safe; use decimal strings for uint64 values");
}
if (Object.is(value, -0)) return 0;
return value;
}
if (typeof value !== "object") invalid(`unsupported JSON value ${typeof value}`);View on GitHub (pinned to d8bc9755e7)