{"record":{"id":"1a91cf808b2539ba","repo":"thedotmack/claude-mem","slug":"sync-hub-push-200-response-contains-an-extra-or-m","errorCode":null,"errorMessage":"sync hub push: 200 response contains an extra or mismatched acknowledgment tuple","messagePattern":"sync hub push: 200 response contains an extra or mismatched acknowledgment tuple","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/services/sync/CloudSync.ts","lineNumber":1039,"sourceCode":"    const sentCounts = new Map<string, number>();\n    for (const op of pushed) {\n      const body = parseCanonicalOperation(op);\n      const key = operationTupleKey({\n        id: body.id,\n        kind: body.kind,\n        entity_rev: body.entity_rev,\n        operation_sha256: op.operation_sha256,\n      });\n      sentCounts.set(key, (sentCounts.get(key) ?? 0) + 1);\n    }\n\n    const ackCounts = new Map<string, number>();\n    const tupleSeq = new Map<string, string>();\n    const seqTuple = new Map<string, string>();\n    for (const ack of response.acked) {\n      const key = operationTupleKey(ack);\n      if (!sentCounts.has(key)) {\n        throw new Error('sync hub push: 200 response contains an extra or mismatched acknowledgment tuple');\n      }\n      ackCounts.set(key, (ackCounts.get(key) ?? 0) + 1);\n\n      const priorTupleSeq = tupleSeq.get(key);\n      if (priorTupleSeq !== undefined && priorTupleSeq !== ack.seq) {\n        throw new Error('sync hub push: duplicate operation tuple claimed different sequences');\n      }\n      tupleSeq.set(key, ack.seq);\n\n      const priorSeqTuple = seqTuple.get(ack.seq);\n      if (priorSeqTuple !== undefined && priorSeqTuple !== key) {\n        throw new Error('sync hub push: distinct operation tuples claimed the same sequence');\n      }\n      seqTuple.set(ack.seq, key);\n    }\n\n    for (const [key, expected] of sentCounts) {\n      const actual = ackCounts.get(key) ?? 0;","sourceCodeStart":1021,"sourceCodeEnd":1057,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d768ba364302d12b76e69e4f021f0bb1d2d50ed6/src/services/sync/CloudSync.ts#L1021-L1057","documentation":"Thrown in validatePushResponse when an ack's operation-tuple key (JSON of [id, kind, entity_rev, operation_sha256]) does not match ANY tuple the client actually sent. The hub is acknowledging an operation the client never pushed, or the tuple identity diverged between send and ack. This is the canonical 'extra or mismatched ack' invariant — every ack must correspond to a sent op.","triggerScenarios":"A 200 push response where at least one acked entry's (id, kind, entity_rev, operation_sha256) combination has no matching entry in the sentCounts multiset built from the pushed WireOp array. Reachable in validatePushResponse, which runs before any SQLite mutation in stampAcked.","commonSituations":"Hub rewrites operation_sha256 or entity_rev on its side (e.g. normalizing/padding), hub echoes back ops from a different device/batch, a proxy that recomputes hashes, or client/hub version skew where the tuple key components differ. The mismatch is almost always operation_sha256 or entity_rev differing.","solutions":["Compare the acked tuple (id, kind, entity_rev, operation_sha256) against the pushed WireOp bodies — operationTupleKey JSON-encodes all four; any single field diff triggers this.","If the hub normalizes entity_rev or operation_sha256 (e.g. leading zeros, hash casing), make the hub preserve the exact client-supplied values.","Verify the hub is not mixing in acks from concurrent push batches or other devices.","Upgrade hub and client together to a matching tuple-identity contract."],"exampleFix":"// before: pushed op { id:'obs-1', kind:'observation', entity_rev:'1', operation_sha256:'abc...' }\n//         but hub acks entity_rev:'01' (re-padded) -> tuple mismatch\n// after:  hub returns entity_rev exactly as sent ('1') so operationTupleKey matches","handlingStrategy":"try-catch","validationCode":"// Pre-check that every ack you EXPECT can match a sent tuple is impossible\n// (acks come from the hub). Instead ensure your pushed ops produce stable keys:\nimport { createHash } from 'crypto';\nfunction stableSha256(buf: string): string {\n  return createHash('sha256').update(buf).digest('hex'); // identical on both sides\n}","typeGuard":null,"tryCatchPattern":"try { await cloudSync.push(pushed); }\ncatch (e) {\n  if (e instanceof Error && e.message.includes('extra or mismatched acknowledgment tuple')) {\n    // log the sent vs acked tuple sets, leave outbox intact for retry\n    logger.error('SYNC', e.message, { sentCount: pushed.length }); return;\n  }\n  throw e;\n}","preventionTips":["Ensure operation_sha256 is computed identically on client and hub (same encoding, casing, canonicalization).","Do not let any intermediary (proxy/CDN) rewrite entity_rev or operation_sha256.","Confirm the hub acks only ops from the current push batch, not other devices."],"tags":["sync","cloud-sync","hub-response","validation","tuple-identity"],"backgroundTag":null,"analyzedSha":"d768ba364302d12b76e69e4f021f0bb1d2d50ed6","analyzedAt":"2026-08-12T23:52:55.241Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}