Skip to content

Daemon Protocol

Wire protocol between the daemon and frontends.

The daemon communicates with frontends (TUI, web UI) over WebSocket using JSON-encoded messages. The protocol is defined in mew-protocol.

Messages are tagged enums serialized with serde(tag = "type"):

{"type": "Prompt", "text": "hello", "attachments": []}
{"type": "SessionReady", "session_id": "sess_01J...", "model": "deepseek-v4-flash"}

Encoding/decoding via encode_json / decode_json in mew-protocol.

Message Fields Purpose
NewSession cwd?: String Create a fresh session
AttachSession session_id: String Attach to an existing session
ListSessions List all known sessions
Prompt text: String, attachments: Vec<Attachment> Send a user prompt
Cancel Cancel the current turn
PermissionResponse request_id: u64, decision: PermissionDecision Respond to a permission prompt
AskUserResponse request_id: u64, answers: Vec<String> Respond to an ask-user question
SlashCommand command: String Run /clear, /compact, etc. on the daemon
ListModels Request the available model list
SwitchModel provider: String, model: String Switch to a different model
SetThinkingVariant variant: String Set or clear thinking variant (“off” or “none” disables)
Ping Liveness check; daemon replies with Pong
ListProjects List known project directories (for project picker UI)
RegenerateTitle session_id: String Regenerate the session title from the first user message via LLM
Message Fields Purpose
SessionReady session_id, model?, provider? Session ready for prompts
SessionHistory messages: Vec<Message> Full message replay on resume
SessionList sessions: Vec<SessionInfo> Response to ListSessions
SessionCleared Context cleared (broadcast to all clients)
SessionTitleChanged session_id, title Daemon generated a title
Message Fields Purpose
Provider event: ProviderEventWire Raw provider event (PartStart, PartDelta, etc.)
ToolStart call_id Tool execution started
ToolEnd call_id, success Tool execution finished
ToolProgress call_id, chunk Intermediate tool output
PartUpdated part_id, part A part’s content or state changed
Message Fields Purpose
PermissionRequest request_id, tool_name, input Ask user to approve a tool call
AskUserRequest request_id, call_id, questions Ask user free-text questions
RequestResolved request_id A pending request was resolved by any client
Message Fields Purpose
SubagentStart parent_call_id, name, child_session_id, display_name? Subagent spawned
SubagentStatus parent_call_id, tool_name, message Subagent progress update
SubagentEnd parent_call_id, child_session_id, outcome Subagent finished
Message Fields Purpose
ModelList models: Vec<ModelInfo> Response to ListModels
ModelSwitched provider, model Confirms model switch
ThinkingVariantChanged variant?: String Confirms thinking variant (null = disabled)
TodosUpdated todos: Vec<Todo> Session todo list changed
PersonaSwitchRequested name switch_persona tool was called
JobUpdate job_id, command, state Background shell job changed
SlashResult text Slash command produced text output
Error message Error before or outside a turn
ErrorEvent message Terminal error during a turn
Pong version: String Response to Ping; carries daemon version
ProjectList projects: Vec<ProjectInfo> Response to ListProjects; deduped project directories with session counts

The daemon owns sessions via SessionManager (mew-daemon/src/session.rs). Connections attach to sessions. Key behaviors:

  • Broadcasting: session.broadcast(msg) sends to all attached clients and removes any that have disconnected.
  • Turn serialization: turn_lock ensures only one turn runs at a time. A second Prompt while a turn is in progress receives an error.
  • Cancellation: each turn gets a fresh CancellationToken. Cancel from any client cancels the current turn.
  • Permission/ask-user requests: go to all clients. Any client can respond. RequestResolved dismisses the modal everywhere.

See mew-daemon/src/session.rs for the full Session struct definition.

Client connects
→ NewSession or AttachSession
→ SessionReady + SessionHistory
→ Prompt
→ Provider events stream (PartStart → PartDelta → ... → MessageEnd)
→ (optional) PermissionRequest → PermissionResponse → RequestResolved
→ (optional) ToolStart → ToolProgress → ToolEnd
→ (optional) more turns
Client disconnects
→ detach_client
→ if last client: cancel turn, remove session from active

Idle sessions can be resumed from disk. The session writer persists every message to ~/.local/share/mew/sessions/<id>/session.jsonl. On resume, Agent::load_messages replays the history.

translate_event (mew-daemon/src/lib.rs) converts each AgentEvent into zero or more ServerMessages:

  • AgentEvent::Provider(pe)ServerMessage::Provider { event: ProviderEventWire::from(pe) }
  • AgentEvent::PermissionRequest { call, tx } → assigns a request_id, stashes the oneshot::Sender in session.pending_permissions, emits ServerMessage::PermissionRequest
  • AgentEvent::AskUser { questions, tx } → same pattern with pending_ask_user

For the TUI daemon-client (mew-daemon/src/client.rs), translate_server_message reverses the translation. Channel-bearing messages reconstruct oneshot channels and return AgentEvent::PermissionRequest { call, tx } with a fresh oneshot::Sender that the client maps to ClientMessage::PermissionResponse.

Messages that don’t map to AgentEvent (model list, session list, etc.) return Vec::new() and are handled by the DaemonClient directly.

  1. Add the variant to ClientMessage or ServerMessage in mew-protocol/src/lib.rs
  2. Add a roundtrip test in the protocol test module
  3. Handle the new message in handle_connection (mew-daemon/src/lib.rs)
  4. Update translate_server_message in mew-daemon/src/client.rs if it’s a ServerMessage the TUI daemon-client needs to handle
  5. Add the TypeScript type + dispatch to mew-web-client/src/index.ts
  6. Wire the store action in mew-web-ui/src/stores/session.ts