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 finite number, got ${typeof v}

What it means

fieldNumber() validates numeric sync-op body fields (created_at_epoch, prompt_number, etc.). It throws when the field is present but not a finite number — strings, NaN, Infinity, booleans, or objects are all rejected. This keeps SQLite numeric columns and downstream date math free of garbage.

Solutions

  1. Fix the op body so the field is a real JSON number (remove quotes around the value).
  2. On the producer, coerce with Number(v) and check Number.isFinite(v) before building the body.
  3. If the value is genuinely absent, omit the key or set null — fieldNumber treats missing as null rather than throwing.
  4. Re-derive created_at from created_at string if present: body.created_at_epoch = Date.parse(body.created_at).

Example fix

// before
{ "created_at_epoch": "1712345678000" }
// after
{ "created_at_epoch": 1712345678000 }
Defensive patterns

Strategy: validation

Validate before calling

function isValidSyncBodyNumber(body) {
  const KEYS = ['created_at_epoch','prompt_number'];
  return KEYS.every(k => !(k in body) || body[k] === null || (typeof body[k] === 'number' && Number.isFinite(body[k])));
}

Type guard

function isFiniteNumberOrNull(v) { return v === undefined || v === null || (typeof v === 'number' && Number.isFinite(v)); }

Try / catch

try {
  applySyncOp(op);
} catch (e) {
  if (String(e.message).includes('must be a finite number')) {
    deadLetterQueue.push(op); // re-emit with corrected numeric field
  } else throw e;
}

Prevention

When it happens

Trigger: A sync op body carries created_at_epoch or prompt_number as a string like "1712345678" (common with JSON produced by weakly-typed languages), or as NaN/Infinity that serialized to null-ish/invalid JSON, or as a boolean.

Common situations: Producers in dynamically typed languages emit epoch timestamps as strings; NaN leaked into a body and was serialized as a non-number; a version change renamed a field so a number lands where the wrong key was read.

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

Appendix: source

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

/**
 * 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 [];
  }
}

export class SyncApply {
  private readonly db: Database;
  private readonly deviceId: string;
  private readonly chromaSync: ChromaSyncLike | null;
  private readonly now: () => number;

View on GitHub (pinned to d8bc9755e7)