diff --git a/collab-presence-heartbeat-liveness-guard/README.md b/collab-presence-heartbeat-liveness-guard/README.md
new file mode 100644
index 00000000..cb222a26
--- /dev/null
+++ b/collab-presence-heartbeat-liveness-guard/README.md
@@ -0,0 +1,35 @@
+# Collaborative Presence Heartbeat Liveness Guard
+
+This module adds a focused Real-Time Collaborative Editor slice for issue #12: heartbeat-based cleanup before live cursor and user-presence fanout.
+
+It evaluates synthetic collaboration rooms for:
+
+- stale heartbeats
+- ghost sessions that still show cursors or editing state
+- reconnect-grace sessions that should keep restore state but not broadcast cursors
+- duplicate collaborator tabs
+- orphaned cursors without a session record
+- expired typing indicators
+- expired presence fanout leases
+- future heartbeat clock skew
+
+The output is a sanitized presence snapshot plus cleanup, recovery, and fanout queues. The module is dependency-free and uses synthetic data only. It does not call live editor transports, identity services, notebook kernels, storage providers, notification systems, or external APIs.
+
+## Run
+
+```bash
+npm run check
+npm test
+npm run demo
+```
+
+`npm run demo` writes reviewer artifacts under `reports/`:
+
+- `presence-liveness-packet.json`
+- `presence-liveness-report.md`
+- `summary.svg`
+- `demo.mp4`
+
+## Scope
+
+This is intentionally separate from presence privacy, notification visibility, notebook kernel leases, lock/checkpoint recovery, autosave recovery, broad editor foundations, and release/export guards. It handles the liveness decision immediately before a presence room publishes cursor and activity snapshots.
diff --git a/collab-presence-heartbeat-liveness-guard/acceptance-notes.md b/collab-presence-heartbeat-liveness-guard/acceptance-notes.md
new file mode 100644
index 00000000..344ff068
--- /dev/null
+++ b/collab-presence-heartbeat-liveness-guard/acceptance-notes.md
@@ -0,0 +1,37 @@
+# Acceptance Notes
+
+The reviewer can verify this slice without accounts, services, or private data.
+
+## Expected Synthetic Result
+
+The demo fixture produces:
+
+- 2 rooms reviewed
+- 8 sessions reviewed
+- 3 sessions published
+- 5 sessions suppressed
+- 7 cleanup actions
+- 1 reconnect recovery action
+- 3 presence fanout events
+- 1 room held for broadcast cleanup
+- 1 room sanitized and ready
+
+The critical case is `sess-bio-ghost`: its heartbeat is stale while an editing cursor and typing state are still visible. The guard suppresses it and emits cleanup actions before the room fanout can show a ghost collaborator.
+
+## Validation
+
+```bash
+npm run check
+npm test
+npm run demo
+```
+
+Optional video metadata check:
+
+```bash
+ffprobe -v error -select_streams v:0 -show_entries stream=codec_name,width,height,duration -show_entries format=size,duration -of default=noprint_wrappers=1 reports/demo.mp4
+```
+
+## Boundaries
+
+This module does not implement the editor UI, network transport, CRDT/OT operations, section locking, notebook execution, privacy redaction, notification fanout, or release export. It is a deterministic pre-broadcast liveness guard for cursor and presence snapshots.
diff --git a/collab-presence-heartbeat-liveness-guard/demo.js b/collab-presence-heartbeat-liveness-guard/demo.js
new file mode 100644
index 00000000..8671ba02
--- /dev/null
+++ b/collab-presence-heartbeat-liveness-guard/demo.js
@@ -0,0 +1,91 @@
+const fs = require("node:fs")
+const path = require("node:path")
+const { spawnSync } = require("node:child_process")
+const { evaluatePresenceLiveness } = require("./index")
+const { rooms, policy } = require("./sample-data")
+
+const reportsDir = path.join(__dirname, "reports")
+fs.mkdirSync(reportsDir, { recursive: true })
+
+const packet = evaluatePresenceLiveness({
+ asOf: "2026-05-23T01:20:00.000Z",
+ rooms,
+ policy,
+})
+
+fs.writeFileSync(
+ path.join(reportsDir, "presence-liveness-packet.json"),
+ `${JSON.stringify(packet, null, 2)}\n`,
+)
+
+const markdown = [
+ "# Collaborative Presence Heartbeat Liveness Report",
+ "",
+ `Rooms reviewed: ${packet.summary.totalRooms}`,
+ `Sessions reviewed: ${packet.summary.totalSessions}`,
+ `Published sessions: ${packet.summary.publishedSessions}`,
+ `Suppressed sessions: ${packet.summary.suppressedSessions}`,
+ `Cleanup actions: ${packet.summary.cleanupActions}`,
+ `Recovery actions: ${packet.summary.recoveryActions}`,
+ `Fanout events: ${packet.summary.fanoutEvents}`,
+ `Audit digest: \`${packet.audit.digest}\``,
+ "",
+ "## Room Decisions",
+ ...packet.rooms.flatMap((room) => [
+ "",
+ `### ${room.title}`,
+ `- Status: ${room.status}`,
+ `- Reviewed sessions: ${room.reviewedSessions}`,
+ `- Published sessions: ${room.publishedSessions}`,
+ `- Suppressed sessions: ${room.suppressedSessions}`,
+ `- Cleanup codes: ${room.cleanupQueue.map((item) => item.code).join(", ") || "none"}`,
+ `- Recovery codes: ${room.recoveryQueue.map((item) => item.code).join(", ") || "none"}`,
+ ]),
+ "",
+ "## Cleanup Queue",
+ ...packet.cleanupQueue.map((item) => (
+ `- ${item.roomId}/${item.sessionId}: ${item.code} (${item.severity})`
+ )),
+ "",
+]
+
+fs.writeFileSync(path.join(reportsDir, "presence-liveness-report.md"), markdown.join("\n"))
+
+const svg = `
+`
+
+fs.writeFileSync(path.join(reportsDir, "summary.svg"), svg)
+
+const ffmpeg = spawnSync("ffmpeg", [
+ "-y",
+ "-f",
+ "lavfi",
+ "-i",
+ "color=c=0x101827:s=960x540:d=5:r=15",
+ "-vf",
+ "drawbox=x=48:y=170:w=250:h=150:color=0x047857@1:t=fill,drawbox=x=355:y=170:w=250:h=150:color=0xb45309@1:t=fill,drawbox=x=662:y=170:w=250:h=150:color=0xbe123c@1:t=fill,drawbox=x=48:y=368:w=864:h=18:color=0x22d3ee@1:t=fill",
+ "-pix_fmt",
+ "yuv420p",
+ path.join(reportsDir, "demo.mp4"),
+], { stdio: "ignore" })
+
+if (ffmpeg.status !== 0) {
+ console.warn("ffmpeg video generation failed; JSON, Markdown, and SVG reports were still generated.")
+}
+
+console.log(`Wrote collaborative presence liveness artifacts to ${reportsDir}`)
diff --git a/collab-presence-heartbeat-liveness-guard/index.js b/collab-presence-heartbeat-liveness-guard/index.js
new file mode 100644
index 00000000..cbb9bb59
--- /dev/null
+++ b/collab-presence-heartbeat-liveness-guard/index.js
@@ -0,0 +1,343 @@
+const crypto = require("node:crypto")
+
+const DEFAULT_POLICY = Object.freeze({
+ heartbeatStaleSeconds: 45,
+ ghostSessionSeconds: 120,
+ reconnectGraceSeconds: 90,
+ typingIndicatorSeconds: 20,
+ maxFutureClockSkewSeconds: 10,
+ duplicateTabWindowSeconds: 15,
+})
+
+function evaluatePresenceLiveness(input) {
+ const policy = { ...DEFAULT_POLICY, ...(input.policy || {}) }
+ const asOf = input.asOf || new Date().toISOString()
+ const now = Date.parse(asOf)
+ const rooms = Array.isArray(input.rooms) ? input.rooms : []
+ const roomDecisions = rooms.map((room) => evaluateRoom(room, now, policy))
+ const cleanupQueue = roomDecisions.flatMap((room) => room.cleanupQueue)
+ const recoveryQueue = roomDecisions.flatMap((room) => room.recoveryQueue)
+ const fanoutQueue = roomDecisions.flatMap((room) => room.fanoutQueue)
+ const summary = summarize(roomDecisions, cleanupQueue, recoveryQueue, fanoutQueue)
+ const auditPayload = {
+ asOf,
+ policy,
+ summary,
+ rooms: roomDecisions.map((room) => ({
+ roomId: room.roomId,
+ status: room.status,
+ publishedSessions: room.sanitizedPresence.sessions.map((session) => session.sessionId),
+ cleanupCodes: room.cleanupQueue.map((item) => item.code),
+ recoveryCodes: room.recoveryQueue.map((item) => item.code),
+ })),
+ }
+
+ return {
+ asOf,
+ summary,
+ rooms: roomDecisions,
+ cleanupQueue,
+ recoveryQueue,
+ fanoutQueue,
+ audit: {
+ algorithm: "sha256",
+ digest: digest(auditPayload),
+ policyVersion: policy.version || "presence-heartbeat-liveness-v1",
+ },
+ }
+}
+
+function evaluateRoom(room, now, policy) {
+ const sessions = Array.isArray(room.sessions) ? room.sessions : []
+ const cursors = new Map((room.cursors || []).map((cursor) => [cursor.sessionId, cursor]))
+ const leases = new Map((room.presenceLeases || []).map((lease) => [lease.sessionId, lease]))
+ const duplicateWinners = selectDuplicateWinners(sessions, now)
+ const sessionDecisions = sessions.map((session) => {
+ return evaluateSession({
+ room,
+ session,
+ cursor: cursors.get(session.sessionId),
+ lease: leases.get(session.sessionId),
+ isDuplicateLoser: duplicateWinners.get(session.collaboratorId) !== session.sessionId,
+ now,
+ policy,
+ })
+ })
+
+ const knownSessionIds = new Set(sessions.map((session) => session.sessionId))
+ const orphanCursorActions = (room.cursors || [])
+ .filter((cursor) => !knownSessionIds.has(cursor.sessionId))
+ .map((cursor) => cleanupAction({
+ roomId: room.roomId,
+ sessionId: cursor.sessionId,
+ collaboratorId: "unknown",
+ code: "orphan_cursor",
+ severity: "warning",
+ message: "Cursor has no matching live session record.",
+ remediation: "Drop cursor from the next presence snapshot.",
+ }))
+
+ const cleanupQueue = [
+ ...sessionDecisions.flatMap((decision) => decision.cleanupActions),
+ ...orphanCursorActions,
+ ]
+ const recoveryQueue = sessionDecisions.flatMap((decision) => decision.recoveryActions)
+ const fanoutQueue = sessionDecisions
+ .filter((decision) => decision.broadcast)
+ .map((decision) => ({
+ roomId: room.roomId,
+ sessionId: decision.sessionId,
+ collaboratorId: decision.collaboratorId,
+ channel: "presence_snapshot",
+ cursor: decision.publicCursor,
+ typing: decision.publicTyping,
+ }))
+
+ const sanitizedPresence = {
+ roomId: room.roomId,
+ documentId: room.documentId,
+ sessions: sessionDecisions
+ .filter((decision) => decision.broadcast)
+ .map((decision) => ({
+ sessionId: decision.sessionId,
+ collaboratorId: decision.collaboratorId,
+ displayName: decision.displayName,
+ status: decision.publicStatus,
+ cursor: decision.publicCursor,
+ typing: decision.publicTyping,
+ })),
+ }
+
+ const critical = cleanupQueue.some((item) => item.severity === "critical")
+ const warning = cleanupQueue.length > 0 || recoveryQueue.length > 0
+ const status = critical ? "hold_broadcast_cleanup" : warning ? "sanitized_ready" : "ready"
+
+ return {
+ roomId: room.roomId,
+ documentId: room.documentId,
+ title: room.title,
+ status,
+ reviewedSessions: sessions.length,
+ publishedSessions: sanitizedPresence.sessions.length,
+ suppressedSessions: sessionDecisions.filter((decision) => !decision.broadcast).length,
+ sanitizedPresence,
+ sessionDecisions,
+ cleanupQueue,
+ recoveryQueue,
+ fanoutQueue,
+ }
+}
+
+function evaluateSession({ room, session, cursor, lease, isDuplicateLoser, now, policy }) {
+ const heartbeatAge = secondsBetween(now, session.lastHeartbeatAt)
+ const disconnectedAge = session.disconnectedAt ? secondsBetween(now, session.disconnectedAt) : null
+ const typingAge = session.typingSince ? secondsBetween(now, session.typingSince) : null
+ const leaseAge = lease ? secondsBetween(now, lease.expiresAt) : null
+ const cleanupActions = []
+ const recoveryActions = []
+ const futureHeartbeat = heartbeatAge < -policy.maxFutureClockSkewSeconds
+ const staleHeartbeat = heartbeatAge > policy.heartbeatStaleSeconds
+ const ghostSession = heartbeatAge > policy.ghostSessionSeconds && Boolean(cursor || session.status === "editing")
+ const reconnecting = session.status === "disconnected" && disconnectedAge !== null && disconnectedAge <= policy.reconnectGraceSeconds
+ const expiredTyping = typingAge !== null && typingAge > policy.typingIndicatorSeconds
+ const expiredLease = lease && leaseAge > 0
+
+ let broadcast = true
+ let publicStatus = session.status || "active"
+ let publicCursor = cursor ? sanitizeCursor(cursor) : null
+ let publicTyping = Boolean(session.typingSince)
+
+ if (futureHeartbeat) {
+ cleanupActions.push(cleanupAction({
+ roomId: room.roomId,
+ sessionId: session.sessionId,
+ collaboratorId: session.collaboratorId,
+ code: "future_heartbeat_clock_skew",
+ severity: "warning",
+ message: "Heartbeat timestamp is ahead of the room clock.",
+ remediation: "Hold the timestamp for clock-skew review before fanout.",
+ }))
+ publicStatus = "clock_skew_review"
+ publicCursor = null
+ publicTyping = false
+ broadcast = false
+ }
+
+ if (isDuplicateLoser) {
+ cleanupActions.push(cleanupAction({
+ roomId: room.roomId,
+ sessionId: session.sessionId,
+ collaboratorId: session.collaboratorId,
+ code: "duplicate_tab_shadowed",
+ severity: "warning",
+ message: "A newer tab for the same collaborator is active.",
+ remediation: "Keep only the newest tab in the next presence fanout.",
+ }))
+ broadcast = false
+ }
+
+ if (ghostSession) {
+ cleanupActions.push(cleanupAction({
+ roomId: room.roomId,
+ sessionId: session.sessionId,
+ collaboratorId: session.collaboratorId,
+ code: "ghost_session",
+ severity: "critical",
+ message: `Heartbeat is stale by ${heartbeatAge}s while cursor or editing state is still visible.`,
+ remediation: "Suppress cursor and remove the session from live fanout until a fresh heartbeat arrives.",
+ }))
+ broadcast = false
+ } else if (staleHeartbeat && !reconnecting) {
+ cleanupActions.push(cleanupAction({
+ roomId: room.roomId,
+ sessionId: session.sessionId,
+ collaboratorId: session.collaboratorId,
+ code: "stale_heartbeat",
+ severity: "warning",
+ message: `Heartbeat age ${heartbeatAge}s exceeds the ${policy.heartbeatStaleSeconds}s policy.`,
+ remediation: "Hide cursor and ask the client to refresh presence.",
+ }))
+ broadcast = false
+ }
+
+ if (reconnecting) {
+ recoveryActions.push({
+ roomId: room.roomId,
+ sessionId: session.sessionId,
+ collaboratorId: session.collaboratorId,
+ code: "reconnect_grace",
+ action: "hold_cursor_restore_marker",
+ expiresInSeconds: Math.max(0, policy.reconnectGraceSeconds - disconnectedAge),
+ })
+ publicStatus = "reconnecting"
+ publicCursor = null
+ publicTyping = false
+ broadcast = false
+ }
+
+ if (expiredTyping) {
+ cleanupActions.push(cleanupAction({
+ roomId: room.roomId,
+ sessionId: session.sessionId,
+ collaboratorId: session.collaboratorId,
+ code: "expired_typing_indicator",
+ severity: "warning",
+ message: `Typing indicator age ${typingAge}s exceeds the ${policy.typingIndicatorSeconds}s policy.`,
+ remediation: "Drop typing state while preserving the session if heartbeat is fresh.",
+ }))
+ publicTyping = false
+ }
+
+ if (expiredLease) {
+ cleanupActions.push(cleanupAction({
+ roomId: room.roomId,
+ sessionId: session.sessionId,
+ collaboratorId: session.collaboratorId,
+ code: "expired_presence_fanout_lease",
+ severity: "warning",
+ message: "Presence fanout lease expired before the next broadcast window.",
+ remediation: "Renew lease before including this session in a fanout batch.",
+ }))
+ broadcast = false
+ }
+
+ return {
+ roomId: room.roomId,
+ sessionId: session.sessionId,
+ collaboratorId: session.collaboratorId,
+ displayName: session.displayName,
+ heartbeatAge,
+ broadcast,
+ publicStatus,
+ publicCursor,
+ publicTyping,
+ cleanupActions,
+ recoveryActions,
+ }
+}
+
+function selectDuplicateWinners(sessions, now) {
+ const byCollaborator = new Map()
+ for (const session of sessions) {
+ const previous = byCollaborator.get(session.collaboratorId)
+ if (!previous || Date.parse(session.lastHeartbeatAt) > Date.parse(previous.lastHeartbeatAt)) {
+ byCollaborator.set(session.collaboratorId, session)
+ }
+ }
+
+ const winners = new Map()
+ for (const [collaboratorId, session] of byCollaborator.entries()) {
+ const sameCollaborator = sessions.filter((candidate) => candidate.collaboratorId === collaboratorId)
+ if (sameCollaborator.length === 1) {
+ winners.set(collaboratorId, session.sessionId)
+ continue
+ }
+
+ const winnerAge = secondsBetween(now, session.lastHeartbeatAt)
+ if (winnerAge <= DEFAULT_POLICY.duplicateTabWindowSeconds || session.status === "active") {
+ winners.set(collaboratorId, session.sessionId)
+ } else {
+ winners.set(collaboratorId, session.sessionId)
+ }
+ }
+
+ return winners
+}
+
+function sanitizeCursor(cursor) {
+ return {
+ sectionId: cursor.sectionId,
+ blockId: cursor.blockId,
+ offset: Number.isFinite(Number(cursor.offset)) ? Number(cursor.offset) : 0,
+ }
+}
+
+function cleanupAction({ roomId, sessionId, collaboratorId, code, severity, message, remediation }) {
+ return { roomId, sessionId, collaboratorId, code, severity, message, remediation }
+}
+
+function summarize(rooms, cleanupQueue, recoveryQueue, fanoutQueue) {
+ return {
+ totalRooms: rooms.length,
+ totalSessions: rooms.reduce((sum, room) => sum + room.reviewedSessions, 0),
+ publishedSessions: rooms.reduce((sum, room) => sum + room.publishedSessions, 0),
+ suppressedSessions: rooms.reduce((sum, room) => sum + room.suppressedSessions, 0),
+ cleanupActions: cleanupQueue.length,
+ recoveryActions: recoveryQueue.length,
+ fanoutEvents: fanoutQueue.length,
+ holdRooms: rooms.filter((room) => room.status === "hold_broadcast_cleanup").length,
+ sanitizedRooms: rooms.filter((room) => room.status === "sanitized_ready").length,
+ readyRooms: rooms.filter((room) => room.status === "ready").length,
+ }
+}
+
+function secondsBetween(nowMillis, timestamp) {
+ const parsed = Date.parse(timestamp)
+ if (!Number.isFinite(parsed)) {
+ return Number.POSITIVE_INFINITY
+ }
+
+ return Math.round((nowMillis - parsed) / 1000)
+}
+
+function stableStringify(value) {
+ if (Array.isArray(value)) {
+ return `[${value.map(stableStringify).join(",")}]`
+ }
+
+ if (value && typeof value === "object") {
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`
+ }
+
+ return JSON.stringify(value)
+}
+
+function digest(value) {
+ return crypto.createHash("sha256").update(stableStringify(value)).digest("hex")
+}
+
+module.exports = {
+ DEFAULT_POLICY,
+ evaluatePresenceLiveness,
+ stableStringify,
+}
diff --git a/collab-presence-heartbeat-liveness-guard/package.json b/collab-presence-heartbeat-liveness-guard/package.json
new file mode 100644
index 00000000..cc761d3c
--- /dev/null
+++ b/collab-presence-heartbeat-liveness-guard/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "collab-presence-heartbeat-liveness-guard",
+ "version": "1.0.0",
+ "private": true,
+ "type": "commonjs",
+ "scripts": {
+ "check": "node --check index.js && node --check sample-data.js && node --check test.js && node --check demo.js",
+ "test": "node test.js",
+ "demo": "node demo.js"
+ }
+}
diff --git a/collab-presence-heartbeat-liveness-guard/reports/demo.mp4 b/collab-presence-heartbeat-liveness-guard/reports/demo.mp4
new file mode 100644
index 00000000..a7c27f31
Binary files /dev/null and b/collab-presence-heartbeat-liveness-guard/reports/demo.mp4 differ
diff --git a/collab-presence-heartbeat-liveness-guard/reports/presence-liveness-packet.json b/collab-presence-heartbeat-liveness-guard/reports/presence-liveness-packet.json
new file mode 100644
index 00000000..2dd7961d
--- /dev/null
+++ b/collab-presence-heartbeat-liveness-guard/reports/presence-liveness-packet.json
@@ -0,0 +1,518 @@
+{
+ "asOf": "2026-05-23T01:20:00.000Z",
+ "summary": {
+ "totalRooms": 2,
+ "totalSessions": 8,
+ "publishedSessions": 3,
+ "suppressedSessions": 5,
+ "cleanupActions": 7,
+ "recoveryActions": 1,
+ "fanoutEvents": 3,
+ "holdRooms": 1,
+ "sanitizedRooms": 1,
+ "readyRooms": 0
+ },
+ "rooms": [
+ {
+ "roomId": "manuscript-alpha",
+ "documentId": "doc-alpha",
+ "title": "Collaborative manuscript draft",
+ "status": "hold_broadcast_cleanup",
+ "reviewedSessions": 5,
+ "publishedSessions": 2,
+ "suppressedSessions": 3,
+ "sanitizedPresence": {
+ "roomId": "manuscript-alpha",
+ "documentId": "doc-alpha",
+ "sessions": [
+ {
+ "sessionId": "sess-ada-active",
+ "collaboratorId": "ada",
+ "displayName": "Ada",
+ "status": "active",
+ "cursor": {
+ "sectionId": "intro",
+ "blockId": "intro-2",
+ "offset": 81
+ },
+ "typing": true
+ },
+ {
+ "sessionId": "sess-chen-new-tab",
+ "collaboratorId": "chen",
+ "displayName": "Chen",
+ "status": "active",
+ "cursor": {
+ "sectionId": "results",
+ "blockId": "result-2",
+ "offset": 33
+ },
+ "typing": false
+ }
+ ]
+ },
+ "sessionDecisions": [
+ {
+ "roomId": "manuscript-alpha",
+ "sessionId": "sess-ada-active",
+ "collaboratorId": "ada",
+ "displayName": "Ada",
+ "heartbeatAge": 15,
+ "broadcast": true,
+ "publicStatus": "active",
+ "publicCursor": {
+ "sectionId": "intro",
+ "blockId": "intro-2",
+ "offset": 81
+ },
+ "publicTyping": true,
+ "cleanupActions": [],
+ "recoveryActions": []
+ },
+ {
+ "roomId": "manuscript-alpha",
+ "sessionId": "sess-bio-ghost",
+ "collaboratorId": "bianca",
+ "displayName": "Bianca",
+ "heartbeatAge": 165,
+ "broadcast": false,
+ "publicStatus": "editing",
+ "publicCursor": {
+ "sectionId": "methods",
+ "blockId": "methods-4",
+ "offset": 12
+ },
+ "publicTyping": false,
+ "cleanupActions": [
+ {
+ "roomId": "manuscript-alpha",
+ "sessionId": "sess-bio-ghost",
+ "collaboratorId": "bianca",
+ "code": "ghost_session",
+ "severity": "critical",
+ "message": "Heartbeat is stale by 165s while cursor or editing state is still visible.",
+ "remediation": "Suppress cursor and remove the session from live fanout until a fresh heartbeat arrives."
+ },
+ {
+ "roomId": "manuscript-alpha",
+ "sessionId": "sess-bio-ghost",
+ "collaboratorId": "bianca",
+ "code": "expired_typing_indicator",
+ "severity": "warning",
+ "message": "Typing indicator age 150s exceeds the 20s policy.",
+ "remediation": "Drop typing state while preserving the session if heartbeat is fresh."
+ }
+ ],
+ "recoveryActions": []
+ },
+ {
+ "roomId": "manuscript-alpha",
+ "sessionId": "sess-chen-old-tab",
+ "collaboratorId": "chen",
+ "displayName": "Chen",
+ "heartbeatAge": 35,
+ "broadcast": false,
+ "publicStatus": "active",
+ "publicCursor": {
+ "sectionId": "results",
+ "blockId": "result-1",
+ "offset": 3
+ },
+ "publicTyping": false,
+ "cleanupActions": [
+ {
+ "roomId": "manuscript-alpha",
+ "sessionId": "sess-chen-old-tab",
+ "collaboratorId": "chen",
+ "code": "duplicate_tab_shadowed",
+ "severity": "warning",
+ "message": "A newer tab for the same collaborator is active.",
+ "remediation": "Keep only the newest tab in the next presence fanout."
+ }
+ ],
+ "recoveryActions": []
+ },
+ {
+ "roomId": "manuscript-alpha",
+ "sessionId": "sess-chen-new-tab",
+ "collaboratorId": "chen",
+ "displayName": "Chen",
+ "heartbeatAge": 5,
+ "broadcast": true,
+ "publicStatus": "active",
+ "publicCursor": {
+ "sectionId": "results",
+ "blockId": "result-2",
+ "offset": 33
+ },
+ "publicTyping": false,
+ "cleanupActions": [],
+ "recoveryActions": []
+ },
+ {
+ "roomId": "manuscript-alpha",
+ "sessionId": "sess-dina-reconnect",
+ "collaboratorId": "dina",
+ "displayName": "Dina",
+ "heartbeatAge": 58,
+ "broadcast": false,
+ "publicStatus": "reconnecting",
+ "publicCursor": null,
+ "publicTyping": false,
+ "cleanupActions": [],
+ "recoveryActions": [
+ {
+ "roomId": "manuscript-alpha",
+ "sessionId": "sess-dina-reconnect",
+ "collaboratorId": "dina",
+ "code": "reconnect_grace",
+ "action": "hold_cursor_restore_marker",
+ "expiresInSeconds": 50
+ }
+ ]
+ }
+ ],
+ "cleanupQueue": [
+ {
+ "roomId": "manuscript-alpha",
+ "sessionId": "sess-bio-ghost",
+ "collaboratorId": "bianca",
+ "code": "ghost_session",
+ "severity": "critical",
+ "message": "Heartbeat is stale by 165s while cursor or editing state is still visible.",
+ "remediation": "Suppress cursor and remove the session from live fanout until a fresh heartbeat arrives."
+ },
+ {
+ "roomId": "manuscript-alpha",
+ "sessionId": "sess-bio-ghost",
+ "collaboratorId": "bianca",
+ "code": "expired_typing_indicator",
+ "severity": "warning",
+ "message": "Typing indicator age 150s exceeds the 20s policy.",
+ "remediation": "Drop typing state while preserving the session if heartbeat is fresh."
+ },
+ {
+ "roomId": "manuscript-alpha",
+ "sessionId": "sess-chen-old-tab",
+ "collaboratorId": "chen",
+ "code": "duplicate_tab_shadowed",
+ "severity": "warning",
+ "message": "A newer tab for the same collaborator is active.",
+ "remediation": "Keep only the newest tab in the next presence fanout."
+ },
+ {
+ "roomId": "manuscript-alpha",
+ "sessionId": "missing-session",
+ "collaboratorId": "unknown",
+ "code": "orphan_cursor",
+ "severity": "warning",
+ "message": "Cursor has no matching live session record.",
+ "remediation": "Drop cursor from the next presence snapshot."
+ }
+ ],
+ "recoveryQueue": [
+ {
+ "roomId": "manuscript-alpha",
+ "sessionId": "sess-dina-reconnect",
+ "collaboratorId": "dina",
+ "code": "reconnect_grace",
+ "action": "hold_cursor_restore_marker",
+ "expiresInSeconds": 50
+ }
+ ],
+ "fanoutQueue": [
+ {
+ "roomId": "manuscript-alpha",
+ "sessionId": "sess-ada-active",
+ "collaboratorId": "ada",
+ "channel": "presence_snapshot",
+ "cursor": {
+ "sectionId": "intro",
+ "blockId": "intro-2",
+ "offset": 81
+ },
+ "typing": true
+ },
+ {
+ "roomId": "manuscript-alpha",
+ "sessionId": "sess-chen-new-tab",
+ "collaboratorId": "chen",
+ "channel": "presence_snapshot",
+ "cursor": {
+ "sectionId": "results",
+ "blockId": "result-2",
+ "offset": 33
+ },
+ "typing": false
+ }
+ ]
+ },
+ {
+ "roomId": "notebook-review",
+ "documentId": "doc-notebook",
+ "title": "Notebook review room",
+ "status": "sanitized_ready",
+ "reviewedSessions": 3,
+ "publishedSessions": 1,
+ "suppressedSessions": 2,
+ "sanitizedPresence": {
+ "roomId": "notebook-review",
+ "documentId": "doc-notebook",
+ "sessions": [
+ {
+ "sessionId": "sess-eli-clean",
+ "collaboratorId": "eli",
+ "displayName": "Eli",
+ "status": "active",
+ "cursor": {
+ "sectionId": "analysis",
+ "blockId": "cell-2",
+ "offset": 18
+ },
+ "typing": false
+ }
+ ]
+ },
+ "sessionDecisions": [
+ {
+ "roomId": "notebook-review",
+ "sessionId": "sess-eli-clean",
+ "collaboratorId": "eli",
+ "displayName": "Eli",
+ "heartbeatAge": 12,
+ "broadcast": true,
+ "publicStatus": "active",
+ "publicCursor": {
+ "sectionId": "analysis",
+ "blockId": "cell-2",
+ "offset": 18
+ },
+ "publicTyping": false,
+ "cleanupActions": [],
+ "recoveryActions": []
+ },
+ {
+ "roomId": "notebook-review",
+ "sessionId": "sess-farid-expired-lease",
+ "collaboratorId": "farid",
+ "displayName": "Farid",
+ "heartbeatAge": 10,
+ "broadcast": false,
+ "publicStatus": "active",
+ "publicCursor": {
+ "sectionId": "analysis",
+ "blockId": "cell-5",
+ "offset": 44
+ },
+ "publicTyping": false,
+ "cleanupActions": [
+ {
+ "roomId": "notebook-review",
+ "sessionId": "sess-farid-expired-lease",
+ "collaboratorId": "farid",
+ "code": "expired_typing_indicator",
+ "severity": "warning",
+ "message": "Typing indicator age 65s exceeds the 20s policy.",
+ "remediation": "Drop typing state while preserving the session if heartbeat is fresh."
+ },
+ {
+ "roomId": "notebook-review",
+ "sessionId": "sess-farid-expired-lease",
+ "collaboratorId": "farid",
+ "code": "expired_presence_fanout_lease",
+ "severity": "warning",
+ "message": "Presence fanout lease expired before the next broadcast window.",
+ "remediation": "Renew lease before including this session in a fanout batch."
+ }
+ ],
+ "recoveryActions": []
+ },
+ {
+ "roomId": "notebook-review",
+ "sessionId": "sess-gita-skew",
+ "collaboratorId": "gita",
+ "displayName": "Gita",
+ "heartbeatAge": -22,
+ "broadcast": false,
+ "publicStatus": "clock_skew_review",
+ "publicCursor": null,
+ "publicTyping": false,
+ "cleanupActions": [
+ {
+ "roomId": "notebook-review",
+ "sessionId": "sess-gita-skew",
+ "collaboratorId": "gita",
+ "code": "future_heartbeat_clock_skew",
+ "severity": "warning",
+ "message": "Heartbeat timestamp is ahead of the room clock.",
+ "remediation": "Hold the timestamp for clock-skew review before fanout."
+ }
+ ],
+ "recoveryActions": []
+ }
+ ],
+ "cleanupQueue": [
+ {
+ "roomId": "notebook-review",
+ "sessionId": "sess-farid-expired-lease",
+ "collaboratorId": "farid",
+ "code": "expired_typing_indicator",
+ "severity": "warning",
+ "message": "Typing indicator age 65s exceeds the 20s policy.",
+ "remediation": "Drop typing state while preserving the session if heartbeat is fresh."
+ },
+ {
+ "roomId": "notebook-review",
+ "sessionId": "sess-farid-expired-lease",
+ "collaboratorId": "farid",
+ "code": "expired_presence_fanout_lease",
+ "severity": "warning",
+ "message": "Presence fanout lease expired before the next broadcast window.",
+ "remediation": "Renew lease before including this session in a fanout batch."
+ },
+ {
+ "roomId": "notebook-review",
+ "sessionId": "sess-gita-skew",
+ "collaboratorId": "gita",
+ "code": "future_heartbeat_clock_skew",
+ "severity": "warning",
+ "message": "Heartbeat timestamp is ahead of the room clock.",
+ "remediation": "Hold the timestamp for clock-skew review before fanout."
+ }
+ ],
+ "recoveryQueue": [],
+ "fanoutQueue": [
+ {
+ "roomId": "notebook-review",
+ "sessionId": "sess-eli-clean",
+ "collaboratorId": "eli",
+ "channel": "presence_snapshot",
+ "cursor": {
+ "sectionId": "analysis",
+ "blockId": "cell-2",
+ "offset": 18
+ },
+ "typing": false
+ }
+ ]
+ }
+ ],
+ "cleanupQueue": [
+ {
+ "roomId": "manuscript-alpha",
+ "sessionId": "sess-bio-ghost",
+ "collaboratorId": "bianca",
+ "code": "ghost_session",
+ "severity": "critical",
+ "message": "Heartbeat is stale by 165s while cursor or editing state is still visible.",
+ "remediation": "Suppress cursor and remove the session from live fanout until a fresh heartbeat arrives."
+ },
+ {
+ "roomId": "manuscript-alpha",
+ "sessionId": "sess-bio-ghost",
+ "collaboratorId": "bianca",
+ "code": "expired_typing_indicator",
+ "severity": "warning",
+ "message": "Typing indicator age 150s exceeds the 20s policy.",
+ "remediation": "Drop typing state while preserving the session if heartbeat is fresh."
+ },
+ {
+ "roomId": "manuscript-alpha",
+ "sessionId": "sess-chen-old-tab",
+ "collaboratorId": "chen",
+ "code": "duplicate_tab_shadowed",
+ "severity": "warning",
+ "message": "A newer tab for the same collaborator is active.",
+ "remediation": "Keep only the newest tab in the next presence fanout."
+ },
+ {
+ "roomId": "manuscript-alpha",
+ "sessionId": "missing-session",
+ "collaboratorId": "unknown",
+ "code": "orphan_cursor",
+ "severity": "warning",
+ "message": "Cursor has no matching live session record.",
+ "remediation": "Drop cursor from the next presence snapshot."
+ },
+ {
+ "roomId": "notebook-review",
+ "sessionId": "sess-farid-expired-lease",
+ "collaboratorId": "farid",
+ "code": "expired_typing_indicator",
+ "severity": "warning",
+ "message": "Typing indicator age 65s exceeds the 20s policy.",
+ "remediation": "Drop typing state while preserving the session if heartbeat is fresh."
+ },
+ {
+ "roomId": "notebook-review",
+ "sessionId": "sess-farid-expired-lease",
+ "collaboratorId": "farid",
+ "code": "expired_presence_fanout_lease",
+ "severity": "warning",
+ "message": "Presence fanout lease expired before the next broadcast window.",
+ "remediation": "Renew lease before including this session in a fanout batch."
+ },
+ {
+ "roomId": "notebook-review",
+ "sessionId": "sess-gita-skew",
+ "collaboratorId": "gita",
+ "code": "future_heartbeat_clock_skew",
+ "severity": "warning",
+ "message": "Heartbeat timestamp is ahead of the room clock.",
+ "remediation": "Hold the timestamp for clock-skew review before fanout."
+ }
+ ],
+ "recoveryQueue": [
+ {
+ "roomId": "manuscript-alpha",
+ "sessionId": "sess-dina-reconnect",
+ "collaboratorId": "dina",
+ "code": "reconnect_grace",
+ "action": "hold_cursor_restore_marker",
+ "expiresInSeconds": 50
+ }
+ ],
+ "fanoutQueue": [
+ {
+ "roomId": "manuscript-alpha",
+ "sessionId": "sess-ada-active",
+ "collaboratorId": "ada",
+ "channel": "presence_snapshot",
+ "cursor": {
+ "sectionId": "intro",
+ "blockId": "intro-2",
+ "offset": 81
+ },
+ "typing": true
+ },
+ {
+ "roomId": "manuscript-alpha",
+ "sessionId": "sess-chen-new-tab",
+ "collaboratorId": "chen",
+ "channel": "presence_snapshot",
+ "cursor": {
+ "sectionId": "results",
+ "blockId": "result-2",
+ "offset": 33
+ },
+ "typing": false
+ },
+ {
+ "roomId": "notebook-review",
+ "sessionId": "sess-eli-clean",
+ "collaboratorId": "eli",
+ "channel": "presence_snapshot",
+ "cursor": {
+ "sectionId": "analysis",
+ "blockId": "cell-2",
+ "offset": 18
+ },
+ "typing": false
+ }
+ ],
+ "audit": {
+ "algorithm": "sha256",
+ "digest": "4a2254121a88799219fbeb58149dc0cc31087ce21619de6817fa5963bf7b55ba",
+ "policyVersion": "presence-heartbeat-liveness-v1"
+ }
+}
diff --git a/collab-presence-heartbeat-liveness-guard/reports/presence-liveness-report.md b/collab-presence-heartbeat-liveness-guard/reports/presence-liveness-report.md
new file mode 100644
index 00000000..0bd6143c
--- /dev/null
+++ b/collab-presence-heartbeat-liveness-guard/reports/presence-liveness-report.md
@@ -0,0 +1,37 @@
+# Collaborative Presence Heartbeat Liveness Report
+
+Rooms reviewed: 2
+Sessions reviewed: 8
+Published sessions: 3
+Suppressed sessions: 5
+Cleanup actions: 7
+Recovery actions: 1
+Fanout events: 3
+Audit digest: `4a2254121a88799219fbeb58149dc0cc31087ce21619de6817fa5963bf7b55ba`
+
+## Room Decisions
+
+### Collaborative manuscript draft
+- Status: hold_broadcast_cleanup
+- Reviewed sessions: 5
+- Published sessions: 2
+- Suppressed sessions: 3
+- Cleanup codes: ghost_session, expired_typing_indicator, duplicate_tab_shadowed, orphan_cursor
+- Recovery codes: reconnect_grace
+
+### Notebook review room
+- Status: sanitized_ready
+- Reviewed sessions: 3
+- Published sessions: 1
+- Suppressed sessions: 2
+- Cleanup codes: expired_typing_indicator, expired_presence_fanout_lease, future_heartbeat_clock_skew
+- Recovery codes: none
+
+## Cleanup Queue
+- manuscript-alpha/sess-bio-ghost: ghost_session (critical)
+- manuscript-alpha/sess-bio-ghost: expired_typing_indicator (warning)
+- manuscript-alpha/sess-chen-old-tab: duplicate_tab_shadowed (warning)
+- manuscript-alpha/missing-session: orphan_cursor (warning)
+- notebook-review/sess-farid-expired-lease: expired_typing_indicator (warning)
+- notebook-review/sess-farid-expired-lease: expired_presence_fanout_lease (warning)
+- notebook-review/sess-gita-skew: future_heartbeat_clock_skew (warning)
diff --git a/collab-presence-heartbeat-liveness-guard/reports/summary.svg b/collab-presence-heartbeat-liveness-guard/reports/summary.svg
new file mode 100644
index 00000000..0c0ce961
--- /dev/null
+++ b/collab-presence-heartbeat-liveness-guard/reports/summary.svg
@@ -0,0 +1,16 @@
+
diff --git a/collab-presence-heartbeat-liveness-guard/requirements-map.md b/collab-presence-heartbeat-liveness-guard/requirements-map.md
new file mode 100644
index 00000000..d0913e3d
--- /dev/null
+++ b/collab-presence-heartbeat-liveness-guard/requirements-map.md
@@ -0,0 +1,19 @@
+# Requirements Map
+
+Issue #12 asks for a real-time collaborative research editor with live cursor tracking, user presence indicators, activity status, comments, suggestions, section controls, autosave/versioning, and notebook collaboration. This module covers the liveness safety layer that must run before cursor and presence broadcasts.
+
+| Issue #12 area | Implementation |
+| --- | --- |
+| Multi-user editing | Reviews active collaboration sessions before they are included in room fanout. |
+| Live cursor tracking | Drops stale, ghost, duplicate, and orphan cursor records before broadcast. |
+| User presence indicators | Emits sanitized active/reconnecting status and suppresses stale presence. |
+| Activity status | Expires typing indicators independently from session presence. |
+| Controlled collaboration | Removes expired presence fanout leases before clients receive stale room state. |
+| Autosave/local continuity | Keeps reconnect-grace restore markers without leaking stale cursors. |
+| Reviewer-ready governance | Produces deterministic cleanup queues, recovery queues, fanout events, and audit digests. |
+
+## Non-Overlap Notes
+
+Existing issue #12 submissions cover broad editor foundations, deterministic operation replay, offline conflict rebasing, notebook workbench and kernel leases, reference formatting and merging, authorship/submission governance, lock/checkpoint recovery, review freeze lanes, figure/table lanes, discussion/sidebar audit, autosave recovery, round-trip fidelity, review decisions, task dependencies, equation/figure anchors, presence privacy, accessibility parity, evidence binding, embargo release, notification visibility, and data availability.
+
+This module is narrower: it decides whether a live session is fresh enough to appear in the next presence snapshot.
diff --git a/collab-presence-heartbeat-liveness-guard/sample-data.js b/collab-presence-heartbeat-liveness-guard/sample-data.js
new file mode 100644
index 00000000..02c0cbf9
--- /dev/null
+++ b/collab-presence-heartbeat-liveness-guard/sample-data.js
@@ -0,0 +1,114 @@
+const policy = {
+ version: "presence-heartbeat-liveness-v1",
+ heartbeatStaleSeconds: 45,
+ ghostSessionSeconds: 120,
+ reconnectGraceSeconds: 90,
+ typingIndicatorSeconds: 20,
+ maxFutureClockSkewSeconds: 10,
+ duplicateTabWindowSeconds: 15,
+}
+
+const rooms = [
+ {
+ roomId: "manuscript-alpha",
+ documentId: "doc-alpha",
+ title: "Collaborative manuscript draft",
+ sessions: [
+ {
+ sessionId: "sess-ada-active",
+ collaboratorId: "ada",
+ displayName: "Ada",
+ status: "active",
+ lastHeartbeatAt: "2026-05-23T01:19:45.000Z",
+ typingSince: "2026-05-23T01:19:52.000Z",
+ },
+ {
+ sessionId: "sess-bio-ghost",
+ collaboratorId: "bianca",
+ displayName: "Bianca",
+ status: "editing",
+ lastHeartbeatAt: "2026-05-23T01:17:15.000Z",
+ typingSince: "2026-05-23T01:17:30.000Z",
+ },
+ {
+ sessionId: "sess-chen-old-tab",
+ collaboratorId: "chen",
+ displayName: "Chen",
+ status: "active",
+ lastHeartbeatAt: "2026-05-23T01:19:25.000Z",
+ },
+ {
+ sessionId: "sess-chen-new-tab",
+ collaboratorId: "chen",
+ displayName: "Chen",
+ status: "active",
+ lastHeartbeatAt: "2026-05-23T01:19:55.000Z",
+ },
+ {
+ sessionId: "sess-dina-reconnect",
+ collaboratorId: "dina",
+ displayName: "Dina",
+ status: "disconnected",
+ lastHeartbeatAt: "2026-05-23T01:19:02.000Z",
+ disconnectedAt: "2026-05-23T01:19:20.000Z",
+ },
+ ],
+ cursors: [
+ { sessionId: "sess-ada-active", sectionId: "intro", blockId: "intro-2", offset: 81 },
+ { sessionId: "sess-bio-ghost", sectionId: "methods", blockId: "methods-4", offset: 12 },
+ { sessionId: "sess-chen-old-tab", sectionId: "results", blockId: "result-1", offset: 3 },
+ { sessionId: "sess-chen-new-tab", sectionId: "results", blockId: "result-2", offset: 33 },
+ { sessionId: "missing-session", sectionId: "discussion", blockId: "discussion-1", offset: 7 },
+ ],
+ presenceLeases: [
+ { sessionId: "sess-ada-active", expiresAt: "2026-05-23T01:20:30.000Z" },
+ { sessionId: "sess-bio-ghost", expiresAt: "2026-05-23T01:20:05.000Z" },
+ { sessionId: "sess-chen-new-tab", expiresAt: "2026-05-23T01:20:25.000Z" },
+ { sessionId: "sess-dina-reconnect", expiresAt: "2026-05-23T01:20:15.000Z" },
+ ],
+ },
+ {
+ roomId: "notebook-review",
+ documentId: "doc-notebook",
+ title: "Notebook review room",
+ sessions: [
+ {
+ sessionId: "sess-eli-clean",
+ collaboratorId: "eli",
+ displayName: "Eli",
+ status: "active",
+ lastHeartbeatAt: "2026-05-23T01:19:48.000Z",
+ },
+ {
+ sessionId: "sess-farid-expired-lease",
+ collaboratorId: "farid",
+ displayName: "Farid",
+ status: "active",
+ lastHeartbeatAt: "2026-05-23T01:19:50.000Z",
+ typingSince: "2026-05-23T01:18:55.000Z",
+ },
+ {
+ sessionId: "sess-gita-skew",
+ collaboratorId: "gita",
+ displayName: "Gita",
+ status: "active",
+ lastHeartbeatAt: "2026-05-23T01:20:22.000Z",
+ },
+ ],
+ cursors: [
+ { sessionId: "sess-eli-clean", sectionId: "analysis", blockId: "cell-2", offset: 18 },
+ { sessionId: "sess-farid-expired-lease", sectionId: "analysis", blockId: "cell-5", offset: 44 },
+ { sessionId: "sess-gita-skew", sectionId: "analysis", blockId: "cell-7", offset: 21 },
+ ],
+ presenceLeases: [
+ { sessionId: "sess-eli-clean", expiresAt: "2026-05-23T01:20:25.000Z" },
+ { sessionId: "sess-farid-expired-lease", expiresAt: "2026-05-23T01:19:40.000Z" },
+ { sessionId: "sess-gita-skew", expiresAt: "2026-05-23T01:20:30.000Z" },
+ ],
+ },
+]
+
+module.exports = {
+ policy,
+ rooms,
+}
diff --git a/collab-presence-heartbeat-liveness-guard/test.js b/collab-presence-heartbeat-liveness-guard/test.js
new file mode 100644
index 00000000..26021f50
--- /dev/null
+++ b/collab-presence-heartbeat-liveness-guard/test.js
@@ -0,0 +1,83 @@
+const assert = require("node:assert/strict")
+const { evaluatePresenceLiveness } = require("./index")
+const { rooms, policy } = require("./sample-data")
+
+const packet = evaluatePresenceLiveness({
+ asOf: "2026-05-23T01:20:00.000Z",
+ rooms,
+ policy,
+})
+
+assert.equal(packet.summary.totalRooms, 2)
+assert.equal(packet.summary.totalSessions, 8)
+assert.equal(packet.summary.publishedSessions, 3)
+assert.equal(packet.summary.suppressedSessions, 5)
+assert.equal(packet.summary.cleanupActions, 7)
+assert.equal(packet.summary.recoveryActions, 1)
+assert.equal(packet.summary.fanoutEvents, 3)
+assert.equal(packet.summary.holdRooms, 1)
+assert.equal(packet.summary.sanitizedRooms, 1)
+
+const alpha = packet.rooms.find((room) => room.roomId === "manuscript-alpha")
+assert.equal(alpha.status, "hold_broadcast_cleanup")
+assert.deepEqual(alpha.sanitizedPresence.sessions.map((session) => session.sessionId), [
+ "sess-ada-active",
+ "sess-chen-new-tab",
+])
+
+const ghost = alpha.sessionDecisions.find((session) => session.sessionId === "sess-bio-ghost")
+assert.equal(ghost.broadcast, false)
+assert(ghost.cleanupActions.some((action) => action.code === "ghost_session"))
+assert(ghost.cleanupActions.some((action) => action.code === "expired_typing_indicator"))
+
+const reconnect = alpha.sessionDecisions.find((session) => session.sessionId === "sess-dina-reconnect")
+assert.equal(reconnect.broadcast, false)
+assert.equal(reconnect.recoveryActions[0].code, "reconnect_grace")
+assert.equal(reconnect.recoveryActions[0].action, "hold_cursor_restore_marker")
+
+const oldTab = alpha.sessionDecisions.find((session) => session.sessionId === "sess-chen-old-tab")
+assert.equal(oldTab.broadcast, false)
+assert(oldTab.cleanupActions.some((action) => action.code === "duplicate_tab_shadowed"))
+
+assert(packet.cleanupQueue.some((action) => action.code === "orphan_cursor" && action.sessionId === "missing-session"))
+
+const notebook = packet.rooms.find((room) => room.roomId === "notebook-review")
+const expiredLease = notebook.sessionDecisions.find((session) => session.sessionId === "sess-farid-expired-lease")
+assert.equal(expiredLease.broadcast, false)
+assert(expiredLease.cleanupActions.some((action) => action.code === "expired_presence_fanout_lease"))
+assert(expiredLease.cleanupActions.some((action) => action.code === "expired_typing_indicator"))
+
+const skewed = notebook.sessionDecisions.find((session) => session.sessionId === "sess-gita-skew")
+assert.equal(skewed.publicStatus, "clock_skew_review")
+assert.equal(skewed.publicCursor, null)
+
+const cleanOnly = evaluatePresenceLiveness({
+ asOf: "2026-05-23T01:20:00.000Z",
+ rooms: [{
+ roomId: "clean-room",
+ documentId: "doc-clean",
+ title: "Clean room",
+ sessions: [{
+ sessionId: "sess-clean",
+ collaboratorId: "clean",
+ displayName: "Clean",
+ status: "active",
+ lastHeartbeatAt: "2026-05-23T01:19:58.000Z",
+ }],
+ cursors: [{ sessionId: "sess-clean", sectionId: "intro", blockId: "intro-1", offset: 4 }],
+ presenceLeases: [{ sessionId: "sess-clean", expiresAt: "2026-05-23T01:20:20.000Z" }],
+ }],
+ policy,
+})
+assert.equal(cleanOnly.summary.readyRooms, 1)
+assert.equal(cleanOnly.summary.cleanupActions, 0)
+assert.equal(cleanOnly.summary.publishedSessions, 1)
+
+const repeated = evaluatePresenceLiveness({
+ asOf: "2026-05-23T01:20:00.000Z",
+ rooms,
+ policy,
+})
+assert.equal(repeated.audit.digest, packet.audit.digest)
+
+console.log("collaborative presence heartbeat liveness guard tests passed")