- agent-scoped streaming through
GET /agents/{agent_id}/events - mob-merged streaming through
GET /mob/events - structural mob events through
GET /meta/mob_events/stream - lossy WorkGraph wake events through
GET /meta/workgraph/facts/stream - console timeline replay and live continuation through
GET /console/timeline/stream - identity-filtered console timeline streaming through
GET /console/identity/{identity}/stream
meta/events/subscribe JSON-RPC method returns bounded replay
snapshots in SSE frame format, but does not open a stream.
Authentication
Every SSE route honorsRuntimeDecisionState.console.require_app_auth.
When require_app_auth is on, the request must carry one of:
Authorization: Bearer <token>header, or?auth_token=<token>query parameter
=, &, +, %) is handled correctly.
A stream stays open only while its token is accepted. Once a request with
the same token would get 401, 30 seconds after the token’s exp, the
stream ends without a final event, and it sends nothing it decided after that
point; reconnect with a current token. On a route that does not require a
token, a token the caller sends anyway identifies it to access control; such a
stream ends the same way, and its reconnect without a current token is
anonymous. A token without exp never lapses. A revocation of the token’s
principal (meta/console/revoke_sessions) ends the stream at once, with or
without exp, and its reconnect is refused with 401.
Agent event stream
Content-Type: text/event-stream).
Replay and reconnect
A connection withoutLast-Event-ID first replays the events the agent’s
current actor still retains (Meerkat keeps each actor’s latest 1024 events
and 4 MiB), then streams live. A client that connects after a run started
therefore still receives that run from run_started. If the retained window
no longer reaches back far enough, the replay starts with a stream_truncated
event ({"reason":{"kind":"stream_lagged","dropped":N}}).
Each event’s id is its position in the agent’s event sequence,
<epoch>:<seq>. The epoch names the sequence space, which changes when the
runtime restarts or a placed member moves to a new host binding. An
EventSource reconnect sends the last id as Last-Event-ID, and the stream
resumes right after that event, with nothing replayed twice. If the id
belongs to another sequence space, the stream replays the current space from
its earliest retained event. A stream_truncated gap event carries no id,
so it does not move the reconnect position.
Dormant members
A member that is seated but not running in this process (dormant after a restart, or seated with a session its first wake has not created yet) still answers 200 at once. Opening the stream never wakes the member: the connection stays open, with keep-alives and no events, until the member is woken (a delivery, or an operator or boot wake). The stream then attaches to the woken member and replays from its first retained event, or resumes after the connection’sLast-Event-ID when that id names the woken member’s
sequence space. Every event with a place in that sequence carries an
<epoch>:<seq> id of that space (a stream_truncated gap event carries
none), so a reconnect resumes exactly as above. A stream that is waiting ends
without a final event when the member leaves the mob, when the runtime stops,
or when the runtime refuses the member’s stream for any other reason; a
reconnect opens it again or answers the refusal. An agent that is not a
member answers 404.
Frame format
agent_events_sse_router) keep connection-local <agent_id>:<n> ids and do
not replay.
Mob-merged event stream
EventEnvelope<UnifiedEvent> and
not the structural mob ledger. Each event uses the agent event name and wraps
its projected payload with member_id and source:
worker-1:0 here). The mob:<sequence> ID is connection-local and
starts again on a new connection; it is not a durable cursor. This route
does not replay from Last-Event-ID. Use
structural mob SSE for ledger-cursor replay,
or meta/events/subscribe for a bounded merged-event RPC snapshot with its
own envelope and checkpoint contract.
Console timeline stream
identity, conversation_id, after, before, mode, and
limit parameters as GET /console/timeline, with distinct seed
semantics: without after, since switches to recent. recent returns a
bounded seed page (limit sizes it); anchored since replays all retained
matching backlog, sent page by page as it is read, whatever limit says. A
non-empty Last-Event-ID header overrides the URL’s after.
Timeline frame format
SSEdata is a type-tagged ConsoleTimelineEvent, not a bare
ConsoleFrame or agent event. For example (the frame is abbreviated):
after and cursor are omitted on markers when absent. The SSE event name
and data.type need not coincide for updates. Persist cursor-bearing IDs for
Last-Event-ID continuation; frame content remains in frame.payload. A
cursor still resumes after its frame once the log has removed it, as it does
for a streamed delta frame its block’s complete frame superseded (see
GET /console/timeline).
Initial replay refusal is an HTTP error before the stream opens, rather than
a promise that every failure arrives as a replay_unavailable event.
If replay cannot satisfy the requested cursor, the server returns HTTP 409 with:
latest_cursor
rather than retrying the stale cursor.
An anchored replay is read after the stream opens, one page at a time, so it
can also end early in two ways. If the log no longer holds what follows the
frames sent (it was cleared while the stream replayed), the stream sends a
replay_unavailable event and ends: handle it as the 409. If a later page
cannot be read, the stream ends after the frames it sent, with no
snapshot_complete: reconnect with the last frame’s cursor as
Last-Event-ID.
Identity timeline stream
identity from the path and otherwise uses the same replay rules, event envelopes, markers, and cursor headers as GET /console/timeline/stream.
Event subscription
Use themeta/events/subscribe JSON-RPC method to request a bounded
point-in-time snapshot. It returns each selected event both as a typed envelope
and as an SSE-ready frame. It does not open a live connection or return a
stream URL; use the HTTP routes on this page for live observation.
Example request:
Supported scopes:
For
agent scope, agent_id may be the exact projected ID or the bare alias
whose only difference is a trailing numeric generation suffix. The server
applies the scope first and retains at most its three newest matching events.
It selects from the runtime’s buffer of recent merged events (at least the
newest 4,096), so a scope whose newest matching events are older than the
buffer returns fewer events, or none.
last_event_id is an event_id from an earlier
meta/events/subscribe result, not an HTTP SSE frame ID. It must be present
in that bounded, scope-filtered window. Replay is inclusive: the checkpoint
event is returned again, followed by later retained events. An unknown,
out-of-scope, or expired checkpoint (one that has left the window or the
buffer) returns JSON-RPC -32602, so reconnecting consumers should
deduplicate the first event by event_id.
WorkGraph wake stream
data field is a bare UnifiedEvent::Module value with module
meta.workgraph; the event is not wrapped in EventEnvelope and is not
duplicated into GET /mob/events or meta/events/subscribe.
Every connection starts with this mandatory resynchronization signal:
meta/workgraph/get,
meta/workgraph/list, meta/workgraph/ready, or
meta/workgraph/snapshot. Subsequent workgraph.fact events carry
identifiers that can accelerate another read, but they are never state
authority.
The stream has no SSE id, Last-Event-ID, cursor query, catch-up, or replay
contract. A slow consumer can miss events. When broadcast lag occurs, the
server sends workgraph.resync_required with reason: "lagged" and the
number of skipped wakes in skipped; read authoritative WorkGraph state again.
When access control is enforced, the caller also needs the workgraph.view
grant.
Structural mob events stream
MobEvents (typed envelopes
preserving mob_id, run_id, step_id, agent_identity, and the
full MobEventKind payload).
Each connection opens its own MobEventsView::subscribe_after so
catch-up and live tail share the same ordered stream — there is no
race window between snapshot and live subscription.
Query parameters
Frame format
Stale cursor
Ifafter_seq > latest_cursor, the route returns HTTP 410 Gone
with {"error":"event_query_stale","after_cursor":N,"latest_cursor":M}.
SDK clients should rewind to latest_cursor and reconnect.
Continuation from meta/mob_events/subscribe
The JSON-RPC handshake returns a snapshot frame plus a
subscribe_url field that already encodes the resume cursor and
filters:
after_seq falls back to latest_cursor captured at handshake
when the snapshot is empty, so the SSE handler picks up gaplessly
even when no events match yet.
Cursor durability
MobStructuralEvent.cursor is the upstream meerkat ledger cursor. Consumers
can checkpoint it and reconnect with after_seq=<checkpointed cursor> only
while the configured mob-event store retains that cursor and its later events.
.persistent_state(path) alone does not make the mob-event ledger durable. It
persists Meta projection metadata, while the stock unified-runtime builder
still uses in-memory Mob storage. Restart-safe replay requires a separately
configured upstream Meerkat mob-event store that retains the ledger across the
restart. If a checkpoint is ahead of the available ledger, the route returns
HTTP 410 Gone as described above.
Keep-alive
The stock HTTP streams send a keep-alive comment after 15 seconds of idle time to prevent connection timeouts. This interval is fixed, not an exposed configuration option:latest_cursor or a fresh Last-Event-ID.
Sending messages
To send a message to an agent, use themeta/send_message RPC method and capture the returned session_id for correlation:
GET /console/timeline/stream or
GET /console/identity/{identity}/stream. Use meta/events/subscribe only
when a bounded merged-event replay snapshot is sufficient.
Error handling
If the target agent/member does not exist or a subscription request is malformed, SSE routes return an HTTP error response before opening the stream:See also
- REST API — HTTP endpoints overview
- Console guide — how the console uses SSE
- Events — unified event model
