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}: field ${key} must be a string, got ${typeof v}

What it means

SyncApply validates every field of an incoming sync op body before writing it to SQLite. fieldString() throws when a column that must be a string (or absent/null) contains a value of some other type. Failing the whole batch prevents silently coercing garbage (e.g. numbers, objects) into NULL or text columns.

Solutions

  1. Inspect the op body at seq=${op.seq} and fix the offending field to be a JSON string or remove it (null/undefined is allowed).
  2. Fix the producer that built the op body so string columns are serialized as strings (e.g. String(v) or ISO date formatting).
  3. If a schema version skew caused the type change, re-sync the affected entity from the origin device with a corrected canonical body.
  4. Add a pre-enqueue validation on the writer side that rejects non-string values for string columns before the op enters the sync log.

Example fix

// before (producer)
body.created_at_epoch_ms = Date.now();
body.created_at = Date.now();
// after
body.created_at_epoch = Date.now();
body.created_at = new Date().toISOString();
Defensive patterns

Strategy: validation

Validate before calling

function isValidSyncBodyString(body) {
  const KEYS = ['memory_session_id','project','type','content_session_id','created_at'];
  return KEYS.every(k => !(k in body) || body[k] === null || typeof body[k] === 'string');
}

Type guard

function isStringOrNull(v) { return v === undefined || v === null || typeof v === 'string'; }

Try / catch

try {
  applySyncOp(op);
} catch (e) {
  if (String(e.message).includes('must be a string')) {
    logger.warn({ seq: op.seq, err: e.message }, 'skipping malformed sync op');
  } else throw e;
}

Prevention

When it happens

Trigger: A sync op body JSON has a field like memory_session_id, project, type, created_at, or content_session_id present but typed as a number, boolean, array, or nested object instead of a string.

Common situations: A producer device serializes created_at as an epoch number instead of an ISO string; a schema change on the writer side emits project as null-containing nested object; hand-crafted or migrated sync payloads with wrong field types are replayed into the queue.

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


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

Appendix: source

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

}

type ChromaJob = () => Promise<void>;

function invalidOp(op: SyncOp, message: string): Error {
  return new Error(`SyncApply: invalid op seq=${op.seq} kind=${op.kind} origin=${op.origin_device}/${op.origin_id}: ${message}`);
}

/**
 * Typed field readers — loud, not lossy: a MISSING (or null) field is
 * tolerated as null (required-ness is enforced by the per-kind checks), but
 * a field that is PRESENT with the wrong type is a malformed body and throws,
 * failing the batch instead of silently writing NULL.
 */
function fieldString(op: SyncOp, obj: Record<string, unknown>, key: string): string | null {
  const v = obj[key];
  if (v === undefined || v === null) return null;
  if (typeof v === 'string') return v;
  throw invalidOp(op, `field ${key} must be a string, got ${typeof v}`);
}

function fieldNumber(op: SyncOp, obj: Record<string, unknown>, key: string): number | null {
  const v = obj[key];
  if (v === undefined || v === null) return null;
  if (typeof v === 'number' && Number.isFinite(v)) return v;
  throw invalidOp(op, `field ${key} must be a finite number, got ${typeof v}`);
}

/** Parse a JSON-string list column for Chroma; never throws. */
function parseListColumn(v: unknown): string[] {
  if (typeof v !== 'string') return [];
  try {
    const parsed = JSON.parse(v);
    return Array.isArray(parsed) ? parsed.map(String) : [];
  } catch {
    return [];
  }

View on GitHub (pinned to d8bc9755e7)