Skip to content
eternego / docs

WebSockets

This page covers the two WebSocket endpoints. They are how the dashboard stays live: the persona's replies and her media stream into the chat view, and every internal signal streams into the status feed, without polling. A persona driving her own panel can open the same sockets.

Both are read-mostly from the client's side. You connect, then receive a stream of JSON text frames. The server reads from the socket only to notice when you disconnect — sending it messages has no effect (there is no inbound command protocol). To act on a persona, use the HTTP routes (Perception, Lifecycle, …); the sockets are the outbound half.

Every frame is a JSON object with a type field that tells you which kind it is. Connect with the ws:// scheme on the same host and port as the API:

ws://localhost:5000/ws/{persona_id}
ws://localhost:5000/ws/system

Per-persona socket

Live feed for one persona — her chat replies, her voice and image messages, and the internal signals that mention her. This is what the chat screen subscribes to.

WS /ws/{persona_id}

Path param Type Description
persona_id string Her UUID.

On connect, if she's running, the server ensures she has a web channel (connecting one named after her id if absent) and subscribes this socket to her channel's broadcast hub. If she isn't running, the socket still opens and still receives the system-wide signal feed (below) — it just won't carry chat frames until she's up.

Frames you receive

Chat frames — pushed by her web channel as she replies:

{ "type": "chat_message", "persona_id": "<id>", "content": "<her text>" }
{ "type": "chat_audio", "persona_id": "<id>", "url": "/api/persona/<id>/media/<file>", "content": "<caption>" }
{ "type": "chat_image", "persona_id": "<id>", "url": "/api/persona/<id>/media/<file>", "content": "<caption>" }
type When Fields
chat_message She says something (text). persona_id, content (her words).
chat_audio She speaks (Mouth organ). persona_id, url, content (caption). Fetch the clip from url — it's a media endpoint path.
chat_image She draws (Imagination organ). persona_id, url, content (caption). Fetch the image from url.

For chat_audio / chat_image, the bytes are not in the frame — only a url. Issue a GET to that path (the /media/{filename} endpoint) to retrieve the file.

Signal frames — the same internal feed the system socket carries (see its shape below) is broadcast to every open socket, this one included. So a per-persona socket interleaves her chat frames with the global signal stream. Filter by type (chat frames are chat_message / chat_audio / chat_image; everything else is a signal) and, for signals, by what's in details.

Client → server

Send nothing meaningful. The server drains inbound frames only to detect websocket.disconnect and clean up your subscription.

Example

# requires: websocat (https://github.com/vi/websocat)
websocat ws://localhost:5000/ws/6c17c83c-3158-450d-8e43-0e7efea717c1
const ws = new WebSocket(`ws://localhost:5000/ws/${personaId}`);
ws.onmessage = (e) => {
  const msg = JSON.parse(e.data);
  if (msg.type === "chat_message") render(msg.content);
  else if (msg.type === "chat_image" || msg.type === "chat_audio") fetch(msg.url);
  // anything else is a signal frame
};

System socket

Live feed of every internal signal, across all personas — the bus made visible. This is the debug/status stream the dashboard uses to show activity (who's ticking, faults, transitions). It carries no chat frames.

WS /ws/system

No path params.

Frames you receive

One frame per signal on the bus. Every signal — from any persona, any stage — is forwarded:

{
  "type": "Tick",
  "title": "Recognizing",
  "time": 1717481985123456789,
  "details": { "persona": "Adam", "...": "signal-specific, masked" }
}
Field Type Description
type string The signal's class name (e.g. Tick, Tock, Message, BrainFault). Tells you what kind of event it is.
title string The signal's human title — the short phrase passed when it was raised (e.g. "Recognizing", "Persona knowledge read").
time integer When it fired, as a nanosecond epoch timestamp (time.time_ns()).
details object The signal's payload, run through the platform's safe() masking — sensitive values (keys, tokens) are redacted here, unlike the unmasked HTTP responses. Contents vary by signal.

The exact set of type and title values is open and grows with the code — treat this as a stream to observe and filter, not a fixed enum. Don't hard-code on a particular title; key off type and inspect details.

Client → server

Send nothing. As with the per-persona socket, inbound frames are drained only to detect disconnect.

Example

websocat ws://localhost:5000/ws/system

Notes

  • No auth, same as the HTTP API — the sockets are loopback-only. Don't expose the port.
  • details is masked; HTTP responses are not. The signal feed redacts secrets via safe(). The REST endpoints that echo persona records (e.g. feed, pair) do not mask — they return real keys and tokens. Don't assume one is as safe to log as the other.
  • Reconnect on drop. Nothing replays missed frames; if a socket closes, reconnect and re-read current state over HTTP (conversation, diagnose).
  • Perception — the inbound half: send her text, voice, files over HTTP; watch the reply arrive here.
  • Knowledge — Media — fetch the bytes behind a chat_image / chat_audio url.
  • API overview — base URL, port, the no-auth-on-localhost rule, response masking.
  • The panel — Chat, Status — the screens these sockets drive.