{"record":{"id":"f502bf48888058f5","repo":"thedotmack/claude-mem","slug":"stale-revision-body-id-body-entity-rev-head-entity-rev","errorCode":null,"errorMessage":"stale_revision:${body.id}:${body.entity_rev}<${head.entity_rev}","messagePattern":"stale_revision:(.+?):(.+?)<(.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"workers/sync-hub/src/do/SyncHub.ts","lineNumber":414,"sourceCode":"\n\t\tconst sql = this.ctx.storage.sql;\n\t\tconst now = Date.now();\n\t\tconst nowDecimal = String(now);\n\t\tconst headBefore = this.headSeq();\n\t\tconst acked: AckedOp[] = [];\n\t\ttry {\n\t\t\tthis.ctx.storage.transactionSync(() => {\n\t\t\t\tthis.touchDevice(deviceId, normalizeDeviceName(deviceName), now);\n\t\t\t\tfor (const row of rows) {\n\t\t\t\t\tconst body = row.body;\n\t\t\t\t\tconst head = sql.exec<HeadRow>(\n\t\t\t\t\t\t`SELECT entity_rev, operation_sha256, deleted, CAST(seq AS TEXT) AS seq\n\t\t\t\t\t\t FROM entity_heads WHERE entity_id = ?`,\n\t\t\t\t\t\tbody.id,\n\t\t\t\t\t).toArray()[0];\n\t\t\t\t\tif (head) {\n\t\t\t\t\t\tconst order = compareCanonicalDecimals(body.entity_rev, head.entity_rev);\n\t\t\t\t\t\tif (order < 0) throw invalid(`stale_revision:${body.id}:${body.entity_rev}<${head.entity_rev}`);\n\t\t\t\t\t\tif (order === 0) {\n\t\t\t\t\t\t\tif (row.operationSha256 !== head.operation_sha256) {\n\t\t\t\t\t\t\t\tthrow invalid(`revision_hash_conflict:${body.id}:${body.entity_rev}`);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tacked.push({\n\t\t\t\t\t\t\t\tid: body.id,\n\t\t\t\t\t\t\t\tkind: body.kind,\n\t\t\t\t\t\t\t\torigin_local_id: body.origin_local_id,\n\t\t\t\t\t\t\t\tentity_rev: body.entity_rev,\n\t\t\t\t\t\t\t\toperation_sha256: head.operation_sha256,\n\t\t\t\t\t\t\t\tseq: String(head.seq),\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tconst seq = incrementCanonicalDecimal(this.headSeq());\n\t\t\t\t\tsql.exec(","sourceCodeStart":396,"sourceCodeEnd":432,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/workers/sync-hub/src/do/SyncHub.ts#L396-L432","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nawait push(entity, { entity_rev: localRev });\n// after\nconst head = await fetchEntityHead(entity.id);\nif (compare(localRev, head.entity_rev) < 0) {\n  localRev = rebaseOn(localChanges, head.entity_rev);\n}\nawait push(entity, { entity_rev: localRev });","handlingStrategy":"retry","validationCode":"const head = await fetchEntityHead(entityId);\nif (compareCanonicalDecimals(op.entity_rev, head.entity_rev) < 0) {\n  op = rebase(op, head.entity_rev);\n}","typeGuard":null,"tryCatchPattern":"try {\n  return await hub.push(deviceId, ops);\n} catch (e) {\n  if (String(e).startsWith('stale_revision:')) return rebaseAndRetry(e, ops);\n  throw e;\n}","preventionTips":["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"],"tags":["sync","conflict","revision"],"backgroundTag":"stale-revision-conflict","analyzedSha":"d8bc9755e74915e5c3b999181e10a67c889bce2a","analyzedAt":"2026-09-17T16:40:26.182Z","contentChangedAt":"2026-09-17T16:40:26.182Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}