{"record":{"id":"8f90843e0e4476a7","repo":"thedotmack/claude-mem","slug":"syncapply-invalid-op-seq-op-seq-kind-op-kind-origin-op-8f9084","errorCode":null,"errorMessage":"SyncApply: invalid op seq=${op.seq} kind=${op.kind} origin=${op.origin_device}/${op.origin_id}: set_title requires target and fields objects","messagePattern":"SyncApply: invalid op seq=(.+?) kind=(.+?) origin=(.+?)/(.+?): set_title requires target and fields objects","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/services/sync/SyncApply.ts","lineNumber":973,"sourceCode":"        return this.applyRemapProject(op, body);\n      default:\n        throw invalidOp(op, `unknown mutation op ${String(body.op)}`);\n    }\n  }\n\n  /**\n   * set_title resolution (three steps — see SET_TITLE CONVERGENCE in the\n   * module header): direct session match → via a replicated prompt's content\n   * id → park in sync_state for a later claim. sdk_sessions carries no\n   * sync_rev (v41 touches only the synced tables), so rev is never\n   * consulted: direct applications overwrite in hub-log order; parked\n   * claims fill only a NULL custom_title.\n   */\n  private applySetTitle(op: SyncOp, body: Record<string, unknown>): 'applied' | 'stale' {\n    const target = body.target as Record<string, unknown> | undefined;\n    const fields = body.fields as Record<string, unknown> | undefined;\n    if (!target || typeof target !== 'object' || Array.isArray(target) || !fields || typeof fields !== 'object' || Array.isArray(fields)) {\n      throw invalidOp(op, 'set_title requires target and fields objects');\n    }\n    const customTitle = fieldString(op, fields, 'custom_title');\n    if (customTitle === null) {\n      throw invalidOp(op, 'set_title requires fields.custom_title');\n    }\n\n    // NOTE: every branch here is SELECT-then-UPDATE-by-id, never a decision\n    // on `.run().changes` — bun:sqlite (1.3.9) has been observed reporting a\n    // bogus non-zero `changes` for a zero-match UPDATE when it follows a\n    // RETURNING statement inside the same transaction on this connection\n    // (reported 4 with a verified 0 matching rows), which silently misroutes\n    // apply-vs-park. SELECTs in our transaction are authoritative.\n    const memorySessionId = fieldString(op, target, 'memory_session_id');\n    if (memorySessionId) {\n      const session = this.db.prepare(\n        'SELECT id FROM sdk_sessions WHERE memory_session_id = ?'\n      ).get(memorySessionId) as { id: number } | undefined;\n      if (session) {","sourceCodeStart":955,"sourceCodeEnd":991,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/src/services/sync/SyncApply.ts#L955-L991","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before\nconst op = { kind: 'set_title', body: { target: { memory_session_id: 'm1' } } };\n// after\nconst op = { kind: 'set_title', body: { target: { memory_session_id: 'm1' }, fields: { custom_title: 'My chat' } } };","handlingStrategy":"validation","validationCode":"function isValidSetTitleBody(body: unknown): boolean {\n  if (!body || typeof body !== 'object') return false;\n  const b = body as Record<string, unknown>;\n  const isObj = (v: unknown) => !!v && typeof v === 'object' && !Array.isArray(v);\n  return isObj(b.target) && isObj(b.fields);\n}\nif (!isValidSetTitleBody(op.body)) throw new Error('set_title op missing target/fields');","typeGuard":"const isPlainObject = (v: unknown): v is Record<string, unknown> =>\n  typeof v === 'object' && v !== null && !Array.isArray(v);","tryCatchPattern":"try { apply(op); } catch (e) {\n  if (e instanceof InvalidOpError && e.reason.includes('set_title requires target and fields')) {\n    quarantineOp(e.seq); // drop or re-emit corrected op\n  } else throw e;\n}","preventionTips":["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"],"tags":["sync","validation","schema"],"backgroundTag":"missing-required-argument","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"}