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, TelegramConfig
storeSQLite persistenceStore, OpenStore
providerLLM provider abstractionProvider, AnthropicProvider, OpenAIProvider, ResolveModel, ModelCost
tools18 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
telegramTelegram bot adapterNewAdapter, Run, MarkdownToTelegramHTML

Dependency Graph (no cycles)

domain          ← leaf, no internal imports
  ↑
config          ← imports domain
  ↑
store           ← imports domain, config
  ↑
provider        ← imports domain
  ↑
tools           ← imports domain, config, provider
  ↑
checkpoint      ← imports domain (just git operations)
  ↑
agent           ← imports domain, provider, tools, checkpoint
  ↑
daemon          ← imports domain, store, agent, config, provider
  ↑
service         ← imports config, daemon
  ↑
tui             ← imports domain, store, config, provider, daemon, checkpoint
  ↑
telegram        ← imports domain, store, config, provider, agent, checkpoint
  ↑
main            ← imports all

Bubble Tea Model

muxd's TUI is built with Bubble Tea. The core pattern:

Custom message types are defined at the top of tui/model.go:

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:

TUI → POST /api/sessions/{id}/submit → daemon
daemon → agent.Service.Submit() → runs agent loop
agent loop → emits agent.Event callbacks → daemon converts to SSE
SSE events → DaemonClient.Submit() parses → sends tea.Msg to TUI

Agent Loop

The agent.Service (in internal/agent/) handles multi-turn tool use independently of any UI:

Submit(userText)
    │
    ▼
  Persist user message
    │
    ▼
  Compact if needed (>150k tokens)
    │
    ▼
  provider.StreamMessage()    Stream API call
    │
    ▼
  Emit EventDelta callbacks   Text chunks
    │
    ▼
  EventStreamDone             stop_reason?
    │
   ┌┴──────┐
   │       │
end_turn  tool_use
   │       │
   ▼       ▼
 Done    Create checkpoint (if git available)
              │
              ▼
         Execute tools (parallel, or sequential for ask_user/plan/task)
              │
              ▼
         Emit EventToolStart / EventToolDone
              │
              ▼
         Persist tool results
              │
              ▼
         Loop back (max 25 iterations)

Key details:

Streaming

muxd supports both Anthropic and OpenAI streaming:

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:

messages table:

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 150,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