Skip to main content
Mirrored from Meerkat Meta main at 26f5ae9da1fd on 2026-11-03; Meta version 0.9.9 at that commit. Meta uses Server-Sent Events (SSE) for several live HTTP streams:
  • 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
The related meta/events/subscribe JSON-RPC method returns bounded replay snapshots in SSE frame format, but does not open a stream.

Authentication

Every SSE route honors RuntimeDecisionState.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
Missing or invalid credentials return HTTP 401. Tokens are percent-decoded; a token containing characters that need URL-encoding (=, &, +, %) 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

Subscribe to raw Meerkat agent events for one agent (Content-Type: text/event-stream).

Replay and reconnect

A connection without Last-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’s Last-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

Routers built from a live-only subscribe function (agent_events_sse_router) keep connection-local <agent_id>:<n> ids and do not replay.

Mob-merged event stream

This is a live merger of agent events, not 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:
Both source fields use the projected runtime alias including its generation suffix (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

Streams a replay snapshot followed by live console timeline frames. The route accepts the same 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

SSE data 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:
Clients should refetch a recent page and resume from its 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

Convenience route for an identity-filtered console timeline stream. It sets the timeline query 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 the meta/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:
The response includes: 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

This route is present when the runtime has a WorkGraph service. It is a dedicated, lossy wake channel, not another view of the merged event log. Its 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:
On receipt, read authoritative state through 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

Per-client SSE stream of structural 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

If after_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:
When the console timeline broadcast lags (slow consumer, channel saturated), the stream closes. Clients should refetch a recent timeline page and reconnect from the returned latest_cursor or a fresh Last-Event-ID.

Sending messages

To send a message to an agent, use the meta/send_message RPC method and capture the returned session_id for correlation:
Then observe live responses via 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