WebSocket Events
The daemon streams real-time events over a WebSocket connection. This is the recommended way to build interactive clients — it provides lower latency than HTTP polling and delivers every event as it happens.
Connecting
/v3/ws?token=TOpens a persistent WebSocket connection. The daemon pushes all session events for the authenticated client. The token is passed as a query parameter.
Connect using the daemon token from ~/.config/opta/daemon/state.json:
const ws = new WebSocket("ws://127.0.0.1:9999/v3/ws?token=opta_dk_...");
ws.onopen = () => console.log("connected");
ws.onmessage = (e) => {
const envelope = JSON.parse(e.data);
console.log(envelope.event, envelope.seq, envelope.data);
};
ws.onclose = (e) => console.log("closed", e.code, e.reason);data.sessionId field.Envelope Format
Every WebSocket message is a JSON object called a V3Envelope. It always contains three fields:
interface V3Envelope {
event: string; // Event type (e.g. "turn.token", "tool.start")
seq: number; // Monotonically increasing sequence number
data: unknown; // Event-specific payload
}The seq field is globally ordered across all sessions. It is the key to reliable reconnection — clients track the last received seq and pass it as afterSeq when reconnecting.
Event Types
The daemon emits the following event types. They are grouped by category.
Session Events
Sent immediately after WebSocket connection or session creation. Contains the full session state.
{
"event": "session.snapshot",
"seq": 1,
"data": {
"sessionId": "sess_abc123",
"mode": "chat",
"model": "qwen3-30b-a3b",
"status": "idle",
"turns": 0
}
}Turn Events
The turn has been accepted and is waiting to start.
{
"event": "turn.queued",
"seq": 41,
"data": {
"sessionId": "sess_abc123",
"turnId": "turn_001",
"content": "Explain this code"
}
}Tool Events
A tool call has started executing. Includes the tool name and input parameters.
{
"event": "tool.start",
"seq": 55,
"data": {
"sessionId": "sess_abc123",
"toolName": "file_read",
"toolCallId": "tc_001",
"input": { "path": "/src/index.ts" }
}
}Permission Events
The daemon needs user approval before executing a tool call. The turn is paused until this is resolved.
{
"event": "permission.request",
"seq": 57,
"data": {
"sessionId": "sess_abc123",
"requestId": "perm_001",
"toolName": "file_write",
"input": { "path": "/src/config.ts", "content": "..." },
"reason": "Tool requires write access"
}
}Background Events
Output from a background process (e.g., a long-running shell command). These stream asynchronously alongside turn events.
{
"event": "background.output",
"seq": 70,
"data": {
"sessionId": "sess_abc123",
"taskId": "bg_001",
"stream": "stdout",
"text": "Build complete. 0 errors."
}
}Reconnection
If the WebSocket connection drops, clients should reconnect using the last received sequence number to avoid re-processing events. The daemon supports cursor-based reconnection via the afterSeq query parameter.
let lastSeq = 0;
function connect() {
const url = `ws://127.0.0.1:9999/v3/ws?token=${token}&afterSeq=${lastSeq}`;
const ws = new WebSocket(url);
ws.onmessage = (e) => {
const envelope = JSON.parse(e.data);
lastSeq = envelope.seq;
handleEvent(envelope);
};
ws.onclose = () => {
// Exponential backoff reconnect
setTimeout(connect, Math.min(1000 * Math.pow(2, retries), 30000));
};
}afterSeq, the daemon replays only events with a sequence number greater than the value you provide. This guarantees no duplicates and no gaps.Client Example
Here is a complete example of a minimal TypeScript client that connects to the daemon, submits a turn, and streams the response:
import { readFileSync } from "fs";
import { join } from "path";
import { homedir } from "os";
// Read token from state file
const statePath = join(homedir(), ".config/opta/daemon/state.json");
const state = JSON.parse(readFileSync(statePath, "utf-8"));
const { token, port } = state;
// Connect WebSocket
const ws = new WebSocket(`ws://127.0.0.1:${port}/v3/ws?token=${token}`);
ws.onopen = async () => {
// Create a session
const res = await fetch(`http://127.0.0.1:${port}/v3/sessions`, {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ mode: "chat" }),
});
const { sessionId } = await res.json();
// Submit a turn
await fetch(`http://127.0.0.1:${port}/v3/sessions/${sessionId}/turns`, {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ content: "What is Opta?" }),
});
};
ws.onmessage = (e) => {
const { event, data } = JSON.parse(e.data);
switch (event) {
case "turn.token":
process.stdout.write(data.token);
break;
case "turn.done":
console.log("\n--- Done ---");
console.log(`${data.stats.tokens} tokens at ${data.stats.speed} tok/s`);
ws.close();
break;
case "turn.error":
console.error("Error:", data.error.message);
ws.close();
break;
}
};