Reference: Graph Room Sequencer
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, 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; 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:
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.
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.
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).
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.
| 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
layoutchannel 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_URLis set (defaultGrsRoomService). - The standalone GRS gates
check:grs-cutover,check:grs-observability,check:grs-redis-chaos, andcheck:grs-live-aws-evidenceare defined inpackage.jsonbut are not in the rootcheckchain;check:grs-redis-chaosgates onGRS_REDIS_CHAOS_REQUIREDand 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.tsandserver/tests/grsRedisChaos.test.ts(failover bumps the epoch, epochs stay ahead of the fence after Redis key loss). - Presence:
server/tests/grsRoomPresenceCoordinator.test.tsandserver/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.tsandserver/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 staticcheck:grs-observabilityspec.
Related
- The model behind these surfaces: Graph Room Sequencer model.
- The playable-room sibling runtime: Netcode Contract.
- The authoritative world model GRS writes into: The Project Graph.