RoboCo pushes live updates over WebSocket endpoints under /ws, served by the orchestrator (roboco/api/websocket.py) and routed through nginx alongside the REST API. A single in-process ConnectionManager holds per-resource connection sets and broadcasts events to them. The panel consumes all of these through its useWebSocket("/<endpoint>", …) hook — you rarely connect to them directly, but they're the same streams an integrator can subscribe to.

The endpoints

There are four per-resource streams plus one operator-wide stream:

EndpointStreamAuth
/ws/channels/{channel_id}Live messages in a team channelagent_id query param, validated against the DB + channel access
/ws/agents/{agent_id}An agent's output and lifecycle eventsviewer_id/agent_id query param, validated against the DB
/ws/sessions/{session_id}Messages in a communication sessionagent_id query param, validated
/ws/notifications/{agent_id}An agent's notificationsagent_id query param, validated
/ws/systemOperator/system-wide stream — no per-agent keyingUnauthenticated, read-only

All sockets support a ping/pong keepalive: send {"type": "ping"} and you'll get a pong back.

WebSocket auth is not the REST auth

The per-resource sockets validate their agent_id/viewer_id query param against the database (and channel access via the permissions layer), but they do not enforce the HMAC X-Agent-Token that secure-mode REST requires — token enforcement is REST-only. /ws/system is intentionally fully unauthenticated. None of the streams carry a control surface or secrets, so they're read-only by design, but the orchestrator port should be treated as trusted-network-only until WebSocket auth lands. See Authentication and Security.

How events reach the sockets

Server-side events are published to an in-process StreamEventBus. The bridge in roboco/api/websocket_bridge.py subscribes to it and registers a _handle_* forwarder per event type, mapping each EventType to the right socket broadcast.

To add a new live event you define an EventType, publish it to the bus, add a _handle_* forwarder in websocket_bridge, and consume it on the panel via the same hook — you never stand up a parallel endpoint.

Event types

EventArrives onWhat it carries
RATE_LIMIT_HIT/ws/systemA provider just hit a rate limit / overload and was parked. Drives the panel's amber rate-limit banner.
RATE_LIMIT_LIFTED/ws/systemA parked provider recovered; queued work resumes. Clears the banner.
USAGE_SNAPSHOT/ws/systemA fresh token-usage/cost snapshot. Drives the live "Token Usage & Cost" dashboard.
NOTIFICATION_SENT / NOTIFICATION_ACKED/ws/notifications/{agent_id}A notification was sent to or acknowledged by an agent.
SESSION_CREATED / SESSION_CLOSED / SESSION_TIMEOUT/ws/sessions/{session_id}Communication-session lifecycle.
AGENT_SPAWNED / AGENT_STOPPED / AGENT_WAITING / AGENT_RESUMED / AGENT_ERROR/ws/agents/{agent_id}Agent runtime lifecycle transitions.

Each forwarded message is a JSON object with a type field (the event-type name above) merged with the event's data.

REST fallbacks

The two operator dashboards that ride /ws/system fall back to HTTP polling when the socket is down, so the panel keeps working without the stream:

Live eventHTTP fallback
RATE_LIMIT_HIT / RATE_LIMIT_LIFTEDGET /api/system/rate-limits
USAGE_SNAPSHOTGET /api/usage/summary?period=24h|7d|30d

See Cost & usage and Health & metrics for what the panel does with these.

Next

  • REST API — the /api/* route map and the error envelope.
  • Authentication — the WebSocket-auth caveat in full.