Architecture
This document covers muxd's internal architecture for contributors.
Project Structure
muxd uses internal/ sub-packages organized by domain. main.go is pure wiring: all business logic lives in packages.
| Package | Purpose | Key exports |
|---|---|---|
main.go | Entry point, flag parsing, wiring | |
domain | Shared types (zero internal deps) | ContentBlock, TranscriptMessage, Session, NewUUID, CommandDef |
config | Configuration and preferences | ConfigDir, DataDir, LoadAPIKey, Preferences, LoadPricing |
store | SQLite persistence | Store, OpenStore |
provider | LLM provider abstraction (Anthropic, OpenAI, Mistral, Grok/xAI, ZAI, Fireworks, Ollama) | Provider, AnthropicProvider, OpenAIProvider, MistralProvider, GrokProvider, ZAIProvider, FireworksProvider, OllamaProvider, ResolveModel, ModelCost |
tools | 37 built-in agent tools | ToolDef, ToolContext, AllTools |
agent | Agent loop (adapter-independent) | Service, Event, CompactMessages |
checkpoint | Git undo/redo | DetectGitRepo, StashCreate |
daemon | HTTP server, client, lockfile | Server, DaemonClient, WriteLockfile, ReadLockfile |
service | OS service management | HandleCommand (install/uninstall/start/stop) |
tui | Bubble Tea TUI | Model, InitialModel, Update, View, RenderAssistantLines |
mcp | MCP server management (stdio/HTTP transports) | Manager, LoadConfig, ConvertTools |
hub | Hub coordinator for multi-node management | Hub, NewHub, HubClient, NodeClient, Node, HubLockfile, OpenHubStore |
Dependency Graph (no cycles)
Bubble Tea Model
muxd's TUI is built with Bubble Tea. The core pattern:
tui.Modelstruct: single source of truth for all UI state (messages, input buffer, streaming state, etc.).Update(msg): dispatches on message types. Keys are handled byhandleKey(), slash commands byhandleSlashCommand(), stream events by dedicated handlers.View(): pure render function, no side effects. Renders the input prompt, status footer, completion menu, and any in-progress streaming content.tui.Prog.Println(): pushes finalized content into native terminal scrollback. The activeView()area only shows the current input and in-progress streaming.
Custom message types are defined at the top of tui/model.go:
StreamDeltaMsg: a text chunk from the streaming responseStreamDoneMsg: stream completed (carries token counts, stop reason)ToolStatusMsg: tool started executing on the serverToolResultMsg: tool finished executingTurnDoneMsg: full agent turn complete (server-driven)AskUserMsg: agent's ask_user tool needs user inputPasteMsg: clipboard paste resultClipboardWriteMsg: clipboard copy result
Client/Server Architecture
muxd runs in two modes:
-
TUI mode (default): Starts an embedded HTTP server, creates a
DaemonClientto talk to it, and runs the Bubble Tea TUI. The TUI communicates with the agent loop exclusively through HTTP/SSE. -
Daemon mode (
--daemon): Starts the HTTP server headlessly on port 4096. Multiple TUI clients can connect to the same daemon via lockfile discovery.
Lockfile Discovery
When the TUI starts, it checks ~/.local/share/muxd/server.lock for an existing daemon. If found and healthy (PID alive + HTTP health check passes), it connects. Otherwise it starts an embedded server.
SSE Event Flow
The daemon streams agent events to clients via Server-Sent Events:
Agent Loop
The agent.Service (in internal/agent/) handles multi-turn tool use independently of any UI:
Key details:
- Tools are executed in parallel by default, sequentially when
ask_user,plan_enter,plan_exit, ortaskis present. - The agent loop is capped at 60 iterations to prevent runaway behavior.
- Cancellation via
Cancel()stops at the next safe point. - Checkpoints are created before each tool-use turn.
- Plan mode: When active, write tools (
file_write,file_edit,bash,patch_apply) are excluded from the tool list sent to the provider. The agent can only read and search. - Sub-agents: The
tasktool spawns a freshagent.Servicewith no store (no persistence), no git, andisSubAgent=true. Sub-agents have all tools excepttask(prevents recursion). They run synchronously and return their output. - ToolContext: Each tool execution receives a
*ToolContextproviding access to shared agent state: the working directory, the todo list, plan mode flag, and the sub-agent spawn callback. - Tool profiles: Tools can be grouped into profiles (
safe,coder,research) that enable/disable subsets of tools. Thetools.disabledconfig key can also disable individual tools. - Rate limiting: The agent loop enforces rate limits per provider to avoid API throttling.
Streaming
muxd supports streaming across all providers (Anthropic, OpenAI, Mistral, Grok/xAI, ZAI, Fireworks, Ollama):
Anthropic SSE Streaming
- POST request with
stream: trueto/v1/messages. - Line-by-line parsing with
bufio.Scanner. - Events:
content_block_start,content_block_delta,content_block_stop,message_delta,message_stop. - Text deltas emitted via
EventDeltacallback. - Tool use blocks accumulate JSON incrementally, parsed on
content_block_stop.
OpenAI SSE Streaming
- POST request with
stream: trueto/v1/chat/completions. - Same line-by-line SSE parsing.
- Handles
tool_callsin delta format, accumulating JSON arguments.
Progressive Flush (TUI)
During streaming, long responses are progressively flushed to terminal scrollback:
- Text deltas accumulate in
streamBuf. flushStreamContent()checks for safe flush points (paragraph boundaries).FindSafeFlushPoint()verifies the point isn't inside a code fence.- Safe content is rendered and pushed to scrollback via
tui.Prog.Println().
Session Persistence
SQLite Schema
sessions table:
id(UUID),project_path,title,modeltotal_tokens,input_tokens,output_tokens,message_countcreated_at,updated_at
messages table:
id(UUID),session_id(FK),role,content,content_typetokens,created_at,sequence
content_type is either text (plain string) or blocks (JSON array of content blocks, used for tool_use/tool_result messages).
The database uses WAL mode for concurrent read performance and has foreign keys enabled. Schema migrations run on startup with IF NOT EXISTS guards and ALTER TABLE ADD COLUMN with ignored errors for forward compatibility.
Auto-titling
After the first assistant response, the session title is set to the first user message, truncated to 50 characters.
Context Compaction
When input token count exceeds 100,000 tokens, the conversation is compacted:
- Keep the first user + assistant exchange (the "head").
- Keep the last 20 messages (the "tail"), starting on a user message to maintain proper alternation.
- Serialize the dropped middle messages and call the LLM to generate a structured summary (topics, files modified, tools used, key decisions, current task state).
- Replace the placeholder notice with the real summary. On LLM error, falls back to
"[N earlier messages were compacted. No summary available.]".
The summary is persisted via SaveCompaction so resumed sessions restore the LLM-generated context. The compactIfNeeded method handles this flow and runs automatically before each API call in the agent loop.
OS Service Management
The internal/service/ package supports installing muxd as a system service:
| Platform | Mechanism |
|---|---|
| macOS | launchd plist in ~/Library/LaunchAgents/ |
| Linux | systemd user unit in ~/.config/systemd/user/ |
| Windows | Registry run key in HKCU\...\Run |
Commands: muxd -service install|uninstall|status|start|stop|install-hub|uninstall-hub|start-hub|stop-hub|status-hub
Hub Architecture
The hub subsystem enables multi-node coordination. A hub coordinator manages multiple muxd daemon instances (nodes) across machines.
Components
- Hub coordinator (
--hub): Central HTTP server on port 4097 that tracks registered nodes, proxies requests, and aggregates logs. - Node registry: Nodes register via
POST /api/hub/nodes/registerwith name, host, port, and token. The hub monitors health with 30-second heartbeats and marks nodes offline after 90 seconds of inactivity. - Request proxy: Routes like
/api/hub/proxy/{nodeID}/{path...}forward requests to the target node, swapping the hub auth token for the node's daemon token. - Log broker: Nodes push logs to
POST /api/hub/logs. The hub stores them in a separatehub.dbdatabase and streams them to SSE subscribers atGET /api/hub/logs/stream. - Session aggregation:
GET /api/hub/sessionsqueries all online nodes and returns sessions grouped by node for cross-node discovery.
Database
The hub uses a separate SQLite database (hub.db) with two tables:
nodes— node registry (id, name, host, port, token, version, status, timestamps)hub_logs— centralized log history (node_id, level, message, timestamp)
Authentication
Bearer token auth (32 random bytes, hex-encoded). The token is auto-generated on first start and saved to preferences. All protected routes validate the token via middleware.