diff --git a/docs/websocket.md b/docs/websocket.md index 0e6e42c..b25bf37 100644 --- a/docs/websocket.md +++ b/docs/websocket.md @@ -6,7 +6,7 @@ `ws://host/ws/sessions/{session_id}` -The session must exist before connecting (create via `POST /sessions`). If the session is not found, the WebSocket closes with code `4004`. +The session must exist before connecting (create via `POST /sessions`). If the session is not found, the WebSocket closes with code `4004`. Auth failures (anonymous denied in auth-enabled mode, or access denied to a session the user does not own) close with code `4003`. ### Authentication @@ -69,12 +69,25 @@ } ``` -- `type` must be `"message"`. Other types return an error frame. -- `content` is required and must be non-empty. +- `type` is either `"message"` (a user turn) or `"compact"` (force context compression now — see [Forced compression](#forced-compression) below). Any other type returns an error frame. +- `content` is required for `"message"` and must be non-empty. - `cwd`: optional absolute path of the client's current working directory. Recommended for terminal clients; the server treats it as the project root and resolves relative paths in `filesystem`, `terminal`, and `code_exec` against it. Stored per-session in `session_metadata["cwd"]` and injected into the LLM system context. -- `images`: optional list of base64-encoded images (data URIs accepted; the `data:...;base64,` prefix is stripped server-side). **Limits:** max 10 images per message, 5 MB each (decoded). Excess images are rejected with a WebSocket error. +- `images`: optional list of base64-encoded images (data URIs accepted; the `data:...;base64,` prefix is stripped server-side). **Limits:** max 8 images per message, 50 MB total payload (across all images). Excess is rejected with a WebSocket error. - `files`: optional list of uploaded file references (appended to content as `[Uploaded files on disk: ...]`). +### Forced compression + +```json +{"type": "compact"} +``` + +Runs the context compressor immediately, **bypassing the token threshold**. The server streams back `compression_started` → `context_compressed` (or an `error` frame if there is nothing to compress / compression is disabled). Notable behavior: + +- It does **not** send `stream_start` — this is not a generation turn. +- It is rejected with an error if a run is already active for the session (concurrent-run guard below). + +The TUI binds this to `/compact` (`Ctrl+X C`). + ### Concurrent run guard Only one agent run may be active per session at a time. If a second message arrives while a run is already in progress (either a WebSocket run or a headless recall), the server rejects it with a WebSocket error. The client should wait for `stream_end` before sending the next message. @@ -118,10 +131,10 @@ | Frame | When | |---|---| -| `{"type": "tool_started", "tool": "name", "args": {...}, "is_subagent": bool}` | Immediately when a tool call begins (before execution) | -| `{"type": "tool_call", "tool": "name", "args": {...}, "result": "...", "success": bool, "is_subagent": bool, "metadata": {...}}` | When the tool finishes | +| `{"type": "tool_started", "tool": "name", "args": {...}, "is_subagent": bool, "metadata": {...}, "tool_call_id": "..."}` | Immediately when a tool call begins (before execution) | +| `{"type": "tool_call", "tool": "name", "args": {...}, "result": "...", "success": bool, "is_subagent": bool, "metadata": {...}, "tool_call_id": "..."}` | When the tool finishes | -`is_subagent: true` indicates the tool call was made by a nested subagent, not the top-level agent. +`is_subagent: true` indicates the tool call was made by a nested subagent, not the top-level agent. `tool_call_id` is a stable id for client-side `tool_started` → `tool_call` pairing. `metadata` carries extra rendering data (e.g. `step_text` for todo update cards, `is_image`/`base64` for image results). ### Text output @@ -133,19 +146,25 @@ | Frame | When | |---|---| +| `{"type": "model_info", "model": "..."}` | Once per turn after the backend resolves a model (on the first successful chunk). May differ from the profile's configured model when a server/model was down and fallback picked another. Additive. | +| `{"type": "todo_updated", "session_id": "...", "tasks": [{"index": N, "text": "...", "status": "...", "validation": "..."}, ...]}` | Session todo list changed (auto-populated from the plan, or via the `todo` tool). Sub-agent todos are NOT emitted here. Additive. | | `{"type": "compression_started", "context_tokens": N, "max_context_tokens": N}` | Immediately before context compression begins (UI can show a spinner) | -| `{"type": "context_compressed", "messages_before": N, "messages_after": N, "summary": "...", "context_tokens": N, "max_context_tokens": N}` | After context compression runs | +| `{"type": "context_compressed", "messages_before": N, "messages_after": N, "summary": "...", "context_tokens": N, "max_context_tokens": N}` | After context compression runs. `summary` is the produced summary text (rendered as a card in the TUI). | | `{"type": "profile_switched", "profile_id": "...", "profile_name": "..."}` | When `switch_profile` tool succeeds | -| `{"type": "recall_update", "session_id": "...", "recall_id": "...", "call_type": "...", "trigger_at": "...", "status": "...", "action": "..."}` | Recall state changed (scheduled, cancelled, fired, rescheduled) | -| `{"type": "mcp_status_update", "server_name": "...", "status": "connected|disconnected", "tool_count": N, "error": "..."}` | MCP server connection status changed — broadcast to all sessions for toast notifications | +| `{"type": "recall_update", "session_id": "...", "recall_id": "...", "call_type": "...", "trigger_at": "...", "status": "...", "action": "scheduled\|cancelled\|skipped\|fired\|rescheduled"}` | Recall state changed | +| `{"type": "mcp_status_update", "server_name": "...", "status": "connected\|disconnected", "tool_count": N, "error": "..."}` | MCP server connection status changed — broadcast to all sessions for toast notifications | +| `{"type": "terminal_output", "terminal_name": "...", "stream": "stdout\|stderr", "delta": "..."}` | Streamed stdout/stderr chunk from a background persistent terminal | +| `{"type": "terminal_closed", "terminal_name": "...", "reason": "explicit\|idle_timeout\|error\|shutdown\|session_ended"}` | A persistent terminal session ended | | `{"type": "heartbeat"}` | Periodic keepalive during long silent operations (every 20 s) | -| `{"type": "session_sync"}` | Client should reload session history from REST (`GET /sessions/{id}`) | +| `{"type": "session_sync", "session_id": "...", "profile_id": "..."}` | Client should reload session history from REST (`GET /sessions/{id}`) | `session_sync` is sent in three situations: 1. On fresh connect when no run is active — in case the agent finished while the client was disconnected. 2. After a reconnect-and-replay completes — to ensure the client sees the fully saved response. 3. After a headless recall run finishes — so the client sees the full recall turn (recall user message + assistant response). +It carries the active `session_id` and `profile_id` so the client can sync its header / profile indicator without an extra round-trip. + --- ## Stopping generation