{"record":{"id":"d9b9d0eaa74a7644","repo":"thedotmack/claude-mem","slug":"syncapply-invalid-op-seq-op-seq-kind-op-kind-origin-op-d9b9d0","errorCode":null,"errorMessage":"SyncApply: invalid op seq=${op.seq} kind=${op.kind} origin=${op.origin_device}/${op.origin_id}: field ${key} must be a finite number, got ${typeof v}","messagePattern":"SyncApply: invalid op seq=(.+?) kind=(.+?) origin=(.+?)/(.+?): field (.+?) must be a finite number, got (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/services/sync/SyncApply.ts","lineNumber":303,"sourceCode":"\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  }\n}\n\nexport class SyncApply {\n  private readonly db: Database;\n  private readonly deviceId: string;\n  private readonly chromaSync: ChromaSyncLike | null;\n  private readonly now: () => number;","sourceCodeStart":285,"sourceCodeEnd":321,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/src/services/sync/SyncApply.ts#L285-L321","documentation":"fieldNumber() validates numeric sync-op body fields (created_at_epoch, prompt_number, etc.). It throws when the field is present but not a finite number — strings, NaN, Infinity, booleans, or objects are all rejected. This keeps SQLite numeric columns and downstream date math free of garbage.","triggerScenarios":"A sync op body carries created_at_epoch or prompt_number as a string like \"1712345678\" (common with JSON produced by weakly-typed languages), or as NaN/Infinity that serialized to null-ish/invalid JSON, or as a boolean.","commonSituations":"Producers in dynamically typed languages emit epoch timestamps as strings; NaN leaked into a body and was serialized as a non-number; a version change renamed a field so a number lands where the wrong key was read.","solutions":["Fix the op body so the field is a real JSON number (remove quotes around the value).","On the producer, coerce with Number(v) and check Number.isFinite(v) before building the body.","If the value is genuinely absent, omit the key or set null — fieldNumber treats missing as null rather than throwing.","Re-derive created_at from created_at string if present: body.created_at_epoch = Date.parse(body.created_at)."],"exampleFix":"// before\n{ \"created_at_epoch\": \"1712345678000\" }\n// after\n{ \"created_at_epoch\": 1712345678000 }","handlingStrategy":"validation","validationCode":"function isValidSyncBodyNumber(body) {\n  const KEYS = ['created_at_epoch','prompt_number'];\n  return KEYS.every(k => !(k in body) || body[k] === null || (typeof body[k] === 'number' && Number.isFinite(body[k])));\n}","typeGuard":"function isFiniteNumberOrNull(v) { return v === undefined || v === null || (typeof v === 'number' && Number.isFinite(v)); }","tryCatchPattern":"try {\n  applySyncOp(op);\n} catch (e) {\n  if (String(e.message).includes('must be a finite number')) {\n    deadLetterQueue.push(op); // re-emit with corrected numeric field\n  } else throw e;\n}","preventionTips":["Use Number(v) and Number.isFinite(v) checks on the producer before enqueueing","Never stringify epoch timestamps — keep them as JSON numbers","Guard against NaN/Infinity reaching JSON payloads"],"tags":["sync","validation","numeric"],"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"}