{"record":{"id":"d47cf41298560496","repo":"thedotmack/claude-mem","slug":"cloud-sync-canonical-payload-name-must-be-a-no","errorCode":null,"errorMessage":"cloud sync canonical payload: ${name} must be a non-negative safe integer","messagePattern":"cloud sync canonical payload: (.+?) must be a non-negative safe integer","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/services/sync/CloudSync.ts","lineNumber":152,"sourceCode":"   * Drain SELECT: unsynced NATIVE rows only. Replica rows (origin_device_id\n   * NOT NULL) are another device's corpus — pushing them under this device's\n   * identity would fork origin attribution, so they are excluded here even\n   * if something re-nulls their synced_at.\n   */\n  selectSql: string;\n  /** Exact current-row read used by ack drift reconciliation. */\n  selectOneSql: string;\n  /** Op body per the SyncApply BODY FIELD MAPPING — values exactly as stored. */\n  toBody: (r: LocalRow) => OpBody;\n}\n\nfunction decimalPayload(value: unknown, name: string, nullable = false): string | null {\n  if (value === null || value === undefined) {\n    if (nullable) return null;\n    return '0';\n  }\n  if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {\n    throw new Error(`cloud sync canonical payload: ${name} must be a non-negative safe integer`);\n  }\n  return String(value);\n}\n\nfunction jsonPayloadColumn(value: unknown, name: string, expected: 'array' | 'object'): unknown {\n  if (value === null || value === undefined) return null;\n  if (typeof value !== 'string') {\n    throw new Error(`cloud sync canonical payload: ${name} must be stored JSON text`);\n  }\n  let parsed: unknown;\n  try { parsed = JSON.parse(value); } catch {\n    throw new Error(`cloud sync canonical payload: ${name} is not valid JSON`);\n  }\n  if (expected === 'array' && !Array.isArray(parsed)) {\n    throw new Error(`cloud sync canonical payload: ${name} must decode to an array`);\n  }\n  if (expected === 'object' && (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))) {\n    throw new Error(`cloud sync canonical payload: ${name} must decode to an object`);","sourceCodeStart":134,"sourceCodeEnd":170,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/e2d1df569a8f04075d40e92461128ece7cf04c82/src/services/sync/CloudSync.ts#L134-L170","documentation":"decimalPayload builds canonical decimal-string fields of the sync op body from local SQLite columns (prompt_number, discovery_tokens, created_at_epoch). It throws when a value is present but is not a JS number, is not a safe integer, or is negative — the hub's wire format requires non-negative integer decimal strings, so a bad value means a local row drifted from the schema contract and the push aborts rather than emitting a malformed op.","triggerScenarios":"A column such as created_at_epoch stored as TEXT or REAL (schema drift, manual edit, different writer version); a negative prompt_number inserted by a bug or hand-written SQL; a fractional epoch written by an older build.","commonSituations":"DB migrated or copied between versions with different column affinities; hand-edited SQLite; an older writer storing epoch in fractional seconds; a downstream tool writing raw values without validation.","solutions":["Run the diagnostic for the column named in the message: SELECT id FROM <table> WHERE typeof(<col>) <> 'integer' OR <col> < 0","Repair the rows: UPDATE <table> SET <col> = CAST(<col> AS INTEGER) WHERE typeof(<col>) <> 'integer', and fix any negative values to their intended non-negative form","Re-run the sync push — it drains from where it stopped","Fix the writer that produced floats/text/negatives so it stores non-negative integers"],"exampleFix":"-- before\nSELECT id FROM observations WHERE typeof(created_at_epoch) <> 'integer' OR created_at_epoch < 0;\n-- after\nUPDATE observations SET created_at_epoch = CAST(created_at_epoch AS INTEGER)\nWHERE typeof(created_at_epoch) <> 'integer' AND created_at_epoch >= 0;","handlingStrategy":"validation","validationCode":"-- Pre-sync check: no numeric-contract violations\nSELECT 'observations' AS tbl, COUNT(*) AS bad FROM observations\n WHERE (discovery_tokens IS NOT NULL AND (typeof(discovery_tokens) <> 'integer' OR discovery_tokens < 0))\n    OR (created_at_epoch IS NOT NULL AND (typeof(created_at_epoch) <> 'integer' OR created_at_epoch < 0));\n-- bad = 0 before enabling cloud sync","typeGuard":"const isNonNegativeSafeInteger = (v: unknown): v is number =>\n  typeof v === 'number' && Number.isSafeInteger(v) && v >= 0;","tryCatchPattern":"try {\n  await cloudSync.push();\n} catch (e) {\n  if (e instanceof Error && e.message.includes('non-negative safe integer')) {\n    // message names the column; quarantine/repair those rows, then re-push\n  } else throw e;\n}","preventionTips":["Write these columns only through code that coerces to non-negative integers","Add typeof() checks to migrations and seed scripts","Never hand-edit numeric sync columns in SQLite","Run the pre-sync diagnostic query before enabling sync on an old DB"],"tags":["cloud-sync","payload-validation","sqlite","data-integrity","schema-drift"],"backgroundTag":"payload-validation-failed","analyzedSha":"e2d1df569a8f04075d40e92461128ece7cf04c82","analyzedAt":"2026-08-20T23:58:13.836Z","contentChangedAt":"2026-08-20T23:58:13.836Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}