thedotmack/claude-mem · error

sync hub push: 200 response acknowledgment multiplicity…

Error message

sync hub push: 200 response acknowledgment multiplicity mismatch (expected ${expected}, received ${actual})

What it means

Per-tuple acknowledgment counts don't match what was sent: a tuple pushed N times was acked M≠N times. Duplicates in one push are legitimate (the same entity/rev op can appear in more than one lane), which is why counts, not just presence, are checked. Caught before stamping; flush() retries with backoff. Usually means the hub collapsed duplicate submissions into one ack while the client sent copies, or acked one copy too many.

Solutions

  1. Log sentCounts vs ackCounts around the failing push to see which tuple's multiplicity differs
  2. Align hub behavior with the contract: every pushed copy gets one ack entry, including duplicates
  3. Or, if the hub intentionally coalesces, dedupe identical tuples client-side before packing the batch
  4. Check whether a re-enqueue path can queue the exact same (entity_id, entity_rev) twice in one drain and prevent it

Example fix

// before: same tuple can enter the batch twice
buf.push(op); buf.push(op);

// after: dedupe by tuple key when packing
const key = operationTupleKey({ id, kind, entity_rev, operation_sha256 });
if (!seen.has(key)) { seen.add(key); buf.push(op); }
Defensive patterns

Strategy: try-catch

Validate before calling

// If you assemble batches yourself, dedupe identical tuples before packing:
const seen = new Set<string>();
const packed = ops.filter(op => {
  const body = parseCanonicalOperation(op);
  const key = JSON.stringify([body.id, body.kind, body.entity_rev, op.operation_sha256]);
  if (seen.has(key)) return false;
  seen.add(key);
  return true;
});

Try / catch

const m = (sync.status().lastError ?? '').match(/multiplicity mismatch \(expected (\d+), received (\d+)\)/);
if (m) {
  // expected>received: hub coalesced duplicate copies — align on per-copy acks or dedupe client-side
}

Prevention

When it happens

Trigger: The same op tuple queued twice locally (e.g. content lane plus a frozen re-enqueue) and the hub's dedupe acks it once; hub coalesces duplicate appends into one ack entry but the client counts two; a hub contract change on duplicate handling.

Common situations: Version skew where the hub changed from per-copy acks to per-unique-op acks; requeued retries producing duplicate tuples in a single batch; migration paths that legitimately double-enqueue.

Related errors


AI-assisted analysis of thedotmack/claude-mem@e2d1df569a (2026-08-20). Data as JSON: /api/errors/f93e0925e96b98df. Report an issue: GitHub.

Appendix: source

Thrown at src/services/sync/CloudSync.ts:1059

      ackCounts.set(key, (ackCounts.get(key) ?? 0) + 1);

      const priorTupleSeq = tupleSeq.get(key);
      if (priorTupleSeq !== undefined && priorTupleSeq !== ack.seq) {
        throw new Error('sync hub push: duplicate operation tuple claimed different sequences');
      }
      tupleSeq.set(key, ack.seq);

      const priorSeqTuple = seqTuple.get(ack.seq);
      if (priorSeqTuple !== undefined && priorSeqTuple !== key) {
        throw new Error('sync hub push: distinct operation tuples claimed the same sequence');
      }
      seqTuple.set(ack.seq, key);
    }

    for (const [key, expected] of sentCounts) {
      const actual = ackCounts.get(key) ?? 0;
      if (actual !== expected) {
        throw new Error(
          `sync hub push: 200 response acknowledgment multiplicity mismatch (expected ${expected}, received ${actual})`
        );
      }
    }
    if (ackCounts.size !== sentCounts.size) {
      // Defensive: the unknown-tuple branch above should make this impossible.
      throw new Error('sync hub push: 200 response acknowledgment multiset mismatch');
    }

    if (compareCanonicalDecimals(response.head_seq, response.projected_seq) > 0) {
      throw new Error('sync hub push: checkpoint order requires head_seq <= projected_seq');
    }
    for (const ack of response.acked) {
      if (compareCanonicalDecimals(ack.seq, response.head_seq) > 0) {
        throw new Error('sync hub push: acknowledgment seq exceeds head_seq');
      }
      if (compareCanonicalDecimals(ack.seq, response.projected_seq) > 0) {
        throw new Error('sync hub push: sent operation is not covered by projected_seq');

View on GitHub (pinned to e2d1df569a)