{"record":{"id":"abaab51d1a833145","repo":"thedotmack/claude-mem","slug":"syncapply-invalid-op-seq-op-seq-kind-op-kind-origin-op-abaab5","errorCode":null,"errorMessage":"SyncApply: invalid op seq=${op.seq} kind=${op.kind} origin=${op.origin_device}/${op.origin_id}: observation body requires memory_session_id, project, type, created_at_epoch","messagePattern":"SyncApply: invalid op seq=(.+?) kind=(.+?) origin=(.+?)/(.+?): observation body requires memory_session_id, project, type, created_at_epoch","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/services/sync/SyncApply.ts","lineNumber":711,"sourceCode":"        throw new Error(`SyncApply: could not create or adopt a session for memory_session_id=${memorySessionId}`);\n      }\n      sessionId = adopted.id;\n    }\n\n    this.claimParkedTitle(sessionId, SyncApply.parkedTitleMemKey(memorySessionId));\n    if (contentSessionId) {\n      this.claimParkedTitle(sessionId, SyncApply.parkedTitleContentKey(platform, contentSessionId));\n    }\n    return sessionId;\n  }\n\n  private applyObservation(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 type = fieldString(op, body, 'type');\n    const createdAtEpoch = fieldNumber(op, body, 'created_at_epoch');\n    if (!memorySessionId || !project || !type || createdAtEpoch === null) {\n      throw invalidOp(op, 'observation body requires memory_session_id, project, type, created_at_epoch');\n    }\n    const createdAt = fieldString(op, body, 'created_at') ?? new Date(createdAtEpoch).toISOString();\n\n    const existing = this.findByOrigin('observations', 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 observations SET\n          memory_session_id = ?, project = ?, text = ?, type = ?, title = ?, subtitle = ?,\n          facts = ?, narrative = ?, concepts = ?, files_read = ?, files_modified = ?,\n          prompt_number = ?, discovery_tokens = ?, content_hash = ?, generated_by_model = ?,\n          agent_type = ?, agent_id = ?, metadata = ?, merged_into_project = ?,\n          created_at = ?, created_at_epoch = ?, sync_rev = ?, synced_at = ?\n        WHERE id = ?\n      `).run(\n        memorySessionId, project, fieldString(op, body, 'text'), type, fieldString(op, body, 'title'), fieldString(op, body, 'subtitle'),\n        fieldString(op, body, 'facts'), fieldString(op, body, 'narrative'), fieldString(op, body, 'concepts'), fieldString(op, body, 'files_read'), fieldString(op, body, 'files_modified'),","sourceCodeStart":693,"sourceCodeEnd":729,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/src/services/sync/SyncApply.ts#L693-L729","documentation":"applyObservation requires the four columns the observations table treats as NOT NULL/essential: memory_session_id, project, type, and created_at_epoch. If any is missing, null, or empty (and no fallback can fill it), the op is rejected rather than inserting an orphan observation that downstream queries and Chroma embedding jobs can't associate.","triggerScenarios":"An observation sync op body omits one of the required keys, sends them as null/empty strings, or sends created_at_epoch as null while also omitting created_at.","commonSituations":"A producer that records observations before a session/project context is established sends empty project; a schema change renamed created_at_epoch on the writer; a partial serialization skipped optional-looking fields that are actually required.","solutions":["Fix the producer to include memory_session_id, project, type, and created_at_epoch (a number) in every observation body.","Provide created_at (ISO string) as a fallback only if created_at_epoch must be derived — but note created_at_epoch itself is still required.","Reject or queue such observations on the producer side before enqueueing, validating required keys.","If the context genuinely doesn't exist yet, defer creating the observation until the session/project is known."],"exampleFix":"// before\nbody: { type: 'note', content: '...' }\n// after\nbody: { type: 'note', content: '...', memory_session_id: sessionId, project: 'my-project', created_at_epoch: Date.now() }","handlingStrategy":"validation","validationCode":"function isValidObservationBody(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.type === 'string' && b.type.length > 0\n    && typeof b.created_at_epoch === 'number' && Number.isFinite(b.created_at_epoch);\n}","typeGuard":"function hasRequiredObservationFields(b) {\n  return b != null && 'memory_session_id' in b && 'project' in b && 'type' in b && 'created_at_epoch' in b;\n}","tryCatchPattern":"try {\n  applySyncOp(op);\n} catch (e) {\n  if (String(e.message).includes('observation body requires')) {\n    deadLetterQueue.push(op); // backfill required fields and re-emit\n  } else throw e;\n}","preventionTips":["Validate observation payloads against a schema before enqueueing","Never start an observation before session/project context is resolved","Treat created_at_epoch as mandatory even when created_at is provided"],"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"}