---
title: "Reference: Graph Room Sequencer"
description: "The GRS live session route, ownership lease and epoch fence, presence roster, multiplexed durable channels, write path and owner proxy, rate limits, backends, observability, the numbers with the tests that pin them, and the proof surfaces."
engineVersion: v1.0.234
date: 2026-09-28
license: "(c) Gessa, proprietary. Cite with attribution to https://gessa.ai/docs/. Terms: https://gessa.ai/terms/."
canonical: https://gessa.ai/docs/reference/graph-room-sequencer/
---

# Reference: Graph Room Sequencer

{% version engineVersion="v1.0.232" coordinate="docs-public.v0" /%}

_Last verified 2026-09-03 against engine v1.0.232._

The **Graph Room Sequencer** (GRS) is the backend-owned runtime for one live collaboration session per document. Like the [Netcode Contract](netcode-contract.md), it has **no single generated file**; its source of truth is the module under `server/src/modules/graph-room-sequencer` plus the conformance tests that pin its behavior. This page enumerates the surfaces, the ownership and channel invariants, the write path, the fairness limits, the backends, the observability sink, the numbers (each named to its constant), and the proof surfaces. For the model behind these values, read [the Graph Room Sequencer model](../explanation/graph-room-sequencer.md); this page states what is true, that page states why. The module has no barrel `index.ts`; consumers import files directly.

## Live route and wiring

GRS is registered live. `createApp` constructs the room service in `defaultGrsRoomService` and calls `registerGrsSessionRoutes` exactly once, which mounts the session route, wires `gracefulDrain` onto shutdown, and wires `notifyActorControl` for lock-steal notifications. The route is in the generated `capabilityActionAllowlist`.

| Surface | Symbol |
| --- | --- |
| Session route `GET /ws/games/:gameId/session` | `registerGrsSessionRoutes` in `server/src/modules/graph-room-sequencer/routes.ts` |
| Room service | `RoomService` in `server/src/modules/graph-room-sequencer/service.ts` |
| Presence coordinator | `RoomPresenceCoordinator` in `server/src/modules/graph-room-sequencer/roomPresenceCoordinator.ts` |
| Session multiplexer | `GrsSessionMultiplexer` in `server/src/modules/graph-room-sequencer/frameMux.ts` |
| Channel policy | `channelDurability` in `server/src/modules/graph-room-sequencer/channelPolicy.ts` |
| Fail-closed egress | `deliverGrsSessionFrame` in `server/src/modules/graph-room-sequencer/frameEgress.ts` |
| Rate limiter | `GrsRateLimiter` in `server/src/modules/graph-room-sequencer/capacity.ts` |
| Lease and roster ports | `LeaseStore` and `PresenceRosterStore` in `server/src/modules/graph-room-sequencer/types.ts` (in-memory reference plus `RedisLeaseStore` and `RedisPresenceRosterStore`) |
| Graph-activity adapter | `projectGraphActivitySource` in `server/src/modules/graph-room-sequencer/graphActivitySource.ts` |
| Observability sink | `GRS_METRIC_KEYS` in `server/src/modules/graph-room-sequencer/observability.ts` |
| Client egress contract | the `editor.project_graph.websocket` entry in `server/src/modules/asset-delivery-kernel/clientEgressContractRegistry.ts` |

GRS composes, and does not own, the durable graph store, the conflict and revision policy, and the chat, review, and layout timelines, which live in `ProjectGraphService`, `CollaborationChatService`, and `WorkbenchLayoutService`.

## Ownership lease and the epoch fence

One authoritative owner per `gameId` holds a lease of `{gameId, podId, epoch, wsEndpoint, leasedAt, expiresAt}`, defined by `OwnerLease` in `server/src/modules/graph-room-sequencer/types.ts`. The `epoch` is monotonic per document and increments only on a fresh takeover; `InMemoryLeaseStore.acquire` in `server/src/modules/graph-room-sequencer/inMemoryLeaseStore.ts` preserves it on renew and on an idempotent re-acquire by the live holder, and floors a takeover epoch to `max(prevEpoch + 1, floor(nowMs))` so a rebuilt store cannot restart below a durable fence. `RedisLeaseStore` in `server/src/modules/graph-room-sequencer/redisLeaseStore.ts` implements the identical contract atomically with server-side Lua named commands and a persistent counter epoch.

Safety is the epoch fence at the durable write, not the lease. A durable write stamps the owner epoch and is rejected when the epoch is behind the stored fence; the fence decision and the durable owner-epoch record live in `ProjectGraphService` (out of GRS scope). A write that carries no owner epoch is unfenced and does not disturb the floor.

