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}: remap_project fields must set project and/or merged_into_project

What it means

Even when fields is a valid object, remap_project must actually assign at least one of the two supported columns: project or merged_into_project. fieldString returns null for both when the keys are missing/non-string, leaving setClauses empty — the code then throws rather than issuing a no-op UPDATE.

Solutions

  1. Set fields.project and/or fields.merged_into_project to non-null string values
  2. Update producers using old/renamed field names to the supported keys
  3. If no remap is needed, do not enqueue a remap_project op at all

Example fix

// before
fields: { new_project: 'proj-b' }
// after
fields: { project: 'proj-b' }
Defensive patterns

Strategy: validation

Validate before calling

function remapSetsSomething(fields: Record<string, unknown>): boolean {
  return typeof fields.project === 'string' || typeof fields.merged_into_project === 'string';
}
if (!remapSetsSomething(op.body.fields)) throw new Error('remap must set project or merged_into_project');

Type guard

const hasRemapField = (f: unknown): f is { project?: string; merged_into_project?: string } =>
  typeof f === 'object' && f !== null &&
  ('project' in f || 'merged_into_project' in f);

Try / catch

try { apply(op); } catch (e) {
  if (e instanceof InvalidOpError && e.reason.includes('must set project and/or merged_into_project')) {
    log.warn(`no-op remap op seq=${e.seq} discarded`);
  } else throw e;
}

Prevention

When it happens

Trigger: A remap_project op whose fields object contains only unrecognized keys (e.g. { name: 'x' }), or sets both project and merged_into_project to null/non-string values.

Common situations: Producer renamed fields after a schema change (e.g. new_project instead of project); op intended to clear values sent with null instead of valid identifiers; copy-pasted op template with placeholder fields.

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


AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17). Data as JSON: /api/errors/8dd3cbe880f7d935. Report an issue: GitHub.

Appendix: source

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

    const fields = body.fields as Record<string, unknown> | undefined;
    if (!where || typeof where !== 'object' || !fields || typeof fields !== 'object') {
      throw invalidOp(op, 'remap_project requires where and fields objects');
    }

    const setClauses: string[] = [];
    const setParams: (string | number)[] = [];
    const newProject = fieldString(op, fields, 'project');
    const mergedInto = fieldString(op, fields, 'merged_into_project');
    if (newProject !== null) {
      setClauses.push('project = ?');
      setParams.push(newProject);
    }
    if (mergedInto !== null) {
      setClauses.push('merged_into_project = ?');
      setParams.push(mergedInto);
    }
    if (setClauses.length === 0) {
      throw invalidOp(op, 'remap_project fields must set project and/or merged_into_project');
    }

    const whereClauses: string[] = [];
    const whereParams: (string | number)[] = [];
    const whereProject = fieldString(op, where, 'project');
    const whereMemorySessionId = fieldString(op, where, 'memory_session_id');
    if (whereProject !== null) {
      whereClauses.push('project = ?');
      whereParams.push(whereProject);
    }
    if (whereMemorySessionId !== null) {
      whereClauses.push('memory_session_id = ?');
      whereParams.push(whereMemorySessionId);
    }
    if (where.merged_into_project_is_null === true) {
      whereClauses.push('merged_into_project IS NULL');
    }
    if (whereClauses.length === 0) {

View on GitHub (pinned to d8bc9755e7)