-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture Overview
Dispatch's architecture flows from one principle: the agents are somebody else's processes. Dispatch never spawns them, never sees their transcripts, and holds no credential of theirs. It hosts one localhost MCP server, gives each linked project an identity on it, and routes questions between the projects a human has linked. See Technical Specification for layer detail, and How Dispatch Works for a narrative walkthrough of the bus itself.
┌────────────────────────────────────────────────────────────┐
│ SwiftUI Views (design system) │
│ ↓ (Sendable DTOs only — no DB types) │
│ @Observable @MainActor Stores │
│ • AppStores → ProjectStore, MessageStore, │
│ ActivityStore, CrossProjectStore │
│ ↓ (protocol seams: PersistenceReading, GitStatusProviding,│
│ BusArbitrating, SoundPlaying) │
│ Actors & Services │
│ ├─ MCPBusListener (SwiftNIO, 127.0.0.1:<kernel port>) │
│ ├─ DispatchEndpoint (one MCP Server per project identity) │
│ ├─ DispatchRouter (ask/answer/inbox/expiry — the brain) │
│ ├─ RepoMCPInstaller (owns the repo's "dispatch" key) │
│ ├─ GitClient (shells to the git executable) │
│ └─ GlobalDatabase / project DBs (GRDB actors) │
│ ↓ │
│ Outside the app │
│ ├─ YOUR claude sessions, one per repo, started by you │
│ ├─ each repo's .mcp.json (one key: "dispatch") │
│ └─ ~/Library/Application Support/Dispatch/*.sqlite │
└────────────────────────────────────────────────────────────┘
Note what is absent: no subprocess runtime, no worktrees, no chat transcript engine, no commit gate, no QA pipeline, no accounts.
See How Dispatch Works for the full walkthrough; the summary:
-
Listener (
Dispatch/Services/MCPBus/MCPBusListener.swift) — one SwiftNIO HTTP server for the whole app, bound to127.0.0.1on a kernel-assigned port (it re-binds the persisted port on later launches, because every.mcp.jsonon disk already points at it). It routesPOST /bus/<token>to a project. The token is the route; there is no header to trust and no identity to assert. -
Endpoint (
DispatchEndpoint.swift) — one MCPServerper project, serving the four tools, with handlers closed over that project's fixed identity. Project A physically cannot reach B's endpoint. -
Router (
DispatchRouter.swift) — the brain: ask, answer, inbox assembly, long-poll waiters, redelivery, expiry, exactly-once outcome reporting, and the fail-closed link check. -
Framing (
BusProtocolText.swift,BusEventText.swift,BusTextSanitizer) — every value from another repo is folded before it lands on a framing line, and bodies ride inside fences as DATA, never instructions.
One durable 128-bit token per project, minted at link time, stored in the global DB (projectBusToken) and embedded in the URL Dispatch writes into that repo's .mcp.json. Identity is structural: whoever can reach the route is that project, and nothing else can be spoofed. Rotation revokes the listener route and rewrites the entry.
A projectLink row — created by the human, never by an agent — is the only thing that makes one project reachable from another. list_projects reports exactly the reachable set; ask_agent to anything else is refused. Absent a link, an ask fails closed.
-
Connected +
wait_seconds→ the ask long-polls and returns the answer inline. -
Otherwise →
{status:"pending", question_id}; the question waits in the target's inbox until it callscheck_messages. - Outcomes (answer, expiry) are reported to the asker exactly once, so a session that reads one must record what matters.
- Nothing wakes a session that isn't running. Durability is the design: rows outlive processes.
RepoMCPInstaller.swift owns the only file Dispatch writes in a user repo, and it is deliberately conservative:
-
Value-faithful merge — adds or replaces exactly one key,
dispatch. Foreign servers and unknown top-level keys survive verbatim (BOM stripped, formatting re-serialized). -
We only delete what we created — uninstall removes the
dispatchkey and nothing else. -
Name-collision guard — an entry named
dispatchwhose URL does not have the shape Dispatch writes (http,127.0.0.1, a/bus/route — deliberately not the token's spelling) is aconflict, reported to the human, never clobbered. -
States:
installed/stale(right shape, wrong port or token) /conflict/ absent.
The actor-per-installer exists so a token rotation and a repo re-scan cannot interleave on one file.
All UI state lives in @Observable @MainActor stores under Dispatch/Stores/, composed by AppStores:
- ProjectStore — linked projects, git status, install state.
- MessageStore — the questions and answers the Messages tab renders.
- CrossProjectStore — links and the per-project request mirror.
- ActivityStore — the operation timeline behind the ticker.
AppStores.live() is async and runs off the synchronous App init: it bootstraps the databases, builds the router before the writers (destructive delete and unlink must close in-flight questions through the router — closing them in SQL alone would strand a long-polling caller), then wires the listener, notifications and sounds. AppStores.mock() swaps the whole lot for scripted fakes, which is what --mock-scenario runs on.
-
Global DB —
~/Library/Application Support/Dispatch/global.sqlite:project,projectLink,projectBusToken,busMessage. - Per-project DBs — also under Application Support, never anywhere inside the user's repo.
-
Migrations —
DatabaseMigratorfrom day one; the locked-list test compares the ordered migrations array, because a Set cannot detect reordering. -
Reads — through the logged
safeReadhelpers inPersistenceReading.swift; never a baretry?on a read or write. -
DTO boundary — GRDB record types never cross into views; only the Sendable domain DTOs in
Dispatch/Models/DomainModels.swiftdo.
Read-only, and only for what the project cards show. GitClient (an actor) shells to the git executable with --porcelain=v2, parsed by GitPorcelainParser, behind the GitStatusProviding protocol so tests inject a fake. RepoBookmark holds the security-scoped bookmark for a user-selected folder; PathSafety guards path handling. Dispatch never commits, never branches, never writes a worktree.
ContentView → WorkbenchView, with:
- ProjectsRail — project cards, bus health footer, resize handle.
-
Bus Map (
Dispatch/Views/BusMap/) — the switchboard, drawn: projects as stations,projectLinkrows as lines, a traveling dot for each question or answer that crosses one. -
Messages — the question/answer inbox (
QuestionCardView), plus the human's own answer path viaBusArbitrating. - Modals — link/edit project (including linking two projects to each other), delete project.
- Settings — General, Theme, Notifications.
- Onboarding — the first-run surface, gated on an empty registry.
Design tokens live in Dispatch/DesignSystem/ (Theme, Palette, Metrics, Motion, Shadows, TypeScale). Outer chrome #181A21 and canvas #E7E8EC are locked constants; only the accent is user-tunable.
-
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor— every unannotated top-level declaration is implicitly@MainActor. - Stores are
@Observable @MainActor; services (listener, router seams, installer, GitClient, databases) are actors and never@MainActor. - A protocol an actor conforms to must be declared
nonisolated protocol— it bites hardest in test fakes. - File-scope
private nonisolated let logger = Logger(subsystem: "com.wizemann.dispatch", category: …). - Long-polls must never occupy the main actor.
Result: data flows one way (DB → store → view), actor boundaries are explicit, and the UI stays responsive while an ask waits.
Last updated: 2026-07-30.
Getting Started
Architecture
- Persistence: Database Schema and Records
- Services and Actors
Reference
- Getting Started
- How Dispatch Works
- Linking Projects
- The Four Verbs
- Architecture Overview
- Product Specification
- Technical Specification
- Wiki Maintenance
- Testing and Mocks
Runbooks
- Runbook: Project Management