{"record":{"id":"428bed1de42795ed","repo":"thedotmack/claude-mem","slug":"syncapply-invalid-op-seq-op-seq-kind-op-kind-origin-op-428bed","errorCode":null,"errorMessage":"SyncApply: invalid op seq=${op.seq} kind=${op.kind} origin=${op.origin_device}/${op.origin_id}: summary body requires memory_session_id, project, created_at_epoch","messagePattern":"SyncApply: invalid op seq=(.+?) kind=(.+?) origin=(.+?)/(.+?): summary body requires memory_session_id, project, created_at_epoch","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/services/sync/SyncApply.ts","lineNumber":798,"sourceCode":"          facts: parseListColumn(body.facts),\n          narrative: fieldString(op, body, 'narrative'),\n          concepts: parseListColumn(body.concepts),\n          files_read: parseListColumn(body.files_read),\n          files_modified: parseListColumn(body.files_modified),\n        },\n        fieldNumber(op, body, 'prompt_number') ?? 0,\n        createdAtEpoch\n      ));\n    }\n    return 'applied';\n  }\n\n  private applySummary(op: SyncOp, body: Record<string, unknown>, chromaJobs: ChromaJob[]): 'applied' | 'stale' {\n    const memorySessionId = fieldString(op, body, 'memory_session_id');\n    const project = fieldString(op, body, 'project');\n    const createdAtEpoch = fieldNumber(op, body, 'created_at_epoch');\n    if (!memorySessionId || !project || createdAtEpoch === null) {\n      throw invalidOp(op, 'summary body requires memory_session_id, project, created_at_epoch');\n    }\n    const createdAt = fieldString(op, body, 'created_at') ?? new Date(createdAtEpoch).toISOString();\n\n    const existing = this.findByOrigin('session_summaries', op.origin_device, op.origin_id);\n    if (existing) {\n      if (compareCanonicalDecimals(op.rev, existing.sync_rev) <= 0) return 'stale';\n      this.ensureSessionForMemoryId(memorySessionId, project, createdAtEpoch); // FK holds even if the body re-homed the row\n      this.db.prepare(`\n        UPDATE session_summaries SET\n          memory_session_id = ?, project = ?, request = ?, investigated = ?, learned = ?, completed = ?,\n          next_steps = ?, files_read = ?, files_edited = ?, notes = ?, prompt_number = ?,\n          discovery_tokens = ?, merged_into_project = ?, created_at = ?, created_at_epoch = ?,\n          sync_rev = ?, synced_at = ?\n        WHERE id = ?\n      `).run(\n        memorySessionId, project, fieldString(op, body, 'request'), fieldString(op, body, 'investigated'), fieldString(op, body, 'learned'), fieldString(op, body, 'completed'),\n        fieldString(op, body, 'next_steps'), fieldString(op, body, 'files_read'), fieldString(op, body, 'files_edited'), fieldString(op, body, 'notes'), fieldNumber(op, body, 'prompt_number'),\n        fieldNumber(op, body, 'discovery_tokens') ?? 0, fieldString(op, body, 'merged_into_project'), createdAt, createdAtEpoch,","sourceCodeStart":780,"sourceCodeEnd":816,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/src/services/sync/SyncApply.ts#L780-L816","documentation":"applySummary requires memory_session_id, project, and created_at_epoch in the summary body. A summary is always attached to a session and project for querying and Chroma indexing; without these the row would be an orphan, so the op is rejected.","triggerScenarios":"A summary sync op body lacks memory_session_id or project, sends empty strings, or omits created_at_epoch (null) with no numeric value present.","commonSituations":"A summarizer job ran after a session was evicted and couldn't resolve its id; a producer refactor renamed memory_session_id; summaries generated offline without project context were synced as-is.","solutions":["Fix the producer to attach memory_session_id, project, and a finite created_at_epoch to every summary body.","On the producer, resolve the session before generating the summary; if the session is gone, skip the summary instead of enqueueing it.","Validate summary payloads pre-enqueue (required keys + types) so bad ops never enter the sync log.","If project context is genuinely unknown, backfill a default project label rather than sending an empty string."],"exampleFix":"// before\nbody: { summary: '...' }\n// after\nbody: { summary: '...', memory_session_id: sessionId, project: 'my-project', created_at_epoch: Date.now() }","handlingStrategy":"validation","validationCode":"function isValidSummaryBody(b) {\n  return typeof b.memory_session_id === 'string' && b.memory_session_id.length > 0\n    && typeof b.project === 'string' && b.project.length > 0\n    && typeof b.created_at_epoch === 'number' && Number.isFinite(b.created_at_epoch);\n}","typeGuard":"function hasRequiredSummaryFields(b) {\n  return b != null && 'memory_session_id' in b && 'project' in b && 'created_at_epoch' in b;\n}","tryCatchPattern":"try {\n  applySyncOp(op);\n} catch (e) {\n  if (String(e.message).includes('summary body requires')) {\n    deadLetterQueue.push(op); // re-emit with session/project attached\n  } else throw e;\n}","preventionTips":["Generate summaries only while the session context is still resolvable","Validate summary payloads pre-enqueue","Backfill a project label instead of syncing without one"],"tags":["sync","validation","missing-required-field"],"backgroundTag":"missing-required-argument","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"}