Event Stream¶
GET /v1/events/stream is a Server-Sent Events feed of rows from the
orchestrator’s events table in strict insertion order. Subscribers
keep an open HTTP connection; the orchestrator pushes one SSE message
per inserted row.
Endpoint¶
GET /v1/events/stream
Query parameters (all optional, all AND-combined):
Param |
Effect |
|---|---|
|
only events whose |
|
only events whose |
The handler polls the underlying SQLite at 500 ms intervals; new rows
push immediately on the next tick. Disconnect is detected via
http.CloseNotifier; reconnecting with no cursor replays from row 0.
Wire shape (one SSE data: payload per row):
{
"id": "<rowid>",
"node_id": "<node-id>",
"event_type": "<dotted.lowercase.string>",
"payload": { /* event-type-specific JSON */ },
"timestamp": "<RFC3339>"
}
Event types¶
The event_type field uses dotted lower-snake case. Two distinct
sources populate the table:
Node-emitted events — the events the agent POSTs to
/v1/events. Examples:ai.policy.decision,ai.deployment.lifecycle,node.unreachable. These drivefleet_nodesderivation infleet_state.go.Orchestrator-emitted events — events the control plane writes about its own state mutations. PR5 of #725 introduces the first two. These do not drive
fleet_nodes(server-side enrollment writes are not a liveness signal); they exist to let runtime caches refresh by subscription instead of polling.
enrollment.node.registered¶
Emitted exactly once when RegisterNode lands a new row in
enrolled_nodes. The documented idempotent re-enroll path (re-running
autonomy node enroll against an existing node_id, even with a
different --enrollment-ref, returns the original record unchanged)
is silent — no second event fires. A subscriber can treat each
event as “a new node was enrolled, refresh your cache”.
Payload (eventstore.NodeEnrolledPayload):
{
"node_id": "robot-arm-007",
"domain_id": "fleet-alpha",
"enrollment_ref": "deploy:fleet-alpha:v1",
"enrolled_at": "2026-05-14T12:00:00Z",
"enrolled_by": "ops-runbook",
"status": "active"
}
domain_id, enrollment_ref, and enrolled_by are omitempty.
Labels are intentionally not in the payload — the consumer can fetch
them on-demand via GET /v1/enrollment/{node_id}.
rollout.node_state.transitioned¶
Emitted by UpsertNodeRolloutState when the persisted state for a
node actually changes — either the first insert, or an update where
the new (state, directive_id) pair differs from the stored one. A
no-op upsert (writer re-asserts the same state on every poll) is
silent. The transitioned-at timestamp is intentionally not part of
the change-detection comparison so a polling cadence can’t manufacture
phantom “changes” by advancing the clock.
Payload (eventstore.NodeRolloutStateTransitionedPayload):
{
"node_id": "robot-arm-007",
"state": "active",
"transitioned_at": "2026-05-14T12:00:00Z",
"directive_id": "rollout-2026-05-14-001"
}
directive_id is omitempty. The previous state is not included
— UpsertNodeRolloutState does not read the prior row, and a value
the writer never observed would be worse than absent. A subscriber
holding a cache already knows what it thought the value was.
Atomicity contract¶
Both orchestrator-emitted event types share the same contract:
The event row commits in the same SQL transaction as the underlying state mutation. A subscriber that sees an event can trust that
SELECT * FROM enrolled_nodes WHERE node_id = <event.node_id>(or the rollout-state equivalent) returns a row.The event fires only on real state change. Idempotent re-writes are silent. This makes “saw an event” usable as a refresh trigger without false positives.
Consumer pattern¶
A cache that wants near-real-time freshness for one of these event types subscribes once at startup and updates on every push:
GET /v1/events/stream?event_type=enrollment.node.registered
Polling fallback (the default runtime/identity cache PR6 ships with)
remains valid for callers that don’t want to hold an SSE connection
open. The two paths converge on the same payload shape, so a consumer
can switch between them without changing its decoder.
Evidence¶
orchestrator/server_event_stream.go— SSE handler.orchestrator/store.go—eventstable schema,Ingest,QueryStream.orchestrator/eventstore/event_types.go— exported event_type constants and payload struct definitions.orchestrator/eventstore/append.go—AppendEmittedEventhelper used by orchestrator-side emitters.orchestrator/enrollment.go—RegisterNodeemitter.orchestrator/rollout/store.go—UpsertNodeRolloutStateemitter.