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 target and fields objects What it means
SyncApply validates every incoming sync operation before applying it. For ops of kind 'set_title', both body.target and body.fields must be present as plain (non-array) objects; otherwise the op is rejected as invalid and thrown so it is not silently applied or parked. This protects the replica database from malformed or truncated mutation payloads.
Solutions
- Inspect the op body in the sync log at seq=${op.seq} and confirm target/fields are emitted as plain objects
- Fix the producer so set_title ops always include body.target={...} and body.fields={custom_title:...}
- Add validation on the producer side before appending the op to the sync log
- If the op is corrupt and unreproducible, drop/quarantine that op from the log so apply can proceed
Example fix
// before
const op = { kind: 'set_title', body: { target: { memory_session_id: 'm1' } } };
// after
const op = { kind: 'set_title', body: { target: { memory_session_id: 'm1' }, fields: { custom_title: 'My chat' } } }; Defensive patterns
Strategy: validation
Validate before calling
function isValidSetTitleBody(body: unknown): boolean {
if (!body || typeof body !== 'object') return false;
const b = body as Record<string, unknown>;
const isObj = (v: unknown) => !!v && typeof v === 'object' && !Array.isArray(v);
return isObj(b.target) && isObj(b.fields);
}
if (!isValidSetTitleBody(op.body)) throw new Error('set_title op missing target/fields'); Type guard
const isPlainObject = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
Try / catch
try { apply(op); } catch (e) {
if (e instanceof InvalidOpError && e.reason.includes('set_title requires target and fields')) {
quarantineOp(e.seq); // drop or re-emit corrected op
} else throw e;
} Prevention
- Always emit target and fields as plain objects in set_title ops
- Validate op bodies on the producer before appending to the sync log
- Add a schema test for every sync op kind
- Never hand-edit sync log JSON without re-validating
When it happens
Trigger: A set_title sync op arrives whose body lacks target or fields, or where either is an array, a string, or null instead of a plain object (e.g. a producer sent fields as a JSON list or omitted them).
Common situations: An older or third-party sync producer emitting an out-of-date op schema; a hand-crafted or test op JSON missing the fields object; a serialization bug that turns the fields object into an array; mid-flight schema changes between app versions syncing to the same log.
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
- canonical content
- device_id must be 1-128 characters
- deviceId must be non-empty
- Expected JSON array in observation list field, using plain…
- Invalid transcript watch config
AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17).
Data as JSON: /api/errors/8f90843e0e4476a7.
Report an issue: GitHub.
Appendix: source
Thrown at src/services/sync/SyncApply.ts:973
return this.applyRemapProject(op, body);
default:
throw invalidOp(op, `unknown mutation op ${String(body.op)}`);
}
}
/**
* 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) {View on GitHub (pinned to d8bc9755e7)