thedotmack/claude-mem · error
sync hub push: checkpoint order requires head_seq <=…
Error message
sync hub push: checkpoint order requires head_seq <= projected_seq
What it means
The push response reports head_seq greater than projected_seq. The hub's contract is to return 200 only once its authoritative projection checkpoint covers the commit, so head_seq <= projected_seq must always hold; head beyond the checkpoint means the hub answered before projection caught up, inverted the fields, or replicas disagree. Caught in validatePushResponse before stamping; flush() backs off and retries safely.
Solutions
- Poll GET /v1/sync/status during pushes and check whether head_seq/projected_seq genuinely invert or merely lag
- On the hub, gate the 200 response on the projection checkpoint covering the appended seqs (head <= projected before answering)
- If the fields were swapped by a refactor, fix the response construction and redeploy
- Until fixed, the client safely retries — but throughput will collapse, so treat it as urgent hub-side
Defensive patterns
Strategy: validation
Validate before calling
// Hub-side gate before answering 200 (mirror of the client invariant):
function responseIsConsistent(headSeq: string, projectedSeq: string): boolean {
return BigInt(headSeq) <= BigInt(projectedSeq); // push contract: head <= projected
} Type guard
function pushCheckpointOrderOk(head_seq: string, projected_seq: string): boolean {
return BigInt(head_seq) <= BigInt(projected_seq);
} Try / catch
if (/head_seq <= projected_seq/.test(sync.status().lastError ?? '')) {
// hub answered before its projection covered the commit: fix the 200 gate; client retries are safe
} Prevention
- Hub: block the response until the projection checkpoint covers the appended seqs
- Derive head_seq and projected_seq from one consistent read of hub state
- Load-test projection lag — if it can trail appends, the gate must still hold
When it happens
Trigger: Hub returns 200 after durable append but before the Pro projection advances and computes head_seq from a fresher replica than projected_seq; fields swapped in a serializer; a projection worker lagging under load while the response path doesn't wait for it.
Common situations: Hub performance change made projection async without preserving the wait-for-checkpoint guarantee; replica reads of the two fields from different nodes; post-incident hub where the checkpoint restore lagged.
Related errors
- sync hub push: acknowledgment seq exceeds head_seq
- sync hub push: distinct operation tuples claimed the same…
- sync hub push: sent operation is not covered by…
- sync hub push: 200 response acknowledgment multiplicity…
- sync hub push: 200 response contains an extra or mismatched…
AI-assisted analysis of thedotmack/claude-mem@e2d1df569a (2026-08-20).
Data as JSON: /api/errors/acd43acf9b0ec060.
Report an issue: GitHub.
Appendix: source
Thrown at src/services/sync/CloudSync.ts:1070
}
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');
}
}
}
/**
* Stamp rows / delete outbox entries for a fully validated acknowledgment
* multiset. The hub may return entries in any order.
*/
private stampAcked(acked: AckedOp[], pushed: WireOp[]): void {
const now = Date.now();
const bodies = new Map(pushed.map(op => {View on GitHub (pinned to e2d1df569a)