{"record":{"id":"c2b5dcde2f355132","repo":"thedotmack/claude-mem","slug":"projection-error-target-seq-exceeds-head-seq","errorCode":null,"errorMessage":"projection_error: target_seq exceeds head_seq","messagePattern":"projection_error: target_seq exceeds head_seq","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"workers/sync-hub/src/do/SyncHub.ts","lineNumber":639,"sourceCode":"\t\tconst normalizedId = deviceId.trim();\n\t\tconst normalizedName = normalizeDeviceName(name);\n\t\tif (normalizedId.length === 0 || normalizedId.length > 128) throw invalid(\"device_id must be 1-128 characters\");\n\t\tif (normalizedName === null) throw invalid(\"name must be 1-80 characters\");\n\t\treturn this.ctx.storage.sql.exec(\n\t\t\t\"UPDATE devices SET name = ? WHERE device_id = ?\",\n\t\t\tnormalizedName,\n\t\t\tnormalizedId,\n\t\t).rowsWritten > 0;\n\t}\n\n\t// ---------------------------------------------------------------------\n\t// Authoritative projection checkpoint and short per-user lease.\n\t// ---------------------------------------------------------------------\n\n\tacquireProjectionLease(targetSeq: string, now = Date.now()): ProjectionLease {\n\t\tconst target = assertCanonicalDecimal(targetSeq);\n\t\tconst head = this.headSeq();\n\t\tif (compareCanonicalDecimals(target, head) > 0) throw projectionError(\"target_seq exceeds head_seq\");\n\t\tconst projected = this.projectedSeq();\n\t\tconst existingToken = this.metaOptional(\"projection_lease_token\");\n\t\tconst nowValue = this.leaseNow(now);\n\t\tconst existingExpiry = this.metaOptional(\"projection_lease_expires_at\");\n\t\tif (existingToken && existingExpiry && compareCanonicalDecimals(existingExpiry, nowValue) > 0) {\n\t\t\treturn {\n\t\t\t\tacquired: false,\n\t\t\t\tepoch: this.meta(\"epoch\"),\n\t\t\t\thead_seq: head,\n\t\t\t\tprojected_seq: projected,\n\t\t\t\ttarget_seq: target,\n\t\t\t};\n\t\t}\n\t\tconst token = crypto.randomUUID();\n\t\tthis.ctx.storage.transactionSync(() => {\n\t\t\tthis.setMeta(\"projection_lease_token\", token);\n\t\t\tthis.setMeta(\"projection_lease_expires_at\", this.leaseExpiry(nowValue));\n\t\t});","sourceCodeStart":621,"sourceCodeEnd":657,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/workers/sync-hub/src/do/SyncHub.ts#L621-L657","documentation":"acquireProjectionLease(targetSeq) first canonicalizes the requested target sequence and compares it to the log's head sequence. If the projection target is ahead of everything currently committed to the canonical log, the DO throws this projectionError because there is nothing to project up to that sequence yet — a lease for a nonexistent frontier would let the projector checkpoint past real data.","triggerScenarios":"Calling acquireProjectionLease with a target_seq larger than headSeq(): e.g. a target string copied from a stale config, a padded/zero-prefixed decimal that fails canonical comparison semantics the caller didn't expect, or the caller computing a target from its own optimistic view of the log.","commonSituations":"Projector worker restarted with a cached target from a previous epoch; operator manually passing a sequence number instead of reading head_seq from getProjectionState(); clock/ordering issues where the caller assumed ops were committed that weren't yet.","solutions":["Fetch the current head via getProjectionState()/getMetadata() and request min(desiredTarget, head_seq) as the target.","Re-read head_seq immediately before acquiring; if ops may still be in flight, retry acquire after the append completes.","Verify the target is a canonical decimal string (no leading zeros, no '+' sign) since assertCanonicalDecimal runs first and non-canonical values fail earlier.","After an epoch change, discard old targets and re-read state from the hub."],"exampleFix":"// before\nconst lease = await hub.acquireProjectionLease(myCachedTargetSeq);\n// after\nconst state = await hub.getProjectionState();\nconst target = myCachedTargetSeq <= state.head_seq ? myCachedTargetSeq : state.head_seq;\nconst lease = await hub.acquireProjectionLease(target);","handlingStrategy":"validation","validationCode":"const head = (await hub.getProjectionState()).head_seq;\nif (compareDecimals(targetSeq, head) > 0) targetSeq = head; // clamp before acquire","typeGuard":"function isCanonicalDecimal(s: unknown): s is string {\n  return typeof s === 'string' && /^[0-9]+$/.test(s) && (s === '0' || s[0] !== '0');\n}","tryCatchPattern":"try {\n  lease = await hub.acquireProjectionLease(target);\n} catch (e) {\n  if (String(e).includes('target_seq exceeds head_seq')) {\n    const st = await hub.getProjectionState();\n    lease = await hub.acquireProjectionLease(st.head_seq);\n  } else throw e;\n}","preventionTips":["Always derive target_seq from hub state (head_seq), never from local counters","Validate canonical decimal format before calling","Re-read head after any append burst before acquiring"],"tags":["projection","consistency","sync"],"backgroundTag":"invalid-state-transition","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"}