{"record":{"id":"27279dfe1cd3e791","repo":"thedotmack/claude-mem","slug":"syncapply-invalid-op-seq-op-seq-kind-op-kind-origin-op","errorCode":null,"errorMessage":"SyncApply: invalid op seq=${op.seq} kind=${op.kind} origin=${op.origin_device}/${op.origin_id}: field ${key} must be a string, got ${typeof v}","messagePattern":"SyncApply: invalid op seq=(.+?) kind=(.+?) origin=(.+?)/(.+?): field (.+?) must be a string, got (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/services/sync/SyncApply.ts","lineNumber":296,"sourceCode":"}\n\ntype ChromaJob = () => Promise<void>;\n\nfunction invalidOp(op: SyncOp, message: string): Error {\n  return new Error(`SyncApply: invalid op seq=${op.seq} kind=${op.kind} origin=${op.origin_device}/${op.origin_id}: ${message}`);\n}\n\n/**\n * Typed field readers — loud, not lossy: a MISSING (or null) field is\n * tolerated as null (required-ness is enforced by the per-kind checks), but\n * a field that is PRESENT with the wrong type is a malformed body and throws,\n * failing the batch instead of silently writing NULL.\n */\nfunction fieldString(op: SyncOp, obj: Record<string, unknown>, key: string): string | null {\n  const v = obj[key];\n  if (v === undefined || v === null) return null;\n  if (typeof v === 'string') return v;\n  throw invalidOp(op, `field ${key} must be a string, got ${typeof v}`);\n}\n\nfunction fieldNumber(op: SyncOp, obj: Record<string, unknown>, key: string): number | null {\n  const v = obj[key];\n  if (v === undefined || v === null) return null;\n  if (typeof v === 'number' && Number.isFinite(v)) return v;\n  throw invalidOp(op, `field ${key} must be a finite number, got ${typeof v}`);\n}\n\n/** Parse a JSON-string list column for Chroma; never throws. */\nfunction parseListColumn(v: unknown): string[] {\n  if (typeof v !== 'string') return [];\n  try {\n    const parsed = JSON.parse(v);\n    return Array.isArray(parsed) ? parsed.map(String) : [];\n  } catch {\n    return [];\n  }","sourceCodeStart":278,"sourceCodeEnd":314,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/src/services/sync/SyncApply.ts#L278-L314","documentation":"SyncApply validates every field of an incoming sync op body before writing it to SQLite. fieldString() throws when a column that must be a string (or absent/null) contains a value of some other type. Failing the whole batch prevents silently coercing garbage (e.g. numbers, objects) into NULL or text columns.","triggerScenarios":"A sync op body JSON has a field like memory_session_id, project, type, created_at, or content_session_id present but typed as a number, boolean, array, or nested object instead of a string.","commonSituations":"A producer device serializes created_at as an epoch number instead of an ISO string; a schema change on the writer side emits project as null-containing nested object; hand-crafted or migrated sync payloads with wrong field types are replayed into the queue.","solutions":["Inspect the op body at seq=${op.seq} and fix the offending field to be a JSON string or remove it (null/undefined is allowed).","Fix the producer that built the op body so string columns are serialized as strings (e.g. String(v) or ISO date formatting).","If a schema version skew caused the type change, re-sync the affected entity from the origin device with a corrected canonical body.","Add a pre-enqueue validation on the writer side that rejects non-string values for string columns before the op enters the sync log."],"exampleFix":"// before (producer)\nbody.created_at_epoch_ms = Date.now();\nbody.created_at = Date.now();\n// after\nbody.created_at_epoch = Date.now();\nbody.created_at = new Date().toISOString();","handlingStrategy":"validation","validationCode":"function isValidSyncBodyString(body) {\n  const KEYS = ['memory_session_id','project','type','content_session_id','created_at'];\n  return KEYS.every(k => !(k in body) || body[k] === null || typeof body[k] === 'string');\n}","typeGuard":"function isStringOrNull(v) { return v === undefined || v === null || typeof v === 'string'; }","tryCatchPattern":"try {\n  applySyncOp(op);\n} catch (e) {\n  if (String(e.message).includes('must be a string')) {\n    logger.warn({ seq: op.seq, err: e.message }, 'skipping malformed sync op');\n  } else throw e;\n}","preventionTips":["Serialize timestamps as ISO strings for *_at fields and epochs as numbers for *_epoch fields","Validate op bodies against a schema (zod/JSON Schema) before enqueueing","Never let raw non-JSON-typed values flow into body fields without coercion"],"tags":["sync","validation","type-mismatch"],"backgroundTag":"type-mismatch","analyzedSha":"d8bc9755e74915e5c3b999181e10a67c889bce2a","analyzedAt":"2026-09-17T16:40:26.182Z","contentChangedAt":"2026-09-17T16:40:26.182Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}