ConnectOnionConnectOnion
DocsNetworkWebSocket Protocol

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

MessageIntentWhen
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 CONNECT

Message 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..."
}
FieldRequiredDescription
session_idNoSession to resume. Omit for new session.
sessionNoConversation history (messages, mode, etc.)
payloadYesSigned payload for authentication
fromYesClient's public address
signatureYesEd25519 signature of payload

Server response based on state:

session_idServer stateResponse statusServer action
Not provided"new"Allocate new session
ProvidedIn registry, executing"executing"Reattach IO, pipe buffered events
ProvidedIn registry, connected/suspended"connected"Merge sessions, reset idle timer
ProvidedNot 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,..." }]
}
FieldRequiredDescription
promptYesThe user's message
imagesNoArray of base64 data URLs (passed directly to LLM as visual content)
filesNoArray 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": [...]
}
statusMeaningClient action
"new"Fresh sessionSend INPUT when ready
"connected"Session alive, idleSend INPUT when ready
"executing"Agent still runningWait 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

TypeDescription
thinkingAgent reasoning
tool_callTool execution started
tool_resultTool execution completed
ask_userAgent needs human input
approval_neededTool requires approval
plan_reviewPlan ready for review
compactContext 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 LevelCONNECT Behavior
openAccept without signature
carefulAccept unsigned, recommend signature
strictRequire 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 message

Protocol Evolution

v0.9.x — INIT + ATTACH

WS open → INIT { auth }    → CONNECTED { status: "new" }
         INPUT { prompt, session }  → events → OUTPUT → session dies

v0.10.x — CONNECT (unified)

WS open → CONNECT { auth, session_id? } → CONNECTED { status }
         INPUT { prompt, session }     → events → OUTPUT → session dies

v0.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 up

Server 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

FileRole
network/asgi/websocket.pyWebSocket handler — CONNECT/INPUT routing
network/host/session/active.pyActiveSessionRegistry — in-memory session tracking
network/io/websocket.pyWebSocketIO — queue bridge between async/sync
network/host/session/storage.pySessionStorage — JSONL persistence
network/host/session/merge.pySession merge conflict resolution

Enjoying ConnectOnion?

⭐ Star us on GitHub = ☕ Coffee chat with our founder. We love meeting builders.