Appearance
Sessions
A session in Matrix is a multi-message conversation between a caller and an actor that share a session ID. Sessions are not a primitive of the transport — NATS only knows subjects and messages. They are a convention built on top of the request/reply and pub/sub primitives, used for:
- streaming token-by-token LLM responses
- terminal multiplexer attachments
- long-running background tasks with progress events
- paginated list iteration
Why sessions exist
The base request/reply pattern (one envelope in, one envelope out) is sufficient for most ops, but not for any of these cases:
- An LLM produces tokens over seconds; the caller wants each token as it arrives, not a single blob at the end.
- A terminal session lives for minutes or hours; commands and output flow in both directions.
- A background reconciliation job emits progress events the caller wants to follow without polling.
For each, the caller initiates the session with a request, then both sides exchange follow-up messages tagged with a shared sessionId.
Session shape
A session has:
| Element | Where it lives |
|---|---|
| Session ID | first appears in the initiating op's response, then carried in subsequent messages |
| Outbound op | request/reply against the actor's $inbox |
| Inbound stream | events on the actor's $events filtered by session ID, OR a dedicated subject |
| End-of-session | explicit session.end op or a terminal event payload |
Concretely, an LLM session against system.inference looks like:
1. caller → system.inference/$inbox { op: 'inference.start', payload: { prompt, model } }
2. caller ← system.inference/$reply.{cid} { sessionId: 'sess_abc', ok: true }
3. caller subscribes: system.inference/$events filter: { sessionId: 'sess_abc' }
4. caller ← chunk events ... { sessionId, type: 'token', text: 'Hello' }
5. caller ← chunk events ... { sessionId, type: 'token', text: ', world' }
6. caller ← end event { sessionId, type: 'done' }
7. (optional) caller → system.inference/$inbox { op: 'inference.cancel', sessionId }The MxEnvelope and sessions
The canonical envelope (projects/matrix-3/packages/core/src/engine/messaging/MxEnvelope.ts) does NOT carry a sessionId field — it carries op, payload, correlationId, replyTo, iface, lamport, from, traceId, spanId, parentSpanId. The session ID is convention: it lives in the payload of session-aware ops, not in the envelope.
Note: because
sessionIdis in the payload, the actor that introduced the session is responsible for echoing it on every event the session produces. There is no transport-level session demultiplexing today.
Static interactive declaration
MatrixActor exposes a static interactive field for actors that want a default session pattern when invoked via the universal CLI:
typescript
// projects/matrix-3/packages/core/src/core/MatrixActor.ts:238
static interactive?: {
mode: 'repl' | 'stream' | null;
prompt?: string;
sessionOp?: string;
sessionIdField?: string;
codeField?: string;
topic?: string;
} | null;When a user runs matrix <address> without an op, the CLI checks this:
mode | Behavior |
|---|---|
'repl' | Enter a readline → eval → print loop, sending each line via sessionOp carrying the sessionIdField. |
'stream' | Subscribe to the actor's $events (or topic) and print payloads. |
null | No interactive mode; fall back to $introspect. |
Cancellation
The session-owning actor declares a cancellation op (typically <domain>.cancel taking a sessionId). The framework does not enforce cancellation; the actor must implement it. If the caller drops without sending cancel, the actor relies on heartbeat-style timeouts to clean up (the actor is responsible for the policy — there is no built-in idle-session sweeper).
Session vs subscription
A subscription is a long-lived consumer of an actor's $events with no state in the actor. A session is a multi-message exchange where the actor maintains per-session state (token cursor, terminal scroll buffer, plan progress). Use subscriptions when the consumer just wants to observe; use sessions when the consumer is interacting.
Per-actor session limits
Some actors enforce single-flight semantics — only one session per caller — to avoid resource exhaustion. The chat/inference path uses an _activeSessions: Set<sessionId> guard in SystemLlm so a runaway caller cannot start a thousand parallel inference jobs. Each session-aware actor owns its own concurrency policy.
What is NOT a session
- A single request/reply call is not a session; it has a
correlationId, nosessionId. - Subscribing to
$eventswithout an initiating request is not a session; it's a subscription. $statewatches are not sessions; they are last-value observations.
See also
- Streaming / sessions — wire patterns and concrete envelopes.
- Request/reply — the primitive sessions build on.
- Events — the fan-out primitive sessions use for progress.
- Correlation IDs — distinct from session IDs.