Architecture

This document covers muxd's internal architecture for contributors.

Project Structure

muxd uses internal/ sub-packages organized by domain. Entry points live in cmd/ directories: all business logic is in packages.

PackagePurposeKey exports
cmd/muxdTUI client entry point
cmd/muxd-daemonDaemon entry point
cmd/muxd-hubHub entry point
internal/domainShared types (zero internal deps)ContentBlock, TranscriptMessage, Session, NewUUID, CommandDef
internal/configConfiguration and preferencesConfigDir, DataDir, LoadAPIKey, Preferences, LoadPricing
internal/storeSQLite persistenceStore, OpenStore
internal/providerLLM provider abstraction (Anthropic, OpenAI, Mistral, Grok/xAI, ZAI, Fireworks, Ollama)Provider, AnthropicProvider, OpenAIProvider, MistralProvider, GrokProvider, ZAIProvider, FireworksProvider, OllamaProvider, ResolveModel, ModelCost
internal/tools37 built-in agent toolsToolDef, ToolContext, AllTools
internal/agentAgent loop (adapter-independent)Service, Event, CompactMessages
internal/checkpointGit undo/redoDetectGitRepo, StashCreate
internal/daemonHTTP server, client, lockfileServer, DaemonClient, WriteLockfile, ReadLockfile
internal/serviceOS service managementHandleCommand (install/uninstall/start/stop)
internal/tuiBubble Tea TUIModel, InitialModel, Update, View, RenderAssistantLines
internal/mcpMCP server management (stdio/HTTP transports)Manager, LoadConfig, ConvertTools
internal/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

Three Binary Architecture

muxd ships as three separate binaries:

  1. muxd (TUI client): Connects to a local daemon or remote hub. Pure client — no agent logic, no HTTP server. Flags: --version, --remote host:port, --token string.

  2. muxd-daemon (agent server): Headless HTTP server on port 4096 that runs the agent loop, executes tools, and talks to LLM APIs. Configured via environment variables or config.json.

  3. muxd-hub (hub coordinator): Central HTTP server on port 4097 that manages node registration, request proxying, log aggregation, and serves the embedded web dashboard.

All three binaries share one ~/.config/muxd/config.json. Each reads only the fields it needs.

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 prints an error asking the user to start muxd-daemon.

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-daemon -service install|uninstall|status|start|stop and muxd-hub -service install|uninstall|status|start|stop|qr

Hub Architecture

The hub subsystem enables multi-node coordination. A hub coordinator manages multiple muxd daemon instances (nodes) across machines.

Components

  • Hub coordinator (muxd-hub): Central HTTP server on port 4097 that tracks registered nodes, proxies requests, aggregates logs, and serves the web dashboard.
  • 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.