Appearance
Actor sessions
A "session" in Matrix is a conversation thread — a sequence of $prompt invocations sharing context, history, and an audit trail. Sessions are how an actor stays coherent across multiple turns of an LLM agent loop. This page covers how $prompt works, what ActivityFrames are, and how to participate from a custom actor.
Sessions as a concept
A session has three identifiers:
| Field | Meaning | Where it lives |
|---|---|---|
sessionId | Conversation/session id used for transcript threading. | ActivityFrame.sessionId |
traceId | One per user interaction; never changes. | ActivityFrame.traceId |
spanId | One per agent invocation; generated by system.agents. | ActivityFrame.spanId |
Sessions are not stored on the actor itself by default. They are stored by the agents/conversation layer (@open-matrix/agents, system-conversation) and threaded through $prompt payloads. A custom actor receives the sessionId in incoming $prompt/$activity envelopes and forwards it on outgoing ones.
The $prompt op
on$prompt is sealed in MatrixActor (MatrixActor.ts:1173-1276). The base implementation is a trampoline that:
- Validates the incoming payload has a
prompt: string. - Mints a
requestIdif one was not supplied. - Builds a snapshot of
thisactor (mount, accepts, emits, skills, cognitive state). - Mints a
traceIdif one was not supplied. - Resolves the LLM endpoint from
context.getService('llm-endpoint'), falling back to'system.agents'. - Forwards
activityTo,sessionId, andprincipalIdon the outgoing payload. - Either:
- Blocking (
payload.blocking === true) — usesRequestReply.executeand returns the agent's result. - Non-blocking —
RequestReply.sendToInbox(...)and returns{ ok: true, accepted: true, requestId }.
- Blocking (
Warning: Subclasses cannot override
on$prompt. The framework detects override attempts and logs an error, then runs the base trampoline anyway. If you need a domain prompt verb, declare a named op (session.start,chat.send-message,mypackage.summarize).
The exception: an actor with static isPromptRouter = true (typically only AgentsRoot/AgentActor in @open-matrix/agents) is the prompt router and does handle $prompt itself.
ActivityFrame — the streaming protocol
ActivityFrame is the structured event pushed back to interested observers while a $prompt is being processed. It is defined in projects/matrix-3/packages/core/src/framework/ActivityFrame.ts:11-47:
ts
export interface ActivityFrame {
requestId: string;
seq: number;
ts: number;
source: string; // mount that emitted the frame
traceId?: string;
spanId?: string;
parentSpanId?: string;
sessionId?: string;
resumeId?: string;
principalId?: string;
kind: 'prompt' | 'llm' | 'tool' | 'system';
phase: ActivityPhase;
summary?: string; // human-readable, e.g. "Querying flowpad-page.db"
target?: string; // for tool calls
op?: string; // for tool calls
data?: unknown;
durationMs?: number;
}
export type ActivityPhase =
| 'accepted' | 'started' | 'thinking' | 'token'
| 'tool-start' | 'tool-result' | 'output'
| 'done' | 'error' | 'cancelled';The terminal phases (done, error, cancelled) close the frame stream for that requestId. The isTerminalPhase() helper exported from @open-matrix/core is what UI components use to know when to stop accumulating frames.
How frames reach the caller
The caller passes activityTo: [mount1, mount2, ...] on the outgoing $prompt. The agents layer (system.agents) reads that field and emits $activity envelopes carrying ActivityFrame payloads to each of those mounts via sendToInbox (which crosses root boundaries).
ts
// Caller
this.sendTo('mypackage.helper', '$prompt', {
prompt: 'do the thing',
activityTo: [this.mount], // me
sessionId: 'sess-abc',
});
// Caller's on$activity handler
on$activity(payload: ActivityFrame) {
if (payload.phase === 'token') {
this.appendToken(payload.data);
} else if (isTerminalPhase(payload.phase)) {
this.finalize();
}
}MatrixActor.on$activity is a no-op by default (MatrixActor.ts:893-895). MatrixActorHtmlElement overrides it to drive the reactive UI: accumulate frames in this._activityFrames, schedule a render, and call _onActivity for subclass hooks.
Note on dispatch table casing:
toHandlerCase('$activity')produces$activity(lowercaseaafter$). The dispatch table looks upon$activity, NOTon$Activity. The base class comments this explicitly atMatrixActor.ts:890-892. Watch the exact casing when overriding.
Continuing a session
To continue a session across turns, pass the same sessionId on each $prompt:
ts
const sessionId = 'sess-' + crypto.randomUUID();
await this.invokeAgent(prompt1, { sessionId, blocking: true });
await this.invokeAgent(prompt2, { sessionId, blocking: true }); // continuesThe agents layer threads the conversation history into the LLM call automatically when sessionId is supplied. Without it, every $prompt starts a fresh session.
For browser components, the framework keeps a _agentSessionId per MatrixActorHtmlElement instance (MatrixActorHtmlElement.ts:333-334) so successive $prompt calls from the same component element thread by default.
Identifiers as causal chain
When a session's prompt invokes a tool (a sub-actor), the framework propagates trace IDs:
trace-A (top-level user interaction)
├── span-A1 (initial $prompt to system.agents)
│ └── tool-start frame: target=system.factotum, op=put-credential
│ └── span-A1-T1 (factotum operation, parentSpanId=A1)
└── span-A2 (follow-up $prompt with sessionId)This is what makes the activity stream debuggable. Aggregating frames by traceId reconstructs a complete user-level interaction.
Persistence
The session itself (the conversation history) is the responsibility of @open-matrix/agents and @open-matrix/system-conversation, not @open-matrix/core. The SDK provides:
- The
ActivityFrameshape and the$prompttrampoline. static stateScopeandstatic persistStateonMatrixActor(see Actor state).IRecordStore(the per-package observation store) — used by the dispatch path to record op begin/end (MatrixActor.ts:2900-2910,:2980-2997).
How records are stored, how they're queried, and how transcripts get rebuilt from them is documented in @open-matrix/system-conversation and is out of scope for the SDK reference.
Related cognitive surface
MatrixActor carries a small set of static fields that participate in sessions. Each is documented inline in MatrixActor.ts:267-355:
| Field | What it controls |
|---|---|
static promptable | Default false. When true, the actor runs an oracle loop, has prompt triggers, memory queries, plan tracking, and budget tracking. |
static purpose | Short description used in the LLM's system prompt. |
static systemPrompt | Custom fragment appended after the standard system prompt. |
static memoryScope | 'session' | 'actor' | 'subtree'. How far back the actor can query its own records. |
static regime | 'explore' | 'focused' | 'strict'. Cascades to children if more restrictive. |
static promptTriggers | Declarative event-to-prompt bindings (Phase 2). |
static skills | Higher-level skill bundles that compose multiple ops into named actions. |
These belong to the cognitive layer; most package authors do not touch them. They are documented here only because they alter session behaviour.
See also
- Actor ops —
$promptis sealed; sealed-op rules. - Actor state —
stateScopeandpersistState. - Component packaging — how UI components participate in sessions.
Source:
projects/matrix-3/packages/core/src/core/MatrixActor.ts:1173-1276(on$prompt),:893-895(on$activity);projects/matrix-3/packages/core/src/framework/ActivityFrame.ts(full file).