{"record":{"id":"9b42e435dbd47489","repo":"thedotmack/claude-mem","slug":"cloud-sync-canonical-payload-name-must-decode","errorCode":null,"errorMessage":"cloud sync canonical payload: ${name} must decode to an array","messagePattern":"cloud sync canonical payload: (.+?) must decode to an array","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/services/sync/CloudSync.ts","lineNumber":167,"sourceCode":"    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`);\n  }\n  return parsed;\n}\n\nconst KINDS: KindSpec[] = [\n  {\n    kind: 'observation',\n    localTable: 'observations',\n    selectSql: `\n      SELECT CAST(id AS TEXT) AS id, CAST(sync_rev AS TEXT) AS sync_rev,\n        memory_session_id, project, text, type, title, subtitle,\n        facts, narrative, concepts, files_read, files_modified, prompt_number,\n        discovery_tokens, content_hash, generated_by_model, agent_type, agent_id,\n        metadata, merged_into_project, created_at, created_at_epoch\n      FROM observations","sourceCodeStart":149,"sourceCodeEnd":185,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/e2d1df569a8f04075d40e92461128ece7cf04c82/src/services/sync/CloudSync.ts#L149-L185","documentation":"The column parsed as JSON successfully, but the decoded value is not an array even though the kind's op-body contract (facts, concepts, files_read, files_modified) requires one — for example '{}' or '\"text\"' stored in files_read. Canonicalization aborts with the column name so the hub never receives a shape-varying field.","triggerScenarios":"An older writer version stored objects or plain strings in these columns; a merge/migration path wrote a dict keyed by id instead of a list; a default value changed shape between versions.","commonSituations":"Upgrades between writer generations with different array conventions; imports from tools that emit key-maps; hand-written seed data using objects for brevity.","solutions":["Find shape mismatches: SELECT id FROM <table> WHERE <col> IS NOT NULL AND json_type(<col>) <> 'array'","Convert offenders — wrap single values, drop maps to values, or reset to '[]': UPDATE <table> SET <col> = '[]' WHERE json_type(<col>) <> 'array'","Re-run the sync push","Fix the writer to always serialize these fields as arrays"],"exampleFix":"-- before\nSELECT id FROM observations WHERE concepts IS NOT NULL AND json_type(concepts) <> 'array';\n-- after\nUPDATE observations SET concepts = '[]'\nWHERE concepts IS NOT NULL AND json_type(concepts) <> 'array';","handlingStrategy":"validation","validationCode":"-- Pre-sync check: array-contract columns decode to arrays\nSELECT COUNT(*) AS bad FROM observations\n WHERE (facts IS NOT NULL AND json_type(facts) <> 'array')\n    OR (files_read IS NOT NULL AND json_type(files_read) <> 'array');\n-- bad = 0 before enabling cloud sync","typeGuard":"const decodesToArray = (v: unknown): v is string =>\n  typeof v === 'string' && (() => { try { return Array.isArray(JSON.parse(v)); } catch { return false; } })();","tryCatchPattern":"try {\n  await cloudSync.push();\n} catch (e) {\n  if (e instanceof Error && e.message.includes('must decode to an array')) {\n    // normalize the named column to '[]' (or a real array), then re-push\n  } else throw e;\n}","preventionTips":["Pin one array shape for these fields across all writer versions","Add json_type() assertions to migrations between writer generations","Validate imported data with json_type() before it lands","Keep fixture/seed data shape-checked in CI"],"tags":["cloud-sync","payload-validation","json","shape-mismatch","sqlite"],"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-14T05:17:10.506Z"}