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

  1. Pull latest changes (getChanges) for that entity, rebase local edits on head.entity_rev, and re-push with a greater revision
  2. Treat the refusal as a signal to surface a conflict-resolution UI to the user
  3. 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

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


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)