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}: set_title target requires memory_session_id or content_session_id

What it means

applySetTitle needs to know which session row to update. It first tries target.memory_session_id; if absent, it falls back to target.content_session_id. When neither identifier is present the op has no addressable target, so invalidOp is thrown rather than attempting a scoped or unbounded update.

Solutions

  1. Include target.memory_session_id (preferred) or target.content_session_id in every set_title op
  2. Confirm the producer resolves the session id before enqueueing the op
  3. Check for op producers using a stale key name and update them to the supported identifiers

Example fix

// before
target: { platform_source: 'claude-code' }
// after
target: { memory_session_id: 'mem-123', platform_source: 'claude-code' }
Defensive patterns

Strategy: validation

Validate before calling

function hasSessionTarget(target: Record<string, unknown>): boolean {
  return typeof target.memory_session_id === 'string' || typeof target.content_session_id === 'string';
}
if (!hasSessionTarget(op.body.target)) throw new Error('set_title target needs memory_session_id or content_session_id');

Type guard

const hasSessionId = (t: unknown): t is { memory_session_id?: string; content_session_id?: string } & Record<string, unknown> =>
  typeof t === 'object' && t !== null && ('memory_session_id' in t || 'content_session_id' in t);

Try / catch

try { apply(op); } catch (e) {
  if (e instanceof InvalidOpError && e.reason.includes('memory_session_id or content_session_id')) {
    // re-resolve the session id and re-emit the op
  } else throw e;
}

Prevention

When it happens

Trigger: A set_title op whose target object has neither memory_session_id nor content_session_id — e.g. target only carries platform_source, or is an empty object.

Common situations: A producer that targets sessions by a different key (session_uuid, external id) after a schema change; a hand-built op in tooling/tests that omitted the id; a truncation bug dropping the id field.

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/c35081f12c1f58d2. Report an issue: GitHub.

Appendix: source

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

    // apply-vs-park. SELECTs in our transaction are authoritative.
    const memorySessionId = fieldString(op, target, 'memory_session_id');
    if (memorySessionId) {
      const session = this.db.prepare(
        'SELECT id FROM sdk_sessions WHERE memory_session_id = ?'
      ).get(memorySessionId) as { id: number } | undefined;
      if (session) {
        this.db.prepare('UPDATE sdk_sessions SET custom_title = ? WHERE id = ?')
          .run(customTitle, session.id);
        return 'applied';
      }
      // No such session yet — park until ensureSessionForMemoryId creates it.
      this.parkTitle(SyncApply.parkedTitleMemKey(memorySessionId), customTitle);
      return 'applied';
    }

    const contentSessionId = fieldString(op, target, 'content_session_id');
    if (!contentSessionId) {
      throw invalidOp(op, 'set_title target requires memory_session_id or content_session_id');
    }
    const platform = normalizePlatformSource(fieldString(op, target, 'platform_source') ?? undefined);

    // (1) Direct match on the session identity.
    const direct = this.db.prepare(`
      SELECT id FROM sdk_sessions
      WHERE COALESCE(NULLIF(platform_source, ''), ?) = ? AND content_session_id = ?
      LIMIT 1
    `).get(DEFAULT_PLATFORM_SOURCE, platform, contentSessionId) as { id: number } | undefined;
    if (direct) {
      this.db.prepare('UPDATE sdk_sessions SET custom_title = ? WHERE id = ?')
        .run(customTitle, direct.id);
      return 'applied';
    }

    // (2) Via a replicated prompt: when row ops arrived first, the
    // observation-created stub carries a synthetic content id, but any
    // replicated prompt knows its real content_session_id AND its local

View on GitHub (pinned to d8bc9755e7)