## Presence roster

The roster is owner-held with a per-peer TTL. Join, update, heartbeat, and leave mutate one shared roster and are pod-agnostic, so `RoomPresenceCoordinator.join` returns the complete roster with no delta reconstruction. TTL reaping is owner-only: `RoomPresenceCoordinator.reapTick` is a no-op on a non-owner, so an expiry-driven `PresenceLeave` has exactly one source, and a peer that reappears inside the sweep window has its stale departure suppressed. Explicit `RoomPresenceCoordinator.leave` emits exactly one `PresenceLeave` and is idempotent. The Redis roster uses a hash keyed by document plus a TTL-scored set in `RedisPresenceRosterStore`.

Exclusive resource locks ride the same presence channel. A presence `Ping` renews the session's held hard locks through `renewSessionHardLocks`, and a presence lock update re-syncs them through `syncSessionHardLocks`, dropping any lock the owner no longer holds. The lock primitive itself (`renewResourceLock`) belongs to `ProjectGraphService`; GRS owns only the heartbeat-driven renewal, so a session that stops heartbeating loses its hard locks by TTL rather than by an explicit release.

## Channels

Every channel rides one frame shape (a channel, an optional `seq`, and a payload) on the one session route. `channelDurability` classifies each channel once and defines logged, replayable, and durable as one predicate.

| Channel | Class | Sequenced and replayed |
| --- | --- | --- |
| `op` | durable | yes |
| `chat` | durable | yes |
| `review` | durable | yes (a filtered view over the chat log, not a second store) |
| `layout` | durable | yes |
| `presence` | ephemeral | no (fresh snapshot on reconnect) |
| `cursor` | ephemeral | no |
| `control` | control | no |

A durable frame must carry a `seq` and an ephemeral or control frame must not; `GrsSessionMultiplexer` tracks a per-durable-channel high-watermark. Outbound durable delivery is two-phase (`canAdmitOutbound`, then send, then `markOutboundDelivered`), so a serialization or socket failure never advances a cursor past an undelivered frame.

## Resume and egress

On reconnect, `planChannelResume` computes the contiguous deduplicated window after the client's last acknowledged sequence up to the head. It signals a **gap**, forcing a cold hydrate, when the window is not fully covered, when the client is ahead of the server, or when a retained sequence is not a whole number.

Egress is fail-closed. `deliverGrsSessionFrame` validates every outbound frame through `GrsSessionFrameSchema.parse` and the asset client-boundary monitor for the `editor.project_graph.websocket` contract before it leaves, blocking an unprojected delivery coordinate. The `op.result` and `caught_up` frames are critical control frames: backpressure on either forces a resync rather than a silent drop.

## Write path and owner proxy

`RoomService.verifiedOwnedEpochForWrite` returns an epoch only after the cached lease **atomically renews** in the shared store; otherwise it clears the cached lease and returns `undefined`. A pod that is not the owner never applies the op locally. If the lease names a routable owner endpoint, the pod forwards the exact op one hop to the owner over the owner's session socket, guarded against recursion by the `x-grs-owner-proxy-hop` header; a `pod:`-scheme endpoint is not WebSocket-routable and `ownerSessionUrl` throws rather than fabricating a write; with no routable owner, the client is told it is not the owner and retries.

When the owner applies the op and the durable write reports an epoch-fence rejection carrying the stored epoch, `applyOwnerTransactionWithEpochRepair` repairs the epoch to one above the stored fence and retries once. The rejection reason and the client not-owner and unarmed-op signals are stable strings:

```text
project_graph_owner_epoch_fenced   durable write: op epoch is behind the stored fence
grs_not_owner                      this pod is not the owner and there is no routable owner to proxy to
op_cursor_required                 the op channel was not armed with an explicit snapshot cursor
```

The op channel stays unarmed until an explicit snapshot cursor is supplied: an omitted op cursor means "not yet armed from a REST snapshot" and is rejected, whereas an explicit zero cursor is a valid armed cursor for a new or empty graph. The durable op payloads are project-graph mutations applied by `ProjectGraphService`; the canonical authoring verbs behind them are in the Action Catalog.

{% generated-reference file="docs/spec/generated/action-catalog.md" label="Action Catalog" /%}

## Fairness and rate limits

`GrsRateLimiter` is a per-connection token bucket. The op-write rate is lower for an AI actor than a human, the cursor channel gets a generous ephemeral budget, and chat and review share one budget. The inbound gate `handleInboundFrame` meters only the `op`, `cursor`, `chat`, and `review` channels.

