thedotmack/claude-mem · error

sync hub push: 200 response contains an extra or mismatched…

Error message

sync hub push: 200 response contains an extra or mismatched acknowledgment tuple

What it means

validatePushResponse() builds the exact multiset of sent (id, kind, entity_rev, operation_sha256) tuples and rejects any acked tuple that was not in the request. The hub must acknowledge exactly what was pushed — an extra or unknown tuple means the response belongs to a different request or was fabricated. The check runs before any SQLite mutation (stampAcked), so local state is safe; flush() backs off and retries, and hub dedupe makes re-pushes harmless. Treat persistent occurrences as a hub integrity or routing problem.

Solutions

  1. Log the pushed tuple keys next to the acked ones to identify what the hub returned that was never sent
  2. Verify all hub replicas share one authoritative log — no per-replica append paths behind a load balancer
  3. Disable POST caching at every intermediary in front of the hub
  4. Upgrade/patch the hub if its ack derivation diverges from the appended ops
  5. Last resort: the local queue can be re-drained from scratch for the affected entities; hub dedupe on (origin_device, kind, origin_id, rev) makes the re-push safe
Defensive patterns

Strategy: try-catch

Validate before calling

// You cannot pre-validate a remote hub's behavior; pre-compute your sent tuple set
// so you can diff against the hub response when investigating:
const sentKeys = new Set(ops.map(op => {
  const body = parseCanonicalOperation(op);
  return JSON.stringify([body.id, body.kind, body.entity_rev, op.operation_sha256]);
}));
// on failure, diff response.acked keys against sentKeys to see the phantom tuple

Try / catch

if (/extra or mismatched acknowledgment tuple/.test(sync.status().lastError ?? '')) {
  // hub integrity/routing alert: verify single durable log behind the LB,
  // disable POST caching at intermediaries, then let the backoff retry
}

Prevention

When it happens

Trigger: Hub acks ops from a previous batch (cross-request state bleed); load-balanced hub replicas with divergent logs returning each other's acknowledgments; a proxy cache replaying an old 200 body; a hub bug echoing expected acks derived from something other than the appended ops.

Common situations: Sticky-session-less LB over hub replicas that don't share a single durable log; an intermediary caching POST responses (RFC violation but it happens); hub mid-incident returning stale responses.

Related errors


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

Appendix: source

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

    const sentCounts = new Map<string, number>();
    for (const op of pushed) {
      const body = parseCanonicalOperation(op);
      const key = operationTupleKey({
        id: body.id,
        kind: body.kind,
        entity_rev: body.entity_rev,
        operation_sha256: op.operation_sha256,
      });
      sentCounts.set(key, (sentCounts.get(key) ?? 0) + 1);
    }

    const ackCounts = new Map<string, number>();
    const tupleSeq = new Map<string, string>();
    const seqTuple = new Map<string, string>();
    for (const ack of response.acked) {
      const key = operationTupleKey(ack);
      if (!sentCounts.has(key)) {
        throw new Error('sync hub push: 200 response contains an extra or mismatched acknowledgment tuple');
      }
      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;

View on GitHub (pinned to e2d1df569a)