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
- Fix the op body so the field is a real JSON number (remove quotes around the value).
- On the producer, coerce with Number(v) and check Number.isFinite(v) before building the body.
- If the value is genuinely absent, omit the key or set null — fieldNumber treats missing as null rather than throwing.
- 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
- Use Number(v) and Number.isFinite(v) checks on the producer before enqueueing
- Never stringify epoch timestamps — keep them as JSON numbers
- Guard against NaN/Infinity reaching JSON payloads
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
- canonical content
- device_id must be 1-128 characters
- deviceId must be non-empty
- name must be 1-80 characters
- ops[ ]
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)