# Gateway API Protocol

Flynn's gateway exposes a WebSocket-based JSON-RPC protocol for real-time communication with the AI agent. This document describes the protocol in detail.

## Table of Contents

- [Overview](#overview)
- [Connection](#connection)
- [Authentication](#authentication)
- [Message Format](#message-format)
- [Methods](#methods)
- [Events](#events)
- [Error Codes](#error-codes)
- [Example Client](#example-client)

## Overview

The gateway provides:

- **WebSocket Server**: Real-time bidirectional communication
- **JSON-RPC 2.0**: Structured request/response protocol
- **Streaming Events**: Real-time updates during agent processing
- **HTTP Server**: Serves static dashboard and handles webhook endpoints
- **Node Capability Negotiation**: Optional companion-node role/capability registration

### Execution Model (Sessions + Per-Session Queue)

Two concepts matter for correct clients:

- **connectionId**: a single WebSocket connection identity (assigned on connect)
- **sessionId**: the conversation/session the connection is attached to (defaults to a per-connection session, but can be switched to resume an old session)

The gateway serialises agent work **per session**, not per WebSocket connection:

- Requests that target the same `sessionId` run one-at-a-time (FIFO) in a per-session lane.
- Requests for different sessions can run in parallel.
- Lane policy is configurable (`collect`, `followup`, `steer`, `steer_backlog`, `interrupt`) with per-channel and per-session overrides.
- Session-local overrides can be managed at runtime via `agent.send` commands: `/queue`, `/queue set ...`, `/queue reset`.
- Backend selection for a turn is server-side (`native` by default, optional external backends per config: `claude_code`, `opencode`, `codex`, `gemini`, `pi_embedded`) and does not change JSON-RPC method signatures.

This is implemented via a per-lane queue (`LaneQueue`) in the gateway server, and used by `agent.send` and `agent.cancel`.

```mermaid
sequenceDiagram
  autonumber
  participant C as Client
  participant G as Gateway (WS JSON-RPC)
  participant LQ as LaneQueue (per-session)
  participant SB as SessionBridge
  participant A as AgentOrchestrator

  C->>G: agent.send {connectionId, message}
  G->>SB: resolve sessionId for connectionId
  SB-->>G: sessionId (laneId)
  G->>LQ: enqueue(laneId, work)

  alt lane idle
    LQ-->>G: starts work immediately
  else lane busy
    Note over LQ: work queued (FIFO) for this lane
    opt mode=interrupt and newer request arrives
      LQ->>SB: request cancellation of active lane run
      SB->>A: cancel() (best effort)
      G-->>C: transient preempt notice (queue.preempt)
    end
  end

  G->>A: process(message) in that session
  A-->>G: streaming events (content/tool_start/tool_end/context_warning)
  G-->>C: events + final done

  C->>G: agent.cancel {connectionId}
  G->>LQ: cancel(laneId) (queued items rejected)
  G->>SB: cancel active op (best-effort)
  G-->>C: result.cancelled=true/false
```

`interrupt` queue mode also requests active-run cancellation when a newer request is enqueued for the same session lane. Cancellation still completes at agent/tool-loop safe points. When this preemption happens, the requester receives a transient `content` notice and the audit log records `queue.preempt`.

### Base URL

- WebSocket: `ws://localhost:18800` (or `wss://` if using TLS)
- HTTP: `http://localhost:18800` (or `https://` if using TLS)
- Health check: `GET /health`

### Default Ports

- Gateway: `18800`
- Tailscale Serve (if enabled): `443` (HTTPS)

## Connection

### WebSocket Handshake

```javascript
// Browser
const ws = new WebSocket('ws://localhost:18800');

// Node.js (ws library)
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:18800');
```

### Connection Lifecycle

1. Client connects to WebSocket endpoint
2. Server validates authentication (if configured)
3. Connection assigned unique ID
4. Client can start sending requests
5. Server sends responses and events asynchronously

### Disconnection

```javascript
ws.onclose = (event) => {
  console.log('Disconnected:', event.code, event.reason);
};
```

Common close codes:
- `1000`: Normal closure
- `1001`: Endpoint going away
- `1006`: Abnormal closure (network issue)

## Authentication

### Bearer Token Auth

If `server.token` is configured, all WebSocket connections must provide authentication:

```javascript
const ws = new WebSocket('ws://localhost:18800', {
  headers: {
    'Authorization': 'Bearer your-secret-token'
  }
});
```

### Tailscale Identity

If `server.tailscale_identity` is enabled, connections from Tailscale are trusted based on the `Tailscale-User-Login` header.

```javascript
// Automatic when connecting via Tailscale
// No additional auth required if server.tailscale_identity is enabled
```

### HTTP Auth

If `server.auth_http` is `true` (default: true), HTTP requests also require bearer token when `server.token` is set:

```javascript
fetch('http://localhost:18800/api/health', {
  headers: {
    'Authorization': 'Bearer your-secret-token'
  }
});
```

Exceptions (handled by their own trust/auth model and therefore bypass gateway token auth):
- `POST /webhooks/:name` (HMAC-validated when webhook secret is configured)
- `POST /gmail/push` (Google Pub/Sub push)
- `POST /teams/events` (Microsoft Bot Framework activity callback)
- `POST /google-chat/events` (Google Chat event callback, optional webhook token check)
- `POST /bluebubbles/events` (BlueBubbles iMessage webhook callback, optional webhook token check)

WebChat PWA push-subscription endpoints (auth-protected):
- `GET /webchat/push/public-key` (returns enabled/configured push metadata)
- `GET /webchat/push/subscriptions` (returns current subscription count/cap)
- `POST /webchat/push/subscriptions` (registers/updates one browser subscription)
- `DELETE /webchat/push/subscriptions` (removes one browser subscription by endpoint)

## Message Format

### Request (Client → Server)

```typescript
interface GatewayRequest {
  id: number;           // Unique request ID (integer)
  method: string;       // Method name (e.g., 'agent.send', 'system.info')
  params?: Record<string, unknown>;  // Method parameters (optional)
}
```

Example:
```json
{
  "id": 1,
  "method": "agent.send",
  "params": {
    "message": "Hello, Flynn!",
    "session": "telegram:123456"
  }
}
```

### Response (Server → Client)

```typescript
interface GatewayResponse {
  id: number;           // Request ID this responds to
  result: unknown;      // Method result
}
```

Example:
```json
{
  "id": 1,
  "result": {
    "sessionId": "telegram:123456",
    "response": "Hello! How can I help you today?"
  }
}
```

### Error (Server → Client)

```typescript
interface GatewayError {
  id: number;
  error: {
    code: ErrorCode;     // Error code (integer)
    message: string;     // Human-readable error message
  };
}
```

Example:
```json
{
  "id": 1,
  "error": {
    "code": 4,
    "message": "Request cancelled by client"
  }
}
```

### Event (Server → Client)

```typescript
interface GatewayEvent {
  id: number;           // Request ID this relates to
  event: EventType;      // Event type string
  data: unknown;        // Event-specific data
}
```

Example:
```json
{
  "id": 1,
  "event": "content",
  "data": {
    "text": "Hello! How can I help you today?"
  }
}
```

## Methods

### System Methods

#### `system.info`

Get gateway information.

**Request:**
```json
{
  "id": 1,
  "method": "system.info"
}
```

**Response:**
```json
{
  "id": 1,
  "result": {
    "version": "0.1.0",
    "uptime": 12345,
    "connections": 2
  }
}
```

#### `system.disconnect`

Close the connection gracefully.

**Request:**
```json
{
  "id": 2,
  "method": "system.disconnect"
}
```

#### `system.presence`

Return tracked sender presence snapshots (most recent first).

Online/offline is inferred from inactivity threshold in the daemon.

**Request:**
```json
{
  "id": 3,
  "method": "system.presence",
  "params": {
    "channel": "telegram",
    "status": "online",
    "limit": 50
  }
}
```

#### `system.sessionAnalytics`

Return aggregate session analytics from the SQLite message history.

Useful for operator dashboards and trend checks (sessions/day, message volume, top active sessions).

**Request:**
```json
{
  "id": 10,
  "method": "system.sessionAnalytics",
  "params": {
    "days": 30,
    "topLimit": 10
  }
}
```

#### `system.contextUsage`

Return per-session estimated context-window budget snapshots.

Useful for proactive compaction monitoring and operator dashboards.

**Request:**
```json
{
  "id": 11,
  "method": "system.contextUsage"
}
```

#### `system.localBackends`

Return status for user-level local LLM backend daemons (for example `ollama.service` and `llama-server.service`).

**Request:**
```json
{
  "id": 12,
  "method": "system.localBackends"
}
```

**Response:**
```json
{
  "id": 12,
  "result": {
    "backends": [
      {
        "id": "ollama",
        "provider": "ollama",
        "name": "Ollama",
        "unit": "ollama.service",
        "configured": true,
        "loadState": "loaded",
        "activeState": "active",
        "subState": "running",
        "unitFileState": "enabled",
        "description": "Ollama Service",
        "pid": 12345,
        "result": "success",
        "statusText": "active (running)",
        "availableActions": ["restart", "stop"]
      }
    ]
  }
}
```

#### `system.dockerDependencies`

Return status for docker-compose managed dependencies discovered from `docker-compose.yml` (excluding Flynn's own `flynn` service). Includes profile-scoped services (for example `whisper-server`, `brave-search`, `searxng`) when profiles are defined.

**Request:**
```json
{
  "id": 12,
  "method": "system.dockerDependencies"
}
```

**Response:**
```json
{
  "id": 12,
  "result": {
    "dependencies": [
      {
        "id": "whisper",
        "name": "Whisper (whisper.cpp)",
        "service": "whisper-server",
        "configured": true,
        "state": "running",
        "health": "healthy",
        "statusText": "Up 4 minutes (healthy)",
        "containerName": "flynn-whisper-server-1",
        "availableActions": ["restart", "stop", "update"]
      },
      {
        "id": "brave-search",
        "name": "Brave Search",
        "service": "brave-search",
        "configured": true,
        "state": "running",
        "health": "healthy",
        "statusText": "Up 2 minutes",
        "containerName": "brave-search",
        "availableActions": ["restart", "stop", "update"]
      },
      {
        "id": "searxng",
        "name": "SearXNG",
        "service": "searxng",
        "configured": true,
        "state": "running",
        "health": "none",
        "statusText": "Up 2 minutes",
        "containerName": "searxng",
        "availableActions": ["restart", "stop", "update"]
      }
    ]
  }
}
```

#### `system.dockerDependencyControl`

Control a docker-compose dependency (`start`, `restart`, `stop`, `update`).

- `dependency` must match an ID returned by `system.dockerDependencies`.
- `update` pulls the latest image for that compose service and runs `docker compose up -d` to reconcile.

**Request:**
```json
{
  "id": 14,
  "method": "system.dockerDependencyControl",
  "params": {
    "dependency": "whisper",
    "action": "restart"
  }
}
```

**Response:**
```json
{
  "id": 14,
  "result": {
    "dependency": "whisper",
    "action": "restart",
    "status": {
      "id": "whisper",
      "state": "running",
      "health": "healthy",
      "statusText": "running (healthy)"
    },
    "message": "Restarted whisper-server container."
  }
}
```

#### `system.localBackendControl`

Control a local backend daemon (`start`, `restart`, `stop`, `update`).

- `update` semantics:
  - `ollama`: pulls configured Ollama models (tiers/local providers + embedding/audio models) via `ollama pull`.
  - `llamacpp`: performs a safe service restart (model file refresh remains external to Flynn).

**Request:**
```json
{
  "id": 13,
  "method": "system.localBackendControl",
  "params": {
    "backend": "ollama",
    "action": "restart"
  }
}
```

#### `system.observabilitySources`

Return graph/log-capable observability sources for the dashboard.

**Request:**
```json
{
  "id": 15,
  "method": "system.observabilitySources"
}
```

**Response:**
```json
{
  "id": 15,
  "result": {
    "sources": [
      {
        "id": "systemd:flynn",
        "name": "Flynn daemon",
        "kind": "systemd_system",
        "runtime": "systemd_system",
        "status": "running",
        "graphCapable": true,
        "logCapable": true
      },
      {
        "id": "docker:whisper",
        "name": "Whisper (whisper.cpp)",
        "kind": "docker_dependency",
        "runtime": "docker_compose",
        "status": "running",
        "graphCapable": true,
        "logCapable": true
      }
    ]
  }
}
```

#### `system.observabilitySeries`

Return sampled service trend points (bounded, in-memory) for dashboard charts.

**Request:**
```json
{
  "id": 16,
  "method": "system.observabilitySeries",
  "params": {
    "windowMinutes": 60,
    "bucketSeconds": 30,
    "sourceIds": ["systemd:flynn", "docker:whisper"]
  }
}
```

**Response:**
```json
{
  "id": 16,
  "result": {
    "generatedAt": 1739999999000,
    "windowMinutes": 60,
    "bucketSeconds": 30,
    "series": [
      {
        "sourceId": "systemd:flynn",
        "points": [
          { "ts": 1739999970000, "stateCode": 3, "healthCode": 2, "errorCount": 0, "restartCount": 0 },
          { "ts": 1739999985000, "stateCode": 3, "healthCode": 2, "errorCount": 0, "restartCount": 1 }
        ]
      }
    ]
  }
}
```

#### `system.serviceLogs`

Return recent logs for a discovered observability source. Flynn applies secret masking heuristics to returned lines.

**Request:**
```json
{
  "id": 17,
  "method": "system.serviceLogs",
  "params": {
    "sourceId": "docker:whisper",
    "lines": 200,
    "sinceSeconds": 900
  }
}
```

**Response:**
```json
{
  "id": 17,
  "result": {
    "sourceId": "docker:whisper",
    "fetchedAt": 1739999999000,
    "redacted": false,
    "truncated": false,
    "lines": [
      {
        "ts": 1739999990000,
        "level": "warn",
        "text": "queue depth rising"
      }
    ]
  }
}
```

**Response:**
```json
{
  "id": 13,
  "result": {
    "backend": "ollama",
    "action": "restart",
    "status": {
      "id": "ollama",
      "activeState": "active",
      "subState": "running",
      "statusText": "active (running)"
    }
  }
}
```

**Response:**
```json
{
  "id": 11,
  "result": {
    "sessions": [
      {
        "sessionId": "ws:abc-123",
        "budget": {
          "estimatedTokens": 172000,
          "contextWindow": 200000,
          "remainingTokens": 28000,
          "usagePct": 86,
          "thresholdPct": 80,
          "thresholdTokens": 160000,
          "shouldCompact": true
        }
      }
    ]
  }
}
```

**Response:**
```json
{
  "id": 10,
  "result": {
    "daily": [
      { "day": "2026-02-16", "sessions": 14, "messages": 228 }
    ],
    "topSessions": [
      { "sessionId": "telegram:123456", "messages": 42, "lastActivity": 1739700300 }
    ],
    "topTools": [
      { "toolName": "web.search", "executions": 37 }
    ],
    "topTopics": [
      { "topic": "kubernetes", "occurrences": 22 }
    ],
    "averageMessagesPerSession": 16.29,
    "totalSessions": 14,
    "totalMessages": 228
  }
}
```

**Response:**
```json
{
  "id": 3,
  "result": {
    "presence": [
      {
        "channel": "telegram",
        "senderId": "123456",
        "senderName": "alice",
        "firstSeenAt": 1739700000000,
        "lastSeenAt": 1739700300000,
        "messageCount": 12,
        "status": "online"
      }
    ],
    "summary": { "total": 1, "online": 1, "offline": 0 }
  }
}
```

**Response:**
```json
{
  "id": 2,
  "result": {
    "success": true
  }
}
```

### Session Methods

#### `sessions.list`

List sessions with optional persisted inclusion, frontend filtering, and paging.

**Request:**
```json
{
  "id": 3,
  "method": "sessions.list",
  "params": {
    "includePersisted": true,
    "frontend": "ws",
    "limit": 50,
    "offset": 0
  }
}
```

**Response:**
```json
{
  "id": 3,
  "result": {
    "sessions": [
      {
        "id": "telegram:123456",
        "frontend": "telegram",
        "userId": "123456",
        "messageCount": 42,
        "lastMessageAt": 1739448000000,
        "config": {
          "modelTier": "fast",
          "queue": {
            "mode": "followup",
            "overflow": "drop_old",
            "cap": 8,
            "debounceMs": 250,
            "summarizeOverflow": true
          },
          "elevation": {
            "active": false,
            "untilMs": 1739451600000
          }
        }
      }
    ],
    "total": 1
  }
}
```

#### `sessions.get`

Get session details.

**Request:**
```json
{
  "id": 4,
  "method": "sessions.get",
  "params": {
    "sessionId": "telegram:123456"
  }
}
```

**Response:**
```json
{
  "id": 4,
  "result": {
    "id": "telegram:123456",
    "createdAt": "2025-02-13T10:00:00Z",
    "lastActiveAt": "2025-02-13T12:00:00Z",
    "history": [
      {
        "role": "user",
        "content": "Hello"
      },
      {
        "role": "assistant",
        "content": "Hi there!"
      }
    ]
  }
}
```

#### `sessions.create`

Create or resume a session.

**Request:**
```json
{
  "id": 5,
  "method": "sessions.create",
  "params": {
    "sessionId": "telegram:123456"
  }
}
```

**Response:**
```json
{
  "id": 5,
  "result": {
    "sessionId": "telegram:123456",
    "created": true
  }
}
```

#### `sessions.delete`

Delete a session.

**Request:**
```json
{
  "id": 6,
  "method": "sessions.delete",
  "params": {
    "sessionId": "telegram:123456"
  }
}
```

**Response:**
```json
{
  "id": 6,
  "result": {
    "success": true
  }
}
```

### Agent Methods

#### `agent.send`

Send a message to the agent and stream response.

**Request:**
```json
{
  "id": 7,
  "method": "agent.send",
  "params": {
    "message": "What's the weather?",
    "sessionId": "telegram:123456",
    "attachments": [
      {
        "mimeType": "image/jpeg",
        "data": "base64encodedimage..."
      }
    ]
  }
}
```

**Response (final):**
```json
{
  "id": 7,
  "result": {
    "content": "I can't check the weather without access to weather APIs. Would you like me to help you with something else?",
    "usage": {
      "inputTokens": 25,
      "outputTokens": 30
    }
  }
}
```

**Events (streamed during processing):**

`content` event:
```json
{
  "id": 7,
  "event": "content",
  "data": {
    "text": "I can't check the weather"
  }
}
```

`tool_start` event:
```json
{
  "id": 7,
  "event": "tool_start",
  "data": {
    "tool": "shell.exec",
    "args": {
      "command": "echo 'Hello'"
    }
  }
}
```

`tool_end` event:
```json
{
  "id": 7,
  "event": "tool_end",
  "data": {
    "tool": "shell.exec",
    "result": {
      "success": true,
      "output": "Hello\n",
      "error": null
    }
  }
}
```

`attachment` event:
```json
{
  "id": 7,
  "event": "attachment",
  "data": {
    "mimeType": "image/png",
    "data": "base64encoded..."
  }
}
```

`context_warning` event:
```json
{
  "id": 7,
  "event": "context_warning",
  "data": {
    "level": "checkpoint",
    "message": "Context usage is 86.0% (172,000/200,000 estimated tokens). Checkpoint saved to memory namespace `session/checkpoints/ws/abc-123`.",
    "budget": {
      "estimatedTokens": 172000,
      "contextWindow": 200000,
      "remainingTokens": 28000,
      "usagePct": 86,
      "thresholdPct": 80,
      "thresholdTokens": 160000,
      "shouldCompact": true
    },
    "actions": {
      "checkpointSaved": true,
      "autoCompacted": false,
      "checkpointNamespace": "session/checkpoints/ws/abc-123"
    }
  }
}
```

`done` event:
```json
{
  "id": 7,
  "event": "done",
  "data": {
    "content": "Complete response here..."
  }
}
```

When queue policy rejects/supersedes a request before execution, the server emits an `error` event with `code: 3` (`AgentBusy`) and includes `data.queue` metadata (`code`, `laneId`, `mode`, `overflow`, `droppedCount`).

#### `agent.cancel`

Cancel the current agent operation.

Used by the web dashboard/chat stop button and channel-level `/stop` / `/cancel` command fast-paths.
Cancellation is best-effort and stops at the next agent/tool-loop safe point.
Flynn now propagates a run-level abort signal into model/tool execution, so providers/tools that honor `AbortSignal` typically stop promptly instead of waiting for request/tool timeouts.

### Node Methods

#### `node.register`

Register node role/capabilities for the current WebSocket connection.

**Request:**
```json
{
  "id": 9,
  "method": "node.register",
  "params": {
    "nodeId": "companion-desktop",
    "role": "companion",
    "protocolVersion": 1,
    "capabilities": ["ui.canvas", "notifications"]
  }
}
```

**Response:**
```json
{
  "id": 9,
  "result": {
    "registered": true,
    "node": { "id": "companion-desktop", "role": "companion" },
    "protocol": { "serverVersion": 1, "clientVersion": 1, "negotiatedVersion": 1 },
    "capabilities": {
      "declared": ["ui.canvas", "notifications"],
      "enabled": ["ui.canvas", "notifications"]
    }
  }
}
```

#### `node.capabilities.get`

Return negotiated capabilities for the currently registered node connection.

#### `node.location.set`

Update the last-known location for the currently registered node connection.
Requires `server.nodes.enabled: true` and `server.nodes.location.enabled: true`.

**Request:**
```json
{
  "id": 10,
  "method": "node.location.set",
  "params": {
    "latitude": 37.7749,
    "longitude": -122.4194,
    "accuracyMeters": 12.4,
    "source": "gps",
    "capturedAt": 1763241200000
  }
}
```

**Response:**
```json
{
  "id": 10,
  "result": {
    "updated": true,
    "node": { "id": "companion-desktop", "role": "companion" },
    "location": {
      "latitude": 37.7749,
      "longitude": -122.4194,
      "accuracyMeters": 12.4,
      "source": "gps",
      "capturedAt": 1763241200000,
      "receivedAt": 1763241200451
    }
  }
}
```

#### `node.location.get`

Return the stored last-known location for the currently registered node connection.

#### `node.status.set`

Publish companion/node runtime status metadata (for example macOS menu-bar heartbeat state).

**Request:**
```json
{
  "id": 12,
  "method": "node.status.set",
  "params": {
    "platform": "macos",
    "appVersion": "0.3.1",
    "deviceName": "MacBook Pro",
    "statusText": "Idle",
    "batteryPct": 64,
    "powerSource": "battery"
  }
}
```

#### `node.push_token.set`

Register a node push token (APNs or FCM) for companion delivery routing.
Requires `server.nodes.push.enabled: true`.

**Request:**
```json
{
  "id": 13,
  "method": "node.push_token.set",
  "params": {
    "provider": "fcm",
    "token": "fcm_abcdefghijklmnopqrstuvwxyz123456"
  }
}
```

#### `system.capabilities`

Return gateway protocol version, node policy status, and feature-gate snapshot.

**Request:**
```json
{
  "id": 11,
  "method": "system.capabilities"
}
```

**Response:**
```json
{
  "id": 11,
  "result": {
    "protocol": { "version": 1 },
    "nodes": {
      "enabled": true,
      "locationEnabled": true,
      "pushEnabled": true,
      "allowedRoles": ["companion"],
      "registered": true,
      "role": "companion",
      "nodeId": "companion-desktop"
    },
    "featureGates": {
      "ui.canvas": true
    }
  }
}
```

#### `system.location`

Return the operator-facing snapshot of registered node locations.

#### `system.nodes`

Return the operator-facing snapshot of registered node connections (identity, role, capabilities, location/status).
Push tokens are returned as masked previews (`tokenPreview`) and never exposed in full.

### Canvas Methods

#### `canvas.put`

Upsert a session-scoped canvas artifact.

**Request:**
```json
{
  "id": 12,
  "method": "canvas.put",
  "params": {
    "sessionId": "ws:abc123",
    "artifactId": "summary-card",
    "type": "note",
    "title": "Draft Summary",
    "content": { "markdown": "## Notes" },
    "metadata": { "lane": "analysis" }
  }
}
```

#### `canvas.get`

Fetch a single artifact by id.

#### `canvas.list`

List artifacts for a session (newest first).

#### `canvas.delete`

Delete a single artifact.

#### `canvas.clear`

Delete all artifacts for a session.

#### `agent.setToolUseCallback`

Set callback for tool use events (for confirmation UI).

**Request:**
```json
{
  "id": 9,
  "method": "agent.setToolUseCallback",
  "params": {
    "sessionId": "telegram:123456",
    "enabled": true
  }
}
```

**Response:**
```json
{
  "id": 9,
  "result": {
    "success": true
  }
}
```

### Tool Methods

#### `tools.list`

List available tools.

**Request:**
```json
{
  "id": 10,
  "method": "tools.list",
  "params": {
    "sessionId": "telegram:123456"
  }
}
```

**Response:**
```json
{
  "id": 10,
  "result": {
    "tools": [
      {
        "name": "shell.exec",
        "description": "Execute a shell command...",
        "inputSchema": {
          "type": "object",
          "properties": {
            "command": { "type": "string" }
          }
        }
      }
    ]
  }
}
```

#### `tools.execute`

Execute a tool directly (bypass agent).

**Request:**
```json
{
  "id": 11,
  "method": "tools.execute",
  "params": {
    "sessionId": "telegram:123456",
    "tool": "shell.exec",
    "args": {
      "command": "echo 'Hello'"
    }
  }
}
```

**Response:**
```json
{
  "id": 11,
  "result": {
    "success": true,
    "output": "Hello\n"
  }
}
```

### Config Methods

#### `config.get`

Get current configuration (secrets redacted).

**Request:**
```json
{
  "id": 12,
  "method": "config.get"
}
```

**Response:**
```json
{
  "id": 12,
  "result": {
    "models": {
      "default": {
        "anthropic": {
          "apiKey": "***"
        }
      }
    }
  }
}
```

#### `config.reload`

Reload configuration from file.

**Request:**
```json
{
  "id": 13,
  "method": "config.reload"
}
```

**Response:**
```json
{
  "id": 13,
  "result": {
    "success": true
  }
}
```

### Pairing Methods

#### `pairing.generate`

Generate a pairing code for unknown senders.

**Request:**
```json
{
  "id": 14,
  "method": "pairing.generate",
  "params": {
    "channel": "telegram"
  }
}
```

**Response:**
```json
{
  "id": 14,
  "result": {
    "code": "ABC123",
    "expiresAt": "2025-02-13T12:05:00Z"
  }
}
```

#### `pairing.list`

List active pairing codes.

**Request:**
```json
{
  "id": 15,
  "method": "pairing.list"
}
```

**Response:**
```json
{
  "id": 15,
  "result": {
    "codes": [
      {
        "code": "ABC123",
        "channel": "telegram",
        "createdAt": "2025-02-13T12:00:00Z",
        "expiresAt": "2025-02-13T12:05:00Z"
      }
    ]
  }
}
```

## Events

### Event Types

#### `content`

Streamed text content from the agent.

```json
{
  "id": 1,
  "event": "content",
  "data": {
    "text": "Partial response..."
  }
}
```

#### `tool_start`

Tool execution started.

```json
{
  "id": 1,
  "event": "tool_start",
  "data": {
    "tool": "shell.exec",
    "args": {
      "command": "echo 'test'"
    }
  }
}
```

#### `tool_end`

Tool execution completed.

```json
{
  "id": 1,
  "event": "tool_end",
  "data": {
    "tool": "shell.exec",
    "result": {
      "success": true,
      "output": "test\n"
    }
  }
}
```

#### `attachment`

Outbound attachment (image, audio, file).

```json
{
  "id": 1,
  "event": "attachment",
  "data": {
    "mimeType": "image/jpeg",
    "data": "base64encoded...",
    "filename": "output.jpg"
  }
}
```

#### `context_warning`

Proactive context pressure signal emitted by `agent.send` before `done`.

```json
{
  "id": 1,
  "event": "context_warning",
  "data": {
    "level": "warning",
    "message": "Context usage is 76.0% (152000/200000 estimated tokens).",
    "budget": {
      "estimatedTokens": 152000,
      "contextWindow": 200000,
      "remainingTokens": 48000,
      "usagePct": 76,
      "thresholdPct": 80,
      "thresholdTokens": 160000,
      "shouldCompact": false
    },
    "actions": {
      "checkpointSaved": false,
      "autoCompacted": false
    }
  }
}
```

#### `done`

Agent processing complete (final response).

```json
{
  "id": 1,
  "event": "done",
  "data": {
    "content": "Complete final response..."
  }
}
```

#### `error`

Error occurred during processing.

```json
{
  "id": 1,
  "event": "error",
  "data": {
    "code": 5,
    "message": "Internal error: ...",
    "queue": {
      "code": "overflow",
      "laneId": "ws:abc123",
      "mode": "followup",
      "overflow": "drop_new",
      "droppedCount": 1
    }
  }
}
```

`data.queue` is optional and only present for queue policy rejections/superseded requests.

## Error Codes

| Code | Name | Description |
|------|------|-------------|
| -1 | `ParseError` | Invalid JSON in request |
| -2 | `InvalidRequest` | Missing required fields |
| -3 | `MethodNotFound` | Unknown method name |
| -4 | `AuthRequired` | Authentication required but not provided |
| -5 | `AuthFailed` | Authentication failed |
| 1 | `SessionNotFound` | Session ID doesn't exist |
| 2 | `ToolNotFound` | Tool name doesn't exist |
| 3 | `AgentBusy` | Agent is processing another request |
| 4 | `RequestCancelled` | Request was cancelled by client |
| 5 | `InternalError` | Unexpected server error |

## Example Client

### Browser Client

```javascript
class FlynnClient {
  constructor(url, token) {
    this.url = url;
    this.token = token;
    this.requestId = 0;
    this.pending = new Map();
  }

  connect() {
    const headers = {};
    if (this.token) {
      headers['Authorization'] = `Bearer ${this.token}`;
    }

    this.ws = new WebSocket(this.url, { headers });

    this.ws.onopen = () => {
      console.log('Connected to Flynn gateway');
    };

    this.ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      this.handleMessage(message);
    };

    this.ws.onerror = (error) => {
      console.error('WebSocket error:', error);
    };

    this.ws.onclose = () => {
      console.log('Disconnected from Flynn gateway');
    };
  }

  sendRequest(method, params = {}) {
    return new Promise((resolve, reject) => {
      const id = ++this.requestId;

      const request = {
        id,
        method,
        params
      };

      this.ws.send(JSON.stringify(request));

      // Store promise for response
      this.pending.set(id, { resolve, reject });

      // Set timeout
      setTimeout(() => {
        if (this.pending.has(id)) {
          this.pending.delete(id);
          reject(new Error('Request timeout'));
        }
      }, 30000);
    });
  }

  handleMessage(message) {
    // Response
    if ('result' in message) {
      const { id, result } = message;
      const pending = this.pending.get(id);
      if (pending) {
        this.pending.delete(id);
        pending.resolve(result);
      }
    }

    // Error
    else if ('error' in message) {
      const { id, error } = message;
      const pending = this.pending.get(id);
      if (pending) {
        this.pending.delete(id);
        const err = new Error(error.message);
        err.code = error.code;
        pending.reject(err);
      }
    }

    // Event
    else if ('event' in message) {
      this.handleEvent(message);
    }
  }

  handleEvent(event) {
    const { id, event: eventType, data } = event;

    switch (eventType) {
      case 'content':
        console.log('Content:', data.text);
        break;
      case 'tool_start':
        console.log('Tool started:', data.tool, data.args);
        break;
      case 'tool_end':
        console.log('Tool completed:', data.tool, data.result);
        break;
      case 'attachment':
        console.log('Attachment received:', data.mimeType);
        break;
      case 'context_warning':
        console.warn('Context warning:', data.level, data.message);
        break;
      case 'done':
        console.log('Done:', data.content);
        break;
      case 'error':
        console.error('Error:', data.code, data.message);
        break;
    }
  }

  // Convenience methods
  async systemInfo() {
    return this.sendRequest('system.info');
  }

  async listSessions() {
    return this.sendRequest('sessions.list');
  }

  async contextUsage() {
    return this.sendRequest('system.contextUsage');
  }

  async sendMessage(message, sessionId, attachments = []) {
    return this.sendRequest('agent.send', {
      message,
      sessionId,
      attachments
    });
  }

  async listTools(sessionId) {
    return this.sendRequest('tools.list', { sessionId });
  }
}

// Usage
const client = new FlynnClient('ws://localhost:18800', 'your-token');
client.connect();

client.onopen = async () => {
  const info = await client.systemInfo();
  console.log('Gateway info:', info);

  const response = await client.sendMessage('Hello, Flynn!', 'telegram:123456');
  console.log('Response:', response);
};
```

### Node.js Client

```javascript
const WebSocket = require('ws');

class FlynnNodeClient extends FlynnClient {
  connect() {
    const options = {};
    if (this.token) {
      options.headers = { 'Authorization': `Bearer ${this.token}` };
    }

    this.ws = new WebSocket(this.url, options);

    // ... same as browser client
  }
}

// Usage
const client = new FlynnNodeClient('ws://localhost:18800', 'your-token');
client.connect();
```

### HTTP Fetch Example

```javascript
// Health check
async function checkHealth() {
  const response = await fetch('http://localhost:18800/health', {
    headers: {
      'Authorization': 'Bearer your-token'
    }
  });

  const status = await response.json();
  console.log('Health:', status);
}

checkHealth();
```

---

For more implementation details, see:
- Protocol types: `src/gateway/protocol.ts`
- Handlers: `src/gateway/handlers/`
- Gateway server: `src/gateway/server.ts`
- Companion runtime client helper: `src/companion/runtimeClient.ts` (node + system + `canvas.*` typed RPC wrappers, optional `autoConnect`)
- Platform companion wrappers: `src/companion/platformClients.ts`
