{"record":{"id":"b217aa96b98b3f7b","repo":"thedotmack/claude-mem","slug":"syncapply-invalid-op-seq-op-seq-kind-op-kind-origin-op-b217aa","errorCode":null,"errorMessage":"SyncApply: invalid op seq=${op.seq} kind=${op.kind} origin=${op.origin_device}/${op.origin_id}: set_prompt_session requires target.origin_device_id, target.origin_local_id, fields.memory_session_id","messagePattern":"SyncApply: invalid op seq=(.+?) kind=(.+?) origin=(.+?)/(.+?): set_prompt_session requires target\\.origin_device_id, target\\.origin_local_id, fields\\.memory_session_id","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/services/sync/SyncApply.ts","lineNumber":1048,"sourceCode":"        .run(customTitle, viaPrompt.session_db_id);\n      return 'applied';\n    }\n\n    // (3) Park — claimed when the session (or the content↔memory\n    // association) shows up. Genuine hub-log order is [set_title, row ops…],\n    // so this is the COMMON path on a device that has never seen the session.\n    this.parkTitle(SyncApply.parkedTitleContentKey(platform, contentSessionId), customTitle);\n    return 'applied';\n  }\n\n  private applySetPromptSession(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    const targetDevice = target ? fieldString(op, target, 'origin_device_id') : null;\n    const targetLocalId = target ? fieldString(op, target, 'origin_local_id') : null;\n    const memorySessionId = fields ? fieldString(op, fields, 'memory_session_id') : null;\n    if (!targetDevice || !targetLocalId || !memorySessionId) {\n      throw invalidOp(op, 'set_prompt_session requires target.origin_device_id, target.origin_local_id, fields.memory_session_id');\n    }\n\n    // Origin identity match: a row that originated HERE is a native row\n    // (origin columns NULL, origin_local_id is its rowid); anywhere else it\n    // is a replica keyed by the origin pair.\n    const row = (targetDevice === this.deviceId\n      ? this.db.prepare(\n          `SELECT id, CAST(sync_rev AS TEXT) AS sync_rev\n           FROM user_prompts WHERE origin_device_id IS NULL AND id = ?`\n        ).get(targetLocalId)\n      : this.db.prepare(\n          `SELECT id, CAST(sync_rev AS TEXT) AS sync_rev\n           FROM user_prompts WHERE origin_device_id = ? AND origin_local_id = ?`\n        ).get(targetDevice, targetLocalId)) as RowIdRev | undefined;\n    if (!row) return 'stale'; // row not replicated here (yet) — nothing to repair\n    if (compareCanonicalDecimals(op.rev, row.sync_rev) < 0) return 'stale'; // stale mutation\n\n    const sessionDbId = this.ensureSessionForMemoryId(","sourceCodeStart":1030,"sourceCodeEnd":1066,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/src/services/sync/SyncApply.ts#L1030-L1066","documentation":"The set_prompt_session apply path requires three pieces of addressing data: target.origin_device_id, target.origin_local_id (to locate the originating device's row, native or replica), and fields.memory_session_id (the value to set). Missing any of the three makes the op unresolvable, so it is rejected with invalidOp before any database access.","triggerScenarios":"A set_prompt_session op arrives missing any of: target.origin_device_id, target.origin_local_id, or fields.memory_session_id — e.g. target points at the device by name instead of deviceId, or fields omits the memory session id.","commonSituations":"A producer on another device using its device name rather than the sync deviceId; a partially-built op from new tooling; schema drift where memory_session_id moved into target instead of fields.","solutions":["Populate all three: target.origin_device_id, target.origin_local_id, fields.memory_session_id","Verify the producer uses the registered sync deviceId (this.deviceId format), not a hostname","If memory_session_id was moved to target, restore it under fields to match the current op schema","Log the offending op at seq=${op.seq} and re-emit it correctly"],"exampleFix":"// before\nbody: { target: { origin_local_id: '42' }, fields: {} }\n// after\nbody: { target: { origin_device_id: 'dev-abc', origin_local_id: '42' }, fields: { memory_session_id: 'mem-123' } }","handlingStrategy":"validation","validationCode":"function isValidSetPromptSessionBody(body: Record<string, unknown>): boolean {\n  const t = body.target as any, f = body.fields as any;\n  return typeof t?.origin_device_id === 'string' &&\n         typeof t?.origin_local_id === 'string' &&\n         typeof f?.memory_session_id === 'string';\n}","typeGuard":"const isSetPromptSessionBody = (b: unknown): b is { target: { origin_device_id: string; origin_local_id: string }; fields: { memory_session_id: string } } =>\n  typeof b === 'object' && b !== null &&\n  typeof (b as any).target?.origin_device_id === 'string' &&\n  typeof (b as any).target?.origin_local_id === 'string' &&\n  typeof (b as any).fields?.memory_session_id === 'string';","tryCatchPattern":"try { apply(op); } catch (e) {\n  if (e instanceof InvalidOpError && e.reason.includes('set_prompt_session requires')) {\n    quarantineOp(e.seq);\n  } else throw e;\n}","preventionTips":["Use the device's registered sync deviceId, never a hostname or display name","Keep origin_local_id as the producer-side rowid string","Centralize op-body builders so all three fields are set together","Test cross-device replication with real device ids"],"tags":["sync","validation","identifier"],"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"}