{% warning severity="caution" title="Layout is durable but unmetered" %}
`presence`, `control`, and the **durable** `layout` channel are not rate-limited: `GrsRateLimiter.budgetFor` returns no budget for them, and `handleInboundFrame` does not include `layout` in its throttle gate. Among durable channels, only `op`, `chat`, and `review` are metered; `layout` is unmetered.
{% /warning %}

## Headless AI presence

A server-side AI commit with no WebSocket session is reflected into the same roster and fan-out a human uses. `RoomService.reflectGraphActivity` filters to AI activity (a non-AI transaction is ignored), and `RoomService.markActorEditing` injects the peer only when the room already holds a live session. The synthetic peer coalesces on a deterministic id per document and actor from `deterministicPresenceSessionId` in `server/src/modules/graph-room-sequencer/presenceIds.ts`: SHA-1 over the fixed seed prefix `grs.presence.v1:` with the version nibble forced to 5 and the IETF variant set, an RFC-4122-shaped version-5 identifier (not a namespaced RFC v5 UUID), stable per `(gameId, actorId)`.

{% warning severity="info" title="Immediate AI-peer retract is not a live behavior" %}
`RoomService.clearActorEditing` would retract a synthetic AI peer immediately, but it has no production call site (tests only), and `RoomService.markActorEditing` is called only from `reflectGraphActivity`. The live retirement path for a quiet AI peer is TTL reaping by the owner loop.
{% /warning %}

## Draining

`RoomService.gracefulDrain` checkpoints the current head, broadcasts an owner-moving control frame, stops accepting local writes, and releases every lease so a surviving pod can take over immediately with a higher epoch. An attach while draining is rejected rather than creating an ownerless room, and a session beyond the per-room capacity cap is rejected.

## Backends

`defaultGrsRoomService` in `server/src/app/createApp.ts` selects the store backend by environment: `RedisLeaseStore` and `RedisPresenceRosterStore` are used only outside the local application environment and outside the test runtime and only when a `REDIS_URL` is set; the in-memory stores are used otherwise. The room service is constructed here with `projectGraphActivitySource(projectGraph)` as its graph-activity source, which is the single seam adapting `ProjectGraphService.subscribeToGraphEvents` into the roster reflection.

## Observability

`GrsObservabilityRecorder` records a fixed metric-key set (`GRS_METRIC_KEYS`) into an O(1) fixed-size ring, retains a stage breakdown only for slow frames, and exports through `GrsCloudWatchMetricExporter` to the namespace `GamePlatform/Collaboration/GRS` (`GRS_CLOUDWATCH_NAMESPACE`), batching `PutMetricData` and chunking raw distributions at the CloudWatch unique-value boundary.

## Numbers

Each value names its constant; the receipt column names the test that pins it. The full receipt list is in the [GRS dossier](../../../spec/dossiers/graph-room-sequencer/dossier.yaml).

| Value | Constant | Pinned by |
| --- | --- | --- |
| `15000` ms | `DEFAULT_OWNER_LEASE_TTL_MS` (`ownerLeaseManager.ts`) | `grsOwnerLease.test.ts` |
| `30000` ms | `DEFAULT_ROSTER_TTL_MS` (`types.ts`) | `grsPresenceRoster.test.ts` |
| `15000` ms | `DEFAULT_ROSTER_HEARTBEAT_MS` (`types.ts`) | `grsPresenceRoster.test.ts` |
| `5000` ms | `DEFAULT_MAINTAIN_INTERVAL_MS` (`service.ts`) | `grsRoomService.test.ts` |
| `250` | `maxSessionsPerRoom` default (`service.ts`) | `grsRoomService.test.ts` |
| `30` / `5` / `20` / `5` per second, burst `2` | `DEFAULTS` cursor, chat, op-human, op-ai, burst (`capacity.ts`) | `grsCapacity.test.ts` |
| `512` KiB | `MAX_INBOUND_FRAME_BYTES` (`routes.ts`) | `grsSessionChannels.test.ts` |
| `1024` | `MAX_LIVE_BUFFER` (`routes.ts`) | `grsSessionOpChannel.test.ts` |
| `256` | `MAX_EARLY_INBOUND_FRAMES` (`routes.ts`) | `grsSessionOpChannel.test.ts` |
| `8` MiB | `MAX_OUTBOUND_BUFFERED_BYTES` (`routes.ts`) | `grsFrameEgress.test.ts` |
| `10000` ms | `OWNER_PROXY_TIMEOUT_MS` (`routes.ts`) | `grsOwnerProxy.test.ts` |
| `20000` samples | ring `maxSamples` default (`observability.ts`) | `grsObservability.test.ts` |
| `100` ms | `slowFrameThresholdMs` default (`observability.ts`) | `grsObservability.test.ts` |
| `1000` | `MAX_METRIC_DATA_PER_PUT` (`observability.ts`) | `grsObservability.test.ts` |
| `150` | `MAX_CLOUDWATCH_VALUES_PER_DATUM` (`observability.ts`) | `grsObservability.test.ts` |

The maintenance cadence is `min(max(1000, requested), max(1000, floor(ttlMs / 2)))`, so it is at most half the lease TTL for a TTL of `2000` ms or more; the `1000` ms floor makes "at most half" false below that, and the clamp test in `grsRoomService.test.ts` exercises only the production-scale TTL. Read the formula, not a fixed ceiling.

## Limits and non-features

- Cursor and other ephemeral relays are best-effort and pod-local; a cross-pod ephemeral backplane is deferred (`RoomService.broadcastFrame`).
- The owner proxy is single-hop, and a `pod:`-scheme endpoint is rejected (`ownerSessionUrl`).
- The durable `layout` channel is not rate-limited (`handleInboundFrame`, `GrsRateLimiter.budgetFor`).
- Immediate AI-peer retract has no production call site; TTL reaping is the live retirement path (`clearActorEditing`, `markActorEditing`, `reflectGraphActivity`).
- Redis stores are inert under the local environment and the test runtime even when a `REDIS_URL` is set (`defaultGrsRoomService`).
- The standalone GRS gates `check:grs-cutover`, `check:grs-observability`, `check:grs-redis-chaos`, and `check:grs-live-aws-evidence` are defined in `package.json` but are **not** in the root `check` chain; `check:grs-redis-chaos` gates on `GRS_REDIS_CHAOS_REQUIRED` and needs a real Redis.

## Proof surfaces

The guarantees are held by conformance tests, not prose. Each row names the test that pins it; the dossier lists the exact assertion names.

- **Lease and epoch**: `server/tests/grsOwnerLease.test.ts` (one owner, live-holder-only renew and epoch repair), `server/tests/grsEpochFence.test.ts` (stale-epoch rejection, unfenced epoch-less writes), `server/tests/grsMultiPodHarness.test.ts` and `server/tests/grsRedisChaos.test.ts` (failover bumps the epoch, epochs stay ahead of the fence after Redis key loss).
- **Presence**: `server/tests/grsRoomPresenceCoordinator.test.ts` and `server/tests/grsPresenceRoster.test.ts` (complete roster on join, owner-only reap, stale-leave suppression, idempotent leave).
- **Channels and resume**: `server/tests/grsFrameMux.test.ts` (logged equals replayable equals durable, seq required or forbidden, cursor advances only after delivery, contiguous resume window), `server/tests/grsFrameEgress.test.ts` (unadvanced cursor on socket failure, unprojected-coordinate block, critical control frames).
- **Write path**: `server/tests/grsOwnerProxy.test.ts` (one-hop forward and owner result, unroutable pod rejection, internal endpoints hidden, malformed result fails fast), `server/tests/grsSessionOpChannel.test.ts` (unarmed without a cursor, gapless replay, epoch repair before applying), `server/tests/grsSessionRevoke.test.ts` (revoked member dropped on its next frame).
- **Capacity, cadence, and drain**: `server/tests/grsRoomService.test.ts` (cadence clamp, per-room cap, drain checkpoint and release), `server/tests/grsCapacity.test.ts` (AI below human, cursor budget, chat and review shared, presence and control unmetered).
- **AI reflection**: `server/tests/grsGraphActivitySource.test.ts` and `server/tests/grsRoomService.test.ts` (AI reflected as an ai-kind peer, human ignored, not injected into an empty room, TTL-reaped when quiet).
- **Observability**: `server/tests/grsObservability.test.ts` (full metric-key set, slow-frame stage breakdown, O(1) ring, CloudWatch chunking), plus the static `check:grs-observability` spec.

## Related

- The model behind these surfaces: [Graph Room Sequencer model](../explanation/graph-room-sequencer.md).
- The playable-room sibling runtime: [Netcode Contract](netcode-contract.md).
- The authoritative world model GRS writes into: [The Project Graph](../explanation/project-graph.md).
