Two-step connection

# Step 1: get a ticket (valid 30s, single-use)
curl -X GET http://localhost:3001/api/ws/ticket \
  -H "Authorization: Bearer <jwt>"
# → { "ticket": "550e8400-...", "expiresAt": "2026-06-01T10:00:30Z" }

# Step 2: connect WebSocket
ws://localhost:3001/ws?ticket=<uuid>

Message Envelope

// All messages follow this envelope
{
  "type":    "<event_type>",
  "id":      "<uuid>",
  "ts":      1748686800000,
  "payload": { }
}

Client → Server Commands

join_room         { workspaceId }     // join workspace room
subscribe_run     { runId }           // subscribe to run events
unsubscribe_run   { runId }
cancel_run        { runId }           // SIGTERM on ada-core process
ping              {}                  // latency test

Server → Client Events

run:started               { runId, profile, conversationId }
run:phase:started         { runId, phase, agent, tier, model }
run:phase:progress        { runId, phase, log, tokens }
run:phase:completed       { runId, phase, cost, tokens, durationMs }
run:phase:failed          { runId, phase, error }
run:completed             { runId, totalCost, totalTokens, durationMs }
run:failed                { runId, error }
run:cancelled             { runId }
message:created           { conversationId, message }
conversation:updated      { conversationId }
core:status               { connected: boolean }
heartbeat                 { ts }      // every 30s

Closure Codes

CodeMeaningAction
1000Normal ClosureDon't reconnect
1006Abnormal ClosureReconnect with backoff
4001Ticket InvalidGet new ticket, reconnect
4002Token ExpiredRefresh JWT, get new ticket
4003Rate LimitedWait 60s
4004Session RevokedRedirect to login

Reconnection (exponential backoff)

// Backoff: 0ms, 1s, 2s, 4s, 8s, 16s, max 30s
class AdaWebSocket {
  connect() {
    const ws = new WebSocket(`ws://localhost:3001/ws?ticket=${ticket}`)
    ws.onclose = (e) => {
      if (e.code === 1000 || e.code === 4004) return // don't retry
      const delay = Math.min(1000 * 2 ** this.retries++, 30000)
      setTimeout(() => this.refreshTicket().then(() => this.connect()), delay)
    }
  }
}