Cmd+Ctrl Daemon Protocol Specification
Version: 1.0 Status: Stable Last updated: February 2026
This document defines the WebSocket protocol between a Cmd+Ctrl daemon and the Cmd+Ctrl backend server. Any program that implements this protocol can act as a daemon, enabling its agent to be controlled via the Cmd+Ctrl web, iOS, and Android clients.
Overview
A daemon is a long-running process on a developer's workstation that:
- Connects outbound to the Cmd+Ctrl server via WebSocket
- Receives task instructions from the server (originating from a user's client)
- Runs an AI agent (or any tool) to process those instructions
- Streams progress and results back to the server
- Serves message history on demand
The server never connects inbound to the daemon — all communication flows over a single outbound WebSocket initiated by the daemon.
┌─────────────────┐ WebSocket (outbound) ┌─────────────────┐
│ │ ────────────────────────────▶ │ │
│ Daemon │ │ Cmd+Ctrl │
│ (your machine) │ ◀──────────────────────────── │ Server │
│ │ JSON messages │ │
└────────┬────────┘ └────────┬────────┘
│ │
▼ ▼
┌──────────┐ ┌──────────────┐
│ Agent │ │ Clients │
│ (LLM, │ │ (Web, iOS, │
│ tool) │ │ Android) │
└──────────┘ └──────────────┘Table of Contents
- Authentication
- Connection Lifecycle
- Message Format
- Server → Daemon Messages
- Daemon → Server Messages
- Event Types
- Session ID Architecture
- Message Storage
- Task Lifecycle
- Optional Capabilities
Authentication
Device Registration
Before connecting, a daemon must register with the server using the device authorization flow (similar to GitHub CLI):
- Daemon →
POST /api/devices/code— requests a verification code - Server responds with a verification URL and code
- User opens the URL in their browser, enters the code, and authenticates
- Server issues a refresh token to the daemon (via polling or callback)
- Daemon stores the token locally (e.g.,
~/.cmdctrl-myagent/credentials, chmod 600)
Connection Headers
When opening the WebSocket, the daemon must send these headers:
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer <refresh_token> |
X-Device-ID | Yes | Device ID assigned during registration |
X-Agent-Type | No | Your agent type identifier (e.g., claude_code, my_custom_agent). Informational — the server uses the agent type from device registration. |
X-Daemon-Version | Yes | Semantic version of your daemon (e.g., 1.0.0) |
The agent type is set during device registration (the agentType field in the device code request). It is a free-form snake_case string that appears in the UI and is used to construct canonical session IDs. The X-Agent-Type header is informational and may be used for logging, but the server reads the authoritative value from the device's registration record.
Connection Lifecycle
1. Connect
Open a WebSocket to wss://<server>/ws/daemon with the headers above.
2. Send Initial Status
Immediately after the connection opens, send two messages:
{"type": "status", "running_tasks": []}{"type": "report_sessions", "sessions": []}The running_tasks array should list any task IDs that are still running from a previous connection (after a reconnect). For a fresh start, send an empty array.
The report_sessions message reports all sessions the daemon knows about. Send an empty array if there are none.
3. Ping/Pong Keepalive
The server sends {"type": "ping"} periodically. Respond immediately with {"type": "pong"}. The server will disconnect daemons that don't respond to pings.
Additionally, send a WebSocket-level ping frame every 30 seconds to detect dead connections.
4. Reconnection
If the connection drops, reconnect with exponential backoff:
- Start at 1 second
- Double on each failure
- Cap at 30 seconds
- Reset to 1 second on successful connection
Message Format
All messages are JSON objects with a type field that determines the message schema. Messages are sent as WebSocket text frames.
Server → Daemon Messages
These are messages the server sends to your daemon. Your daemon must handle all required messages.
ping (Required)
{"type": "ping"}Respond with {"type": "pong"}.
task_start (Required)
Start a new task. The user has created a session and sent their first message.
{
"type": "task_start",
"task_id": "device123:my_agent:PENDING-abc123",
"instruction": "Fix the authentication bug in login.py",
"project_path": "/home/user/myproject"
}| Field | Type | Description |
|---|---|---|
task_id | string | Canonical session ID (see Session ID Architecture) |
instruction | string | The user's message / instruction |
project_path | string? | Optional working directory hint |
On receiving this, your daemon should:
- Start your agent with the given instruction
- Generate a native session ID (any unique string your agent uses to identify sessions)
- Send a
SESSION_STARTEDevent with the native session ID - Stream progress events as the agent works
- Send
TASK_COMPLETE,WAIT_FOR_USER, orERRORwhen done
task_resume (Required)
Continue an existing session. The user has sent a follow-up message.
{
"type": "task_resume",
"task_id": "device123:my_agent:native-session-id",
"session_id": "native-session-id",
"message": "Use the PostgreSQL approach instead",
"project_path": "/home/user/myproject"
}| Field | Type | Description |
|---|---|---|
task_id | string | Canonical session ID |
session_id | string | Your agent's native session ID |
message | string | The user's follow-up message |
project_path | string? | Optional working directory hint |
task_cancel (Required)
Stop a running task.
{
"type": "task_cancel",
"task_id": "device123:my_agent:native-session-id"
}Your daemon should stop the agent and clean up resources. No response message is expected (the task simply stops producing events).
get_messages (Required)
Retrieve message history for a session. The daemon is the source of truth for message content.
{
"type": "get_messages",
"request_id": "req-uuid-123",
"session_id": "native-session-id",
"limit": 50,
"before_uuid": "msg-uuid-oldest-loaded",
"after_uuid": "msg-uuid-newest-loaded"
}| Field | Type | Description |
|---|---|---|
request_id | string | Echo this back in your response |
session_id | string | Native session ID |
limit | number | Max messages to return |
before_uuid | string? | Cursor: return messages older than this UUID |
after_uuid | string? | Cursor: return messages newer than this UUID |
Respond with a messages response (see Daemon → Server Messages).
watch_session
Sent when a user opens a session in the UI. The daemon should track which sessions are being watched and push session_activity updates when new activity occurs.
{
"type": "watch_session",
"session_id": "native-session-id",
"file_path": "/home/user/.my-agent/sessions/abc.json"
}unwatch_session
Sent when a user closes a session in the UI. Stop monitoring and pushing updates.
{
"type": "unwatch_session",
"session_id": "native-session-id"
}version_status (Optional)
Server notification about daemon version requirements.
{
"type": "version_status",
"status": "update_available",
"your_version": "1.0.0",
"min_version": "0.9.0",
"recommended_version": "1.1.0",
"latest_version": "1.1.0",
"changelog_url": "https://github.com/example/daemon/releases",
"message": "Bug fixes and performance improvements"
}| Field | Required | Description |
|---|---|---|
status | Yes | One of current, update_available, update_required |
your_version | Yes | The version you reported via X-Daemon-Version |
min_version | No | Minimum supported version (present when update_required) |
recommended_version | No | Recommended version to upgrade to |
latest_version | No | Latest available version |
changelog_url | No | URL to release notes |
message | No | Human-readable message |
Status values:
current— you're up to dateupdate_available— newer version exists (informational)update_required— your version is below the minimum; server will disconnect
Daemon → Server Messages
These are messages your daemon sends to the server.
pong (Required)
Response to ping.
{"type": "pong"}status (Required)
Report which tasks are currently running. Send on connect and whenever the set of running tasks changes.
{
"type": "status",
"running_tasks": ["device123:my_agent:session-1", "device123:my_agent:session-2"]
}event (Required)
Report task progress or completion. This is the primary message type for communicating agent activity.
{
"type": "event",
"task_id": "device123:my_agent:PENDING-abc123",
"event_type": "SESSION_STARTED",
"session_id": "native-session-id"
}The event_type field determines which additional fields are expected. See Event Types.
messages (Required)
Response to a get_messages request.
{
"type": "messages",
"request_id": "req-uuid-123",
"session_id": "native-session-id",
"messages": [
{
"uuid": "msg-uuid-1",
"role": "USER",
"content": "Fix the auth bug",
"timestamp": "2026-02-19T10:00:00Z"
},
{
"uuid": "msg-uuid-2",
"role": "AGENT",
"content": "I've identified the issue in login.py...",
"timestamp": "2026-02-19T10:00:15Z"
}
],
"has_more": false,
"oldest_uuid": "msg-uuid-1",
"newest_uuid": "msg-uuid-2"
}| Field | Type | Description |
|---|---|---|
request_id | string | Must match the request |
session_id | string | Native session ID |
messages | array | Array of MessageEntry objects |
has_more | boolean | True if there are older messages beyond what was returned |
oldest_uuid | string? | UUID of the oldest message in this response |
newest_uuid | string? | UUID of the newest message in this response |
error | string? | Error message if retrieval failed |
MessageEntry:
| Field | Type | Description |
|---|---|---|
uuid | string | Unique message identifier (UUID v4 recommended) |
role | string | USER, AGENT, or SYSTEM |
content | string | Message text (Markdown supported) |
timestamp | string | ISO 8601 timestamp |
report_sessions (Required)
Report all sessions the daemon knows about. Sent on connect and periodically thereafter (e.g., every 30 seconds) so the server discovers new sessions started outside of Cmd+Ctrl.
{
"type": "report_sessions",
"sessions": [
{
"session_id": "native-session-id",
"slug": "fix-auth-bug",
"title": "Fix authentication bug",
"project": "/home/user/myproject",
"project_name": "myproject",
"file_path": "/home/user/.my-agent/sessions/abc.json",
"last_message": "Done! The auth bug is fixed.",
"last_activity": "2026-02-19T10:30:00Z",
"is_active": false,
"message_count": 5
}
]
}Send "sessions": [] if there are no sessions to report.
session_activity
Report that a watched session has new activity. Send this when a session tracked via watch_session changes.
{
"type": "session_activity",
"session_id": "native-session-id",
"file_path": "/home/user/.my-agent/sessions/abc.json",
"last_message": "Here's the fix...",
"message_count": 6,
"is_completion": true,
"last_activity": "2026-02-19T10:35:00Z"
}| Field | Type | Description |
|---|---|---|
is_completion | boolean | True when the last message is from the agent (triggers push notifications) |
user_message_uuid | string? | UUID of the user message that triggered this activity |
Event Types
Events are sent via the event message type. The event_type field determines the schema.
SESSION_STARTED (Required)
Must be the first event sent for a new task. This tells the server the agent's native session ID so the PENDING placeholder can be resolved.
{
"type": "event",
"task_id": "device123:my_agent:PENDING-abc123",
"event_type": "SESSION_STARTED",
"session_id": "my-native-session-id-12345"
}After this event, the server updates the canonical session ID from device123:my_agent:PENDING-abc123 to device123:my_agent:my-native-session-id-12345. All subsequent events for this task should use the new canonical ID.
PROGRESS (Optional, Recommended)
Report what the agent is currently doing. Displayed as a status indicator in the UI.
{
"type": "event",
"task_id": "device123:my_agent:session-id",
"event_type": "PROGRESS",
"action": "Reading",
"target": "login.py"
}| Field | Type | Description |
|---|---|---|
action | string | Short verb (e.g., "Reading", "Writing", "Thinking", "Searching") |
target | string | What the action applies to (e.g., filename, URL, description) |
OUTPUT (Optional)
Verbose streaming output. Displayed in the "verbose" view for users who want to see raw agent output.
{
"type": "event",
"task_id": "device123:my_agent:session-id",
"event_type": "OUTPUT",
"output": "Analyzing the authentication flow...\n",
"user_message_uuid": "uuid-of-user-message"
}| Field | Type | Description |
|---|---|---|
output | string | Raw text output from the agent |
user_message_uuid | string? | Associates output with the user message that triggered it |
WAIT_FOR_USER (Required for interactive agents)
The agent needs user input before continuing. The session becomes "awaiting reply" in the UI.
{
"type": "event",
"task_id": "device123:my_agent:session-id",
"event_type": "WAIT_FOR_USER",
"prompt": "Which database should I use?",
"context": "I've analyzed the requirements and there are two options...",
"options": [
{"label": "PostgreSQL"},
{"label": "SQLite"}
],
"result": "Which database should I use?\n\nI've analyzed the requirements..."
}| Field | Type | Description |
|---|---|---|
prompt | string | The question being asked |
context | string? | Additional context for the question |
options | array? | Suggested response options (each with a label field) |
result | string | Full message content displayed in the conversation |
user_message_uuid | string? | UUID of the triggering user message |
TASK_COMPLETE (Required)
The agent has finished processing and is not waiting for user input.
{
"type": "event",
"task_id": "device123:my_agent:session-id",
"event_type": "TASK_COMPLETE",
"result": "I've fixed the authentication bug in login.py. The issue was...",
"user_message_uuid": "uuid-of-user-message"
}| Field | Type | Description |
|---|---|---|
result | string | The agent's final response (Markdown supported) |
user_message_uuid | string? | UUID of the triggering user message |
ERROR (Required)
An error occurred during task execution.
{
"type": "event",
"task_id": "device123:my_agent:session-id",
"event_type": "ERROR",
"error": "Agent process crashed: exit code 1"
}Session ID Architecture
Cmd+Ctrl uses a three-part canonical session ID format:
<device_id>:<agent_type>:<native_session_id>Example: macbook-pro:claude_code:sess_abc123
| Part | Source | Description |
|---|---|---|
device_id | Registration | Assigned when the daemon registers |
agent_type | Daemon header | The X-Agent-Type value |
native_session_id | Your agent | Whatever ID your agent uses internally |
The PENDING Flow
When a user creates a new session, the server doesn't yet know your agent's native session ID. It uses a placeholder:
macbook-pro:claude_code:PENDING-uuid-from-serverYour daemon receives a task_start with this PENDING task ID. Once your agent starts and you know its native session ID, send a SESSION_STARTED event with the real ID. The server then migrates:
BEFORE: macbook-pro:claude_code:PENDING-abc123
AFTER: macbook-pro:claude_code:sess_real_id_456For task_resume, the task_id already contains the resolved native session ID.
Message Storage
The daemon is the source of truth for message history. The server stores only metadata (title, status, timestamps) — not message content. When a client needs to display messages, the server sends a get_messages request to the daemon, which returns the messages.
Your daemon must:
- Store all messages (user and agent) with UUIDs and timestamps
- Support cursor-based pagination via
before_uuid/after_uuid - Respond to
get_messagesrequests even when the agent is idle
How you store messages is up to you — JSONL files, SQLite, in-memory, etc. The only requirement is the response format.
Task Lifecycle
New Task (task_start)
Client creates session → Server sends task_start → Daemon starts agent
→ Daemon sends SESSION_STARTED (with native session ID)
→ Daemon sends PROGRESS events (optional)
→ Daemon sends OUTPUT events (optional)
→ Daemon sends TASK_COMPLETE or WAIT_FOR_USER or ERROR
→ Daemon updates status (removes task from running_tasks)Follow-up Message (task_resume)
Client sends message → Server sends task_resume → Daemon resumes agent
→ Daemon sends PROGRESS events (optional)
→ Daemon sends TASK_COMPLETE or WAIT_FOR_USER or ERROR
→ Daemon updates statusCancellation
Client cancels → Server sends task_cancel → Daemon stops agent
→ Daemon updates status (removes task from running_tasks)Optional Capabilities
These features improve the user experience but are not required for basic functionality.
Session Watching
When the server sends watch_session, a user has opened that session in the UI. Monitor the session for changes and send session_activity messages to push updates.
Implementation Checklist
Minimum Viable Daemon
- [ ] Register with server (device auth flow)
- [ ] Connect via WebSocket with required headers
- [ ] Send
statusandreport_sessionson connect - [ ] Handle
ping→ respond withpong - [ ] Handle
task_start→ start agent, sendSESSION_STARTED, thenTASK_COMPLETE/WAIT_FOR_USER/ERROR - [ ] Handle
task_resume→ resume agent, send completion event - [ ] Handle
task_cancel→ stop agent - [ ] Handle
get_messages→ return message history - [ ] Handle
watch_session/unwatch_session→ track watched sessions, pushsession_activity - [ ] Send
statusupdates when running tasks change - [ ] Reconnect with exponential backoff
Enhanced Daemon
- [ ] Send
PROGRESSevents for live status updates - [ ] Send
OUTPUTevents for verbose streaming - [ ] Report sessions with full metadata via
report_sessions - [ ] Handle
version_statusfor update notifications