thedotmack/claude-mem · warning · Error
stale_revision: : <
Error message
stale_revision:${body.id}:${body.entity_rev}<${head.entity_rev} What it means
Concurrency guard in SyncHubDO.pushOps: the incoming op's entity_rev compares (decimal-wise) as strictly older than the entity's current head revision, so applying it would regress the entity. The op is refused with a structured stale_revision:<entityId>:<incoming><<head> message while the rest of the batch may still be acked.
Solutions
- Pull latest changes (getChanges) for that entity, rebase local edits on head.entity_rev, and re-push with a greater revision
- Treat the refusal as a signal to surface a conflict-resolution UI to the user
- Fix client revision generation to always increment from the freshest observed head
Example fix
// before
await push(entity, { entity_rev: localRev });
// after
const head = await fetchEntityHead(entity.id);
if (compare(localRev, head.entity_rev) < 0) {
localRev = rebaseOn(localChanges, head.entity_rev);
}
await push(entity, { entity_rev: localRev }); Defensive patterns
Strategy: retry
Validate before calling
const head = await fetchEntityHead(entityId);
if (compareCanonicalDecimals(op.entity_rev, head.entity_rev) < 0) {
op = rebase(op, head.entity_rev);
} Try / catch
try {
return await hub.push(deviceId, ops);
} catch (e) {
if (String(e).startsWith('stale_revision:')) return rebaseAndRetry(e, ops);
throw e;
} Prevention
- Always fetch the latest head revision before incrementing
- Design a rebase/conflict-resolution flow instead of blind pushes
- Avoid retrying an identical stale batch verbatim
When it happens
Trigger: Two devices edit the same entity; the slower device pushes a revision that another device already superseded — e.g. device A pushes rev 5 while head is already 7. Also occurs when a client retries an old batch after a successful sync elsewhere.
Common situations: Offline editing followed by rebase failure, clock/revision generation bugs where the client doesn't fetch the latest head before incrementing, replayed push after network timeout.
Related errors
- cloud sync ack conflict for
- revision_hash_conflict
- SyncApply: invalid op seq=
- Backfill failed
- canonical content
AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17).
Data as JSON: /api/errors/f502bf48888058f5.
Report an issue: GitHub.
Appendix: source
Thrown at workers/sync-hub/src/do/SyncHub.ts:414
const sql = this.ctx.storage.sql;
const now = Date.now();
const nowDecimal = String(now);
const headBefore = this.headSeq();
const acked: AckedOp[] = [];
try {
this.ctx.storage.transactionSync(() => {
this.touchDevice(deviceId, normalizeDeviceName(deviceName), now);
for (const row of rows) {
const body = row.body;
const head = sql.exec<HeadRow>(
`SELECT entity_rev, operation_sha256, deleted, CAST(seq AS TEXT) AS seq
FROM entity_heads WHERE entity_id = ?`,
body.id,
).toArray()[0];
if (head) {
const order = compareCanonicalDecimals(body.entity_rev, head.entity_rev);
if (order < 0) throw invalid(`stale_revision:${body.id}:${body.entity_rev}<${head.entity_rev}`);
if (order === 0) {
if (row.operationSha256 !== head.operation_sha256) {
throw invalid(`revision_hash_conflict:${body.id}:${body.entity_rev}`);
}
acked.push({
id: body.id,
kind: body.kind,
origin_local_id: body.origin_local_id,
entity_rev: body.entity_rev,
operation_sha256: head.operation_sha256,
seq: String(head.seq),
});
continue;
}
}
const seq = incrementCanonicalDecimal(this.headSeq());
sql.exec(View on GitHub (pinned to d8bc9755e7)