WebSocket Protocol
CONNECT to start or resume, INPUT to message. Session stays alive between executions.
Two client message types, two intents: CONNECT authenticates and restores your session. INPUT sends a prompt. That's the whole protocol.
Overview
| Message | Intent | When |
|---|---|---|
| CONNECT | "Authenticate me, restore my session" | First message on every WebSocket |
| INPUT | "Run this prompt" | After CONNECT |
WebSocket lifecycle
┌────────────────────────────────────────────────────────────────┐ │ WebSocket Lifecycle │ │ │ │ Every connection: WS open → CONNECT → CONNECTED → ... │ │ │ │ CONNECT carries: auth + session (conversation history) │ │ INPUT carries: just the prompt (session already set) │ │ │ │ Server decides: new / connected / executing │ │ │ └────────────────────────────────────────────────────────────────┘
Session Lifecycle
SESSION = connection. EXECUTION = one INPUT → OUTPUT cycle.
Session outlives executions. Multiple INPUTs per session.
State transitions
╭──────────╮
│ new │◄──────────────────── session_id not found
╰────┬─────╯
│ CONNECT
↓
╭──────────╮
│connected │◄──── agent done (OUTPUT) ◄──── user reconnects
╰────┬─────╯ (within 10min)
│ INPUT
↓
╭──────────╮
│executing │──── agent working (LLM → tools → LLM)
╰────┬─────╯
│
┌────┴────────────────────┐
│ │
↓ agent done ↓ WS disconnects
╭──────────╮ ╭───────────╮
│connected │ │ suspended │
│ (idle) │ │ (grace) │
╰──────────╯ ╰─────┬─────╯
│ │
│ next INPUT ├── reconnect within 10min → connected
↓ │
╭──────────╮ └── 10min idle → removed
│executing │
╰──────────╯
Key insight: "completed" is NOT a session state.
It's an execution state. The session stays alive.Protocol Flows
New Session
First connection — no session_id
Client Server
│ │
│── WS open ────────────────────────────►│
│ │
│── CONNECT ─────────────────────────────►│ verify Ed25519 signature
│ { auth, session: {messages} } │ no session_id → new session
│ │ store conversation history
│ │
│◄── CONNECTED ──────────────────────────│ { session_id: "abc", status: "new" }
│ │
│◄── PING ───────────────────────────────│ keep-alive starts (every 30s)
│── PONG ────────────────────────────────►│
│ │
│── INPUT ───────────────────────────────►│ run agent with prompt
│ { prompt: "hello" } │ (no session in INPUT)
│ │
│◄── thinking ───────────────────────────│ stream events
│◄── tool_call ──────────────────────────│
│◄── OUTPUT ─────────────────────────────│ { result, session }
│ │ session → "connected" (not dead)
│ │
│── INPUT ───────────────────────────────►│ same WS, same session
│ { prompt: "tell me more" } │
│◄── ... ────────────────────────────────│
│◄── OUTPUT ─────────────────────────────│Resume After Page Refresh (agent still running)
Reconnect to executing agent
Client Server
│ │
│ (agent still executing on server) │
│ │
│── WS open ────────────────────────────►│
│ │
│── CONNECT ─────────────────────────────►│ verify signature
│ { session_id: "abc", session: {...} } │ registry.get("abc") → executing
│ │ merge sessions if server newer
│ │
│◄── CONNECTED ──────────────────────────│ { session_id: "abc", status: "executing" }
│◄── buffered events ───────────────────│ drain queued events
│◄── PING ───────────────────────────────│ keep-alive resumes
│ │
│◄── stream events ─────────────────────│ live again
│◄── OUTPUT ─────────────────────────────│Resume After Page Refresh (agent finished)
Reconnect to idle session
Client Server
│ │
│ (agent finished while client away) │
│ │
│── WS open ────────────────────────────►│
│ │
│── CONNECT ─────────────────────────────►│ verify signature
│ { session_id: "abc", session: {...} } │ registry.get("abc") → connected
│ │ merge: server has newer data
│ │
│◄── CONNECTED ──────────────────────────│ { session_id: "abc",
│ │ status: "connected",
│ │ server_newer: true,
│ │ session: {merged},
│ │ chat_items: [...] }
│ │
│ (client updates UI with server data) │
│ │
│── INPUT ───────────────────────────────►│ ready for next prompt
│ { prompt: "what else?" } │
│◄── ... ────────────────────────────────│
│◄── OUTPUT ─────────────────────────────│Session Not Found (expired or never existed)
Graceful fallback to new session
Client Server
│ │
│── WS open ────────────────────────────►│
│── CONNECT { session_id: "abc" } ──────►│ not in registry
│◄── CONNECTED ──────────────────────────│ { session_id: "abc", status: "new" }
│ │
│── INPUT ───────────────────────────────►│ fresh session, full history from CONNECTMessage Reference
Client → Server
CONNECT
Authenticate, restore session, and sync conversation. Always the first message.
{
"type": "CONNECT",
"session_id": "550e8400-...",
"session": { "messages": [...], "mode": "safe" },
"payload": { "to": "0x3d4017c3e843...", "timestamp": 1702234567 },
"from": "0xClientPublicKey",
"signature": "0x..."
}| Field | Required | Description |
|---|---|---|
| session_id | No | Session to resume. Omit for new session. |
| session | No | Conversation history (messages, mode, etc.) |
| payload | Yes | Signed payload for authentication |
| from | Yes | Client's public address |
| signature | Yes | Ed25519 signature of payload |
Server response based on state:
| session_id | Server state | Response status | Server action |
|---|---|---|---|
| Not provided | — | "new" | Allocate new session |
| Provided | In registry, executing | "executing" | Reattach IO, pipe buffered events |
| Provided | In registry, connected/suspended | "connected" | Merge sessions, reset idle timer |
| Provided | Not found | "new" | Allocate new session (same id) |
INPUT
Send a prompt. Only valid after CONNECTED. No session data — just the prompt.
{
"type": "INPUT",
"prompt": "Translate hello to Spanish",
"images": ["data:image/png;base64,..."],
"files": [{ "name": "doc.pdf", "data": "data:application/pdf;base64,..." }]
}| Field | Required | Description |
|---|---|---|
| prompt | Yes | The user's message |
| images | No | Array of base64 data URLs (passed directly to LLM as visual content) |
| files | No | Array of file objects (saved to disk, agent reads via tools) |
File Upload Protocol
Files are sent inline as base64-encoded data URLs:
{
"name": "report.pdf",
"data": "data:application/pdf;base64,JVBERi0xLjQK..."
}1. Validates against file limits (default: 10MB per file, 10 files per request)
2. Decodes base64 and saves to .co/uploads/{filename}
3. Adds file paths to the agent's message as a system reminder
4. Agent uses read_file or other tools to process the files
Images vs Files: Images are passed directly to the LLM as visual content (multimodal). Files are saved to disk and read by tools.
PONG
{ "type": "PONG" }ASK_USER_RESPONSE
{ "type": "ASK_USER_RESPONSE", "answer": "Python 3" }APPROVAL_RESPONSE
{ "type": "APPROVAL_RESPONSE", "approved": true, "scope": "once" }Server → Client
CONNECTED
Response to CONNECT.
{
"type": "CONNECTED",
"session_id": "550e8400-...",
"status": "new",
"server_newer": true,
"session": { "messages": [...] },
"chat_items": [...]
}| status | Meaning | Client action |
|---|---|---|
| "new" | Fresh session | Send INPUT when ready |
| "connected" | Session alive, idle | Send INPUT when ready |
| "executing" | Agent still running | Wait for events/OUTPUT |
server_newer, session, and chat_items are only included when the server's session data is newer than the client's.
OUTPUT
Execution completed. Session stays alive for next INPUT.
{
"type": "OUTPUT",
"result": "Hola",
"session_id": "550e8400-...",
"duration_ms": 1250,
"session": { "messages": [...], "trace": [...], "turn": 2 }
}PING
Keep-alive. Sent every 30 seconds.
{ "type": "PING" }Stream Events
| Type | Description |
|---|---|
| thinking | Agent reasoning |
| tool_call | Tool execution started |
| tool_result | Tool execution completed |
| ask_user | Agent needs human input |
| approval_needed | Tool requires approval |
| plan_review | Plan ready for review |
| compact | Context compaction |
ERROR
{ "type": "ERROR", "message": "Something went wrong" }Architecture
End-to-end data flow
╔══════════════╗ ╔═══════════════════════════╗
║ oo-chat ║ ║ Agent Server ║
║ (browser) ║ ║ (Python SDK + host()) ║
╠══════════════╣ ╠═══════════════════════════╣
║ ║ ║ ║
║ localStorage ║ WebSocket ║ ┌─────────────────────┐ ║
║ ┌──────────┐ ║ ┌──────────┐ ║ │ ActiveSessionRegistry│ ║
║ │ session │ ║───│ /ws │────║──│ │ ║
║ │ chatItems│ ║ └──────────┘ ║ │ session_id → { │ ║
║ │ messages │ ║ CONNECT ──► ║ │ io, thread, │ ║
║ └──────────┘ ║ ◄── CONNECTED ║ │ status, last_ping │ ║
║ ║ INPUT ────► ║ │ } │ ║
║ TS SDK ║ ◄── events ║ └─────────┬───────────┘ ║
║ RemoteAgent ║ ◄── OUTPUT ║ │ ║
║ ║ PING/PONG ║ ↓ ║
╚══════════════╝ ║ ┌─────────────────────┐ ║
║ │ SessionStorage │ ║
║ │ (.co/session_ │ ║
║ │ results.jsonl) │ ║
║ └─────────────────────┘ ║
╚═══════════════════════════╝
Data Ownership:
┌────────────────────────────────────────────────────────────────┐
│ Client owns: conversation history (localStorage) │
│ Server owns: execution state (registry), results (storage) │
│ CONNECT syncs: client → server (session), server → client │
│ (if server_newer) │
└────────────────────────────────────────────────────────────────┘Separation of concerns
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ Connection │ │ Conversation │ │ Execution │ │ │ │ │ │ │ │ WebSocket + auth│ │ Message history │ │ One INPUT→OUTPUT│ │ PING/PONG │ │ Owned by client │ │ Agent thread │ │ Persistent │ │ Sent via CONNECT│ │ Temporary │ │ │ │ Merged on server│ │ │ │ Dies: WS close │ │ Dies: never │ │ Dies: OUTPUT │ │ + 10min grace │ │ (localStorage) │ │ │ └─────────────────┘ └─────────────────┘ └─────────────────┘
Authentication
Authentication happens once, on CONNECT. All subsequent INPUT messages on the same WebSocket are trusted.
Auth flow
CONNECT (signed) INPUT (not signed) │ │ ▼ ▼ Server verifies Server trusts signature → OK (same WS, already authenticated)
| Trust Level | CONNECT Behavior |
|---|---|
| open | Accept without signature |
| careful | Accept unsigned, recommend signature |
| strict | Require valid signature |
Client Reconnect
Client-side reconnect logic
Page loads → Zustand hydrates → session_id exists?
│
├── Yes → CONNECT { session_id, session: {messages} }
│ │
│ ├── "new" → session expired, start fresh (client has history)
│ ├── "connected" → session alive, ready for INPUT
│ └── "executing" → agent running, events will stream
│
└── No → show empty state, wait for user input
→ CONNECT (no session_id) on first messageProtocol Evolution
v0.9.x — INIT + ATTACH
WS open → INIT { auth } → CONNECTED { status: "new" }
INPUT { prompt, session } → events → OUTPUT → session diesv0.10.x — CONNECT (unified)
WS open → CONNECT { auth, session_id? } → CONNECTED { status }
INPUT { prompt, session } → events → OUTPUT → session diesv0.11.x — Session survives execution (current)
WS open → CONNECT { auth, session_id?, session }
→ CONNECTED { status: new/connected/executing }
INPUT { prompt } → events → OUTPUT (session stays alive)
INPUT { prompt } → events → OUTPUT (again, same session)
INPUT { prompt } → events → OUTPUT (and again)
WS close → 10min grace → session cleaned upServer Console Output
Structured status lines designed for quick scanning. Routine messages are compact, data flow events are indented sub-lines.
Connection lifecycle
⚡ ws+ 127.0.0.1 (0 active) # new WebSocket, show session count ✓ CONNECT identity=0x2f3d... session=aad5... status=new ✓ INPUT identity=0x2f3d... session=aad5... prompt=hello world... ⚡ ws- (1 active) # disconnect, remaining sessions
Data flow visibility
✓ CONNECT identity=0x2f3d... session=aad5... status=connected ↑ client session: 4 messages # client sent history ↕ merged sessions (server newer) # server had newer data ✓ CONNECT identity=0x2f3d... session=aad5... status=executing ↻ reattaching to running agent # reconnecting mid-execution ✓ INPUT identity=0x2f3d... session=aad5... prompt=analyze this... ↑ 2 images, 1 files # client sent attachments
Errors
✗ CONNECT auth error: forbidden ✗ INPUT rejected: not authenticated (send CONNECT first) ✗ agent error: <exception message>
Key Files
| File | Role |
|---|---|
| network/asgi/websocket.py | WebSocket handler — CONNECT/INPUT routing |
| network/host/session/active.py | ActiveSessionRegistry — in-memory session tracking |
| network/io/websocket.py | WebSocketIO — queue bridge between async/sync |
| network/host/session/storage.py | SessionStorage — JSONL persistence |
| network/host/session/merge.py | Session merge conflict resolution |
