Gessa Docs
Product · Reference

Reference

Reference: Graph Room Sequencer

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.
engine v1.0.234since v1Copy for LLM
engine v1.0.232

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.

SurfaceSymbol
Session route GET /ws/games/:gameId/sessionregisterGrsSessionRoutes in server/src/modules/graph-room-sequencer/routes.ts
Room serviceRoomService in server/src/modules/graph-room-sequencer/service.ts
Presence coordinatorRoomPresenceCoordinator in server/src/modules/graph-room-sequencer/roomPresenceCoordinator.ts
Session multiplexerGrsSessionMultiplexer in server/src/modules/graph-room-sequencer/frameMux.ts
Channel policychannelDurability in server/src/modules/graph-room-sequencer/channelPolicy.ts
Fail-closed egressdeliverGrsSessionFrame in server/src/modules/graph-room-sequencer/frameEgress.ts
Rate limiterGrsRateLimiter in server/src/modules/graph-room-sequencer/capacity.ts
Lease and roster portsLeaseStore and PresenceRosterStore in server/src/modules/graph-room-sequencer/types.ts (in-memory reference plus RedisLeaseStore and RedisPresenceRosterStore)
Graph-activity adapterprojectGraphActivitySource in server/src/modules/graph-room-sequencer/graphActivitySource.ts
Observability sinkGRS_METRIC_KEYS in server/src/modules/graph-room-sequencer/observability.ts
Client egress contractthe 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.

ChannelClassSequenced and replayed
opdurableyes
chatdurableyes
reviewdurableyes (a filtered view over the chat log, not a second store)
layoutdurableyes
presenceephemeralno (fresh snapshot on reconnect)
cursorephemeralno
controlcontrolno

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.

ReferenceAction CatalogResolved signature, schema and example

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.

ValueConstantPinned by
15000 msDEFAULT_OWNER_LEASE_TTL_MS (ownerLeaseManager.ts)grsOwnerLease.test.ts
30000 msDEFAULT_ROSTER_TTL_MS (types.ts)grsPresenceRoster.test.ts
15000 msDEFAULT_ROSTER_HEARTBEAT_MS (types.ts)grsPresenceRoster.test.ts
5000 msDEFAULT_MAINTAIN_INTERVAL_MS (service.ts)grsRoomService.test.ts
250maxSessionsPerRoom default (service.ts)grsRoomService.test.ts
30 / 5 / 20 / 5 per second, burst 2DEFAULTS cursor, chat, op-human, op-ai, burst (capacity.ts)grsCapacity.test.ts
512 KiBMAX_INBOUND_FRAME_BYTES (routes.ts)grsSessionChannels.test.ts
1024MAX_LIVE_BUFFER (routes.ts)grsSessionOpChannel.test.ts
256MAX_EARLY_INBOUND_FRAMES (routes.ts)grsSessionOpChannel.test.ts
8 MiBMAX_OUTBOUND_BUFFERED_BYTES (routes.ts)grsFrameEgress.test.ts
10000 msOWNER_PROXY_TIMEOUT_MS (routes.ts)grsOwnerProxy.test.ts
20000 samplesring maxSamples default (observability.ts)grsObservability.test.ts
100 msslowFrameThresholdMs default (observability.ts)grsObservability.test.ts
1000MAX_METRIC_DATA_PER_PUT (observability.ts)grsObservability.test.ts
150MAX_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.
Was this helpful?Report an issueContact support

On this page