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 requires fields.custom_title

What it means

After shape-checking target and fields, applySetTitle reads fields.custom_title via fieldString, which returns null for missing/non-string values. A set_title op whose purpose is to set the title must carry a custom_title string, so a null result means the op is incomplete and is rejected. This keeps a meaningless title mutation out of the replica.

Solutions

  1. Ensure the producer always sets fields.custom_title to a string in set_title ops
  2. Check for producer-side field renames or null-uncoalesced title values
  3. If clearing the title is a supported intent, use the documented op/kind for clearing rather than omitting custom_title
  4. Verify the op JSON at seq=${op.seq} and re-emit a corrected op

Example fix

// before
fields: { custom_title: null }
// after
fields: { custom_title: 'Session title' }
Defensive patterns

Strategy: validation

Validate before calling

function hasCustomTitle(fields: unknown): fields is { custom_title: string } {
  return !!fields && typeof fields === 'object' &&
    typeof (fields as any).custom_title === 'string' && (fields as any).custom_title.length > 0;
}

Type guard

const hasString = (v: unknown, k: string): v is Record<string, string> =>
  typeof v === 'object' && v !== null && typeof (v as any)[k] === 'string';

Try / catch

try { apply(op); } catch (e) {
  if (e instanceof InvalidOpError && e.reason.includes('set_title requires fields.custom_title')) {
    log.warn(`dropping malformed title op seq=${e.seq}`);
  } else throw e;
}

Prevention

When it happens

Trigger: A set_title op arrives with fields present but fields.custom_title missing, null, or of a non-string type (e.g. a number or nested object).

Common situations: Producer sent a title-clearing op using the wrong shape; a UI bug sending custom_title: null; a renamed field on the producer (e.g. title instead of custom_title) after a schema change.

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

Appendix: source

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

  }

  /**
   * set_title resolution (three steps — see SET_TITLE CONVERGENCE in the
   * module header): direct session match → via a replicated prompt's content
   * id → park in sync_state for a later claim. sdk_sessions carries no
   * sync_rev (v41 touches only the synced tables), so rev is never
   * consulted: direct applications overwrite in hub-log order; parked
   * claims fill only a NULL custom_title.
   */
  private applySetTitle(op: SyncOp, body: Record<string, unknown>): 'applied' | 'stale' {
    const target = body.target as Record<string, unknown> | undefined;
    const fields = body.fields as Record<string, unknown> | undefined;
    if (!target || typeof target !== 'object' || Array.isArray(target) || !fields || typeof fields !== 'object' || Array.isArray(fields)) {
      throw invalidOp(op, 'set_title requires target and fields objects');
    }
    const customTitle = fieldString(op, fields, 'custom_title');
    if (customTitle === null) {
      throw invalidOp(op, 'set_title requires fields.custom_title');
    }

    // NOTE: every branch here is SELECT-then-UPDATE-by-id, never a decision
    // on `.run().changes` — bun:sqlite (1.3.9) has been observed reporting a
    // bogus non-zero `changes` for a zero-match UPDATE when it follows a
    // RETURNING statement inside the same transaction on this connection
    // (reported 4 with a verified 0 matching rows), which silently misroutes
    // 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';
      }

View on GitHub (pinned to d8bc9755e7)