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
- 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).
- Fix the producer that built the op body so string columns are serialized as strings (e.g. String(v) or ISO date formatting).
- If a schema version skew caused the type change, re-sync the affected entity from the origin device with a corrected canonical body.
- 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
- Serialize timestamps as ISO strings for *_at fields and epochs as numbers for *_epoch fields
- Validate op bodies against a schema (zod/JSON Schema) before enqueueing
- Never let raw non-JSON-typed values flow into body fields without coercion
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
- canonical content
- device_id must be 1-128 characters
- deviceId must be non-empty
- mutation body must be an object
- name must be 1-80 characters
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)