{"record":{"id":"033028bfce93006e","repo":"thedotmack/claude-mem","slug":"syncapply-invalid-op-seq-op-seq-kind-op-kind-origin-op-033028","errorCode":null,"errorMessage":"SyncApply: invalid op seq=${op.seq} kind=${op.kind} origin=${op.origin_device}/${op.origin_id}: remap_project where predicate is empty","messagePattern":"SyncApply: invalid op seq=(.+?) kind=(.+?) origin=(.+?)/(.+?): remap_project where predicate is empty","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/services/sync/SyncApply.ts","lineNumber":1122,"sourceCode":"    const whereClauses: string[] = [];\n    const whereParams: (string | number)[] = [];\n    const whereProject = fieldString(op, where, 'project');\n    const whereMemorySessionId = fieldString(op, where, 'memory_session_id');\n    if (whereProject !== null) {\n      whereClauses.push('project = ?');\n      whereParams.push(whereProject);\n    }\n    if (whereMemorySessionId !== null) {\n      whereClauses.push('memory_session_id = ?');\n      whereParams.push(whereMemorySessionId);\n    }\n    if (where.merged_into_project_is_null === true) {\n      whereClauses.push('merged_into_project IS NULL');\n    }\n    if (whereClauses.length === 0) {\n      // Refuse an unbounded remap outright — a predicate-free UPDATE across\n      // the whole corpus is never intentional.\n      throw invalidOp(op, 'remap_project where predicate is empty');\n    }\n\n    // Rev guard per matched row: apply only where op.rev >= sync_rev, and\n    // stamp sync_rev = op.rev on the rows actually remapped (op.rev is the\n    // max by the guard). synced_at untouched — the remap travels via the log.\n    //\n    // The applied/stale outcome comes from COUNT-then-UPDATE, never from\n    // `.run().changes` — see the note in applySetTitle (bun:sqlite reports\n    // unreliable `changes` after RETURNING statements in the same\n    // transaction). Both statements see identical in-transaction state.\n    let matched = 0;\n    for (const table of ['observations', 'session_summaries']) {\n      const rows = this.db.prepare(`\n        SELECT CAST(id AS TEXT) AS id, CAST(sync_rev AS TEXT) AS sync_rev\n        FROM ${table} WHERE ${whereClauses.join(' AND ')}\n      `).all(...whereParams) as Array<{ id: string; sync_rev: string }>;\n      for (const row of rows) {\n        if (compareCanonicalDecimals(op.rev, row.sync_rev) < 0) continue;","sourceCodeStart":1104,"sourceCodeEnd":1140,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/src/services/sync/SyncApply.ts#L1104-L1140","documentation":"remap_project refuses to run an UPDATE with no WHERE predicate: after assembling clauses from where.project, where.memory_session_id, and where.merged_into_project_is_null, an empty predicate list would mean remapping the entire corpus, which is treated as never intentional. The op is rejected outright.","triggerScenarios":"A remap_project op with an empty where object ({}), or where whose keys are all unrecognized/non-string so no clause is generated (e.g. { ids: [...] }).","commonSituations":"A producer that meant to remap everything and tried an empty selector; a bulk-merge tool passing an empty filter after its own query returned no groups; test fixtures with placeholder where clauses.","solutions":["Supply at least one supported predicate: where.project, where.memory_session_id, or where.merged_into_project_is_null=true","Never send an empty where — iterate the matched sessions and emit one bounded op per group instead","Add producer-side assertion that a remap op always has a non-empty, recognized predicate"],"exampleFix":"// before\nbody: { where: {}, fields: { project: 'proj-b' } }\n// after\nbody: { where: { project: 'proj-a' }, fields: { project: 'proj-b' } }","handlingStrategy":"validation","validationCode":"function hasRemapPredicate(where: Record<string, unknown>): boolean {\n  return typeof where.project === 'string' ||\n         typeof where.memory_session_id === 'string' ||\n         where.merged_into_project_is_null === true;\n}\nif (!hasRemapPredicate(op.body.where)) throw new Error('remap needs a non-empty where predicate');","typeGuard":"const hasPredicate = (w: unknown): w is { project?: string; memory_session_id?: string; merged_into_project_is_null?: true } =>\n  typeof w === 'object' && w !== null && Object.keys(w).length > 0;","tryCatchPattern":"try { apply(op); } catch (e) {\n  if (e instanceof InvalidOpError && e.reason.includes('where predicate is empty')) {\n    // never auto-broaden; require explicit re-emission with a bounded predicate\n  } else throw e;\n}","preventionTips":["Never emit an empty or unrecognized where object","Build remaps from concrete query results (per-project, per-session groups)","Add a producer-side guard that rejects predicate-free remaps","Only use supported predicate keys: project, memory_session_id, merged_into_project_is_null"],"tags":["sync","validation","safety"],"backgroundTag":"invalid-argument-value","analyzedSha":"d8bc9755e74915e5c3b999181e10a67c889bce2a","analyzedAt":"2026-09-17T16:40:26.182Z","contentChangedAt":"2026-09-17T16:40:26.182Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}