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 fields.custom_title What it means
After shape-checking target and fields, applySetTitle reads fields.custom_title via fieldString, which returns null for missing/non-string values. A set_title op whose purpose is to set the title must carry a custom_title string, so a null result means the op is incomplete and is rejected. This keeps a meaningless title mutation out of the replica.
Solutions
- Ensure the producer always sets fields.custom_title to a string in set_title ops
- Check for producer-side field renames or null-uncoalesced title values
- If clearing the title is a supported intent, use the documented op/kind for clearing rather than omitting custom_title
- Verify the op JSON at seq=${op.seq} and re-emit a corrected op
Example fix
// before
fields: { custom_title: null }
// after
fields: { custom_title: 'Session title' } Defensive patterns
Strategy: validation
Validate before calling
function hasCustomTitle(fields: unknown): fields is { custom_title: string } {
return !!fields && typeof fields === 'object' &&
typeof (fields as any).custom_title === 'string' && (fields as any).custom_title.length > 0;
} Type guard
const hasString = (v: unknown, k: string): v is Record<string, string> => typeof v === 'object' && v !== null && typeof (v as any)[k] === 'string';
Try / catch
try { apply(op); } catch (e) {
if (e instanceof InvalidOpError && e.reason.includes('set_title requires fields.custom_title')) {
log.warn(`dropping malformed title op seq=${e.seq}`);
} else throw e;
} Prevention
- Coerce title to string (or reject) on the producer before enqueueing
- Never send custom_title: null; use the documented clear-title mechanism
- Add a producer-side unit test asserting fields.custom_title is a non-empty string
- Grep producer code for field renames when upgrading
When it happens
Trigger: A set_title op arrives with fields present but fields.custom_title missing, null, or of a non-string type (e.g. a number or nested object).
Common situations: Producer sent a title-clearing op using the wrong shape; a UI bug sending custom_title: null; a renamed field on the producer (e.g. title instead of custom_title) after a schema change.
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
- SyncApply: invalid op seq=
- canonical content
- device_id must be 1-128 characters
- deviceId must be non-empty
- name must be 1-80 characters
AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17).
Data as JSON: /api/errors/9ae13886d0adeb58.
Report an issue: GitHub.
Appendix: source
Thrown at src/services/sync/SyncApply.ts:977
}
/**
* 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) {
this.db.prepare('UPDATE sdk_sessions SET custom_title = ? WHERE id = ?')
.run(customTitle, session.id);
return 'applied';
}View on GitHub (pinned to d8bc9755e7)