{"record":{"id":"9ae13886d0adeb58","repo":"thedotmack/claude-mem","slug":"syncapply-invalid-op-seq-op-seq-kind-op-kind-origin-op-9ae138","errorCode":null,"errorMessage":"SyncApply: invalid op seq=${op.seq} kind=${op.kind} origin=${op.origin_device}/${op.origin_id}: set_title requires fields.custom_title","messagePattern":"SyncApply: invalid op seq=(.+?) kind=(.+?) origin=(.+?)/(.+?): set_title requires fields\\.custom_title","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/services/sync/SyncApply.ts","lineNumber":977,"sourceCode":"  }\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) {\n        this.db.prepare('UPDATE sdk_sessions SET custom_title = ? WHERE id = ?')\n          .run(customTitle, session.id);\n        return 'applied';\n      }","sourceCodeStart":959,"sourceCodeEnd":995,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/src/services/sync/SyncApply.ts#L959-L995","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before\nfields: { custom_title: null }\n// after\nfields: { custom_title: 'Session title' }","handlingStrategy":"validation","validationCode":"function hasCustomTitle(fields: unknown): fields is { custom_title: string } {\n  return !!fields && typeof fields === 'object' &&\n    typeof (fields as any).custom_title === 'string' && (fields as any).custom_title.length > 0;\n}","typeGuard":"const hasString = (v: unknown, k: string): v is Record<string, string> =>\n  typeof v === 'object' && v !== null && typeof (v as any)[k] === 'string';","tryCatchPattern":"try { apply(op); } catch (e) {\n  if (e instanceof InvalidOpError && e.reason.includes('set_title requires fields.custom_title')) {\n    log.warn(`dropping malformed title op seq=${e.seq}`);\n  } else throw e;\n}","preventionTips":["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"],"tags":["sync","validation","fields"],"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"}