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.

PackagePurposeKey exports
main.goEntry point, flag parsing, wiring
domainShared types (zero internal deps)ContentBlock, TranscriptMessage, Session, NewUUID, CommandDef
configConfiguration and preferencesConfigDir, DataDir, LoadAPIKey, Preferences, LoadPricing
storeSQLite persistenceStore, OpenStore
providerLLM provider abstraction (Anthropic, OpenAI, Mistral, Grok/xAI, ZAI, Fireworks, Ollama)Provider, AnthropicProvider, OpenAIProvider, MistralProvider, GrokProvider, ZAIProvider, FireworksProvider, OllamaProvider, ResolveModel, ModelCost
tools37 built-in agent toolsToolDef, ToolContext, AllTools
agentAgent loop (adapter-independent)Service, Event, CompactMessages
checkpointGit undo/redoDetectGitRepo, StashCreate
daemonHTTP server, client, lockfileServer, DaemonClient, WriteLockfile, ReadLockfile
serviceOS service managementHandleCommand (install/uninstall/start/stop)
tuiBubble Tea TUIModel, InitialModel, Update, View, RenderAssistantLines
mcpMCP server management (stdio/HTTP transports)Manager, LoadConfig, ConvertTools
hubHub coordinator for multi-node managementHub, 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.Model struct: single source of truth for all UI state (messages, input buffer, streaming state, etc.).
  • Update(msg): dispatches on message types. Keys are handled by handleKey(), slash commands by handleSlashCommand(), 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 active View() 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 response
  • StreamDoneMsg: stream completed (carries token counts, stop reason)
  • ToolStatusMsg: tool started executing on the server
  • ToolResultMsg: tool finished executing
  • TurnDoneMsg: full agent turn complete (server-driven)
  • AskUserMsg: agent's ask_user tool needs user input
  • PasteMsg: clipboard paste result
  • ClipboardWriteMsg: clipboard copy result

Client/Server Architecture

muxd runs in two modes:

  1. TUI mode (default): Starts an embedded HTTP server, creates a DaemonClient to talk to it, and runs the Bubble Tea TUI. The TUI communicates with the agent loop exclusively through HTTP/SSE.

  2. 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, or task is 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 task tool spawns a fresh agent.Service with no store (no persistence), no git, and isSubAgent=true. Sub-agents have all tools except task (prevents recursion). They run synchronously and return their output.
  • ToolContext: Each tool execution receives a *ToolContext providing 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. The tools.disabled config 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

  1. POST request with stream: true to /v1/messages.
  2. Line-by-line parsing with bufio.Scanner.
  3. Events: content_block_start, content_block_delta, content_block_stop, message_delta, message_stop.
  4. Text deltas emitted via EventDelta callback.
  5. Tool use blocks accumulate JSON incrementally, parsed on content_block_stop.

OpenAI SSE Streaming

  1. POST request with stream: true to /v1/chat/completions.
  2. Same line-by-line SSE parsing.
  3. Handles tool_calls in delta format, accumulating JSON arguments.

Progressive Flush (TUI)

During streaming, long responses are progressively flushed to terminal scrollback:

  1. Text deltas accumulate in streamBuf.
  2. flushStreamContent() checks for safe flush points (paragraph boundaries).
  3. FindSafeFlushPoint() verifies the point isn't inside a code fence.
  4. Safe content is rendered and pushed to scrollback via tui.Prog.Println().

Session Persistence

SQLite Schema

sessions table:

  • id (UUID), project_path, title, model
  • total_tokens, input_tokens, output_tokens, message_count
  • created_at, updated_at

messages table:

  • id (UUID), session_id (FK), role, content, content_type
  • tokens, 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:

  1. Keep the first user + assistant exchange (the "head").
  2. Keep the last 20 messages (the "tail"), starting on a user message to maintain proper alternation.
  3. Serialize the dropped middle messages and call the LLM to generate a structured summary (topics, files modified, tools used, key decisions, current task state).
  4. 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:

PlatformMechanism
macOSlaunchd plist in ~/Library/LaunchAgents/
Linuxsystemd user unit in ~/.config/systemd/user/
WindowsRegistry 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/register with 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 separate hub.db database and streams them to SSE subscribers at GET /api/hub/logs/stream.
  • Session aggregation: GET /api/hub/sessions queries 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.