{"record":{"id":"c138aa6c8981987f","repo":"thedotmack/claude-mem","slug":"projection-error-unprojected-log-gap","errorCode":null,"errorMessage":"projection_error: unprojected log gap","messagePattern":"projection_error: unprojected log gap","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"workers/sync-hub/src/do/SyncHub.ts","lineNumber":721,"sourceCode":"\t\tconst ops: ChangeOp[] = [];\n\t\tfor (const row of rows) {\n\t\t\tconst op = toChange(row);\n\t\t\tconst candidate = [...ops, op];\n\t\t\tconst bytes = projectionRequestBytes({\n\t\t\t\tuserId,\n\t\t\t\tepoch,\n\t\t\t\tfromSeqExclusive: projected,\n\t\t\t\tthroughSeq: op.seq,\n\t\t\t\tops: candidate,\n\t\t\t});\n\t\t\tif (bytes > byteLimit) {\n\t\t\t\tif (ops.length === 0) throw projectionError(\"one operation exceeds projection request byte budget\");\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tops.push(op);\n\t\t}\n\t\tif (ops.length === 0 && compareCanonicalDecimals(projected, target) < 0) {\n\t\t\tthrow projectionError(\"unprojected log gap\");\n\t\t}\n\t\tthis.renewProjectionLease(leaseToken, now);\n\t\treturn {\n\t\t\tprotocol_version: 1,\n\t\t\tepoch,\n\t\t\tfrom_seq_exclusive: projected,\n\t\t\tthrough_seq: ops.length > 0 ? ops[ops.length - 1].seq : projected,\n\t\t\ttarget_seq: target,\n\t\t\tops,\n\t\t};\n\t}\n\n\theartbeatProjectionLease(leaseToken: string, now = Date.now()): ProjectionState {\n\t\tthis.assertLease(leaseToken, now);\n\t\tthis.renewProjectionLease(leaseToken, now);\n\t\treturn this.getProjectionState();\n\t}\n","sourceCodeStart":703,"sourceCodeEnd":739,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/workers/sync-hub/src/do/SyncHub.ts#L703-L739","documentation":"After paging, getProjectionPage() checks the invariant that if the caller asked for a target beyond the projected checkpoint, at least one op must have been returned. If zero ops came back while projected_seq < target_seq, the canonical log has a hole relative to expectations — the expected contiguous ops are missing — so the DO throws this projectionError to signal a broken invariant rather than silently returning an empty page.","triggerScenarios":"Calling getProjectionPage(token, target, userId) where projected_seq < target but the SQL range (projected, target] yields no rows: e.g. target_seq equals projected (no-op request should return empty, but if target > projected and the log gap exists), or ops between projected and target were deleted/compacted, or a non-canonical target confusing the lexicographic length-aware comparison.","commonSituations":"Manual DB maintenance/compaction removed canonical_ops rows without advancing projected_seq; restoring a DO snapshot from an older backup while meta says projected_seq is ahead; projector pointed at a target from a divergent epoch.","solutions":["Inspect the canonical_ops table for the (projected, target] range; if rows were deleted, restore them or reset projected_seq to the true earliest remaining seq via advanceProjectionCheckpoint under a lease.","Recover the DO from a snapshot taken before the gap, or trigger epoch rotation so state rebuilds cleanly.","Re-acquire the lease and retry once in case of transient read timing; persistent gaps indicate data loss, not a retryable condition.","Halt the projection loop on this error instead of spinning — it's an invariant violation requiring data repair."],"exampleFix":"// before\nwhile (projected < target) { page = hub.getProjectionPage(token, target, userId); }\n// after\ntry {\n  page = hub.getProjectionPage(token, target, userId);\n} catch (e) {\n  if (String(e).includes('unprojected log gap')) {\n    // stop and alert: canonical log hole needs repair, not retry\n  }\n}","handlingStrategy":"try-catch","validationCode":"const st = await hub.getProjectionState();\nif (st.projected_seq >= targetSeq) return; // nothing to project; avoid gap path","typeGuard":null,"tryCatchPattern":"try {\n  page = hub.getProjectionPage(token, target, userId);\n} catch (e) {\n  if (String(e).includes('unprojected log gap')) {\n    alertOps('canonical log gap — manual repair required', { projected, target });\n    return; // do NOT retry; invariant violation\n  }\n  throw e;\n}","preventionTips":["Never delete/compact canonical_ops rows without advancing projected_seq through the lease API","Monitor projection_lag_ops and halt on repeated gap errors","Take DO snapshots only at consistent points; avoid partial restores"],"tags":["projection","data-integrity","consistency"],"backgroundTag":"internal-invariant-violation","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"}