thedotmack/claude-mem · error · Error

SyncApply: invalid op seq=

Error message

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

What it means

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.

Solutions

  1. Fix the producer to include memory_session_id, project, type, and created_at_epoch (a number) in every observation body.
  2. 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.
  3. Reject or queue such observations on the producer side before enqueueing, validating required keys.
  4. If the context genuinely doesn't exist yet, defer creating the observation until the session/project is known.

Example fix

// before
body: { type: 'note', content: '...' }
// after
body: { type: 'note', content: '...', memory_session_id: sessionId, project: 'my-project', created_at_epoch: Date.now() }
Defensive patterns

Strategy: validation

Validate before calling

function isValidObservationBody(b) {
  return typeof b.memory_session_id === 'string' && b.memory_session_id.length > 0
    && typeof b.project === 'string' && b.project.length > 0
    && typeof b.type === 'string' && b.type.length > 0
    && typeof b.created_at_epoch === 'number' && Number.isFinite(b.created_at_epoch);
}

Type guard

function hasRequiredObservationFields(b) {
  return b != null && 'memory_session_id' in b && 'project' in b && 'type' in b && 'created_at_epoch' in b;
}

Try / catch

try {
  applySyncOp(op);
} catch (e) {
  if (String(e.message).includes('observation body requires')) {
    deadLetterQueue.push(op); // backfill required fields and re-emit
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17). Data as JSON: /api/errors/abaab51d1a833145. Report an issue: GitHub.

Appendix: source

Thrown at src/services/sync/SyncApply.ts:711

        throw new Error(`SyncApply: could not create or adopt a session for memory_session_id=${memorySessionId}`);
      }
      sessionId = adopted.id;
    }

    this.claimParkedTitle(sessionId, SyncApply.parkedTitleMemKey(memorySessionId));
    if (contentSessionId) {
      this.claimParkedTitle(sessionId, SyncApply.parkedTitleContentKey(platform, contentSessionId));
    }
    return sessionId;
  }

  private applyObservation(op: SyncOp, body: Record<string, unknown>, chromaJobs: ChromaJob[]): 'applied' | 'stale' {
    const memorySessionId = fieldString(op, body, 'memory_session_id');
    const project = fieldString(op, body, 'project');
    const type = fieldString(op, body, 'type');
    const createdAtEpoch = fieldNumber(op, body, 'created_at_epoch');
    if (!memorySessionId || !project || !type || createdAtEpoch === null) {
      throw invalidOp(op, 'observation body requires memory_session_id, project, type, created_at_epoch');
    }
    const createdAt = fieldString(op, body, 'created_at') ?? new Date(createdAtEpoch).toISOString();

    const existing = this.findByOrigin('observations', op.origin_device, op.origin_id);
    if (existing) {
      if (compareCanonicalDecimals(op.rev, existing.sync_rev) <= 0) return 'stale';
      this.ensureSessionForMemoryId(memorySessionId, project, createdAtEpoch); // FK holds even if the body re-homed the row
      this.db.prepare(`
        UPDATE observations SET
          memory_session_id = ?, project = ?, text = ?, type = ?, title = ?, subtitle = ?,
          facts = ?, narrative = ?, concepts = ?, files_read = ?, files_modified = ?,
          prompt_number = ?, discovery_tokens = ?, content_hash = ?, generated_by_model = ?,
          agent_type = ?, agent_id = ?, metadata = ?, merged_into_project = ?,
          created_at = ?, created_at_epoch = ?, sync_rev = ?, synced_at = ?
        WHERE id = ?
      `).run(
        memorySessionId, project, fieldString(op, body, 'text'), type, fieldString(op, body, 'title'), fieldString(op, body, 'subtitle'),
        fieldString(op, body, 'facts'), fieldString(op, body, 'narrative'), fieldString(op, body, 'concepts'), fieldString(op, body, 'files_read'), fieldString(op, body, 'files_modified'),

View on GitHub (pinned to d8bc9755e7)