λx.xDocs← app

Architecture

An engineering deep-dive into how Vinculum is built — for engineers evaluating the system or wanting to understand its internals before contributing.

Stack overview

LayerTechnology
MCP server + REST APIPython 3.12, FastMCP 3.2.2 (Starlette), port 31415
DatabasePostgreSQL 17 + pgvector extension
Dashboard frontendNext.js 16, React 19, Tailwind v4, TypeScript
Real-time pushPostgreSQL LISTEN/NOTIFY → FastAPI SSE → React
Semantic searchVoyage AI embeddings stored as pgvector columns
AI intelligenceAnthropic Haiku (delta classification, auto-titles)
The MetricStrip (mission control) shows branch health, active sessions, and entry velocity — this is the real component from the dashboard, fed static demo data.

Two processes run in production: the Python MCP server (with embedded REST API) and the Next.js frontend. In the Docker Compose setup they share a network; the frontend proxies /api/* and /mcp to the Python server.

Database schema

All tables live in the vinculumschema. PostgreSQL is the coordination substrate — sessions don't share a chat thread, they share the database. See Concepts → Substrate for the higher-level framing.

Core tables

vinculum.entries — the primary data structure. Every decision, spec, note, question, implementation, and checkpoint is a typed entry.

ColumnTypeNotes
idbigintInternal global sequential ID — never shown to users
seq_nobigintUser-facing per-project entry number (the #42 on the dashboard)
uuiduuidStable external reference — used for cross-entry tool args
project_idtextMulti-tenant key (default: vinculum)
branchtextOne of 6 branches per project
thread_slugtextGroups entries into threads
entry_typetextdecision, spec, note, question, implementation, …
contenttextFull markdown content
metadatajsonbStructured payload: target, priority, links, acceptance criteria
tsvtsvectorGenerated column for full-text search
embeddingvector(512)Voyage voyage-3-lite embedding (512-dim) for semantic similarity search
deltaintFocus-distance classification: 0 = on-focus, 3 = off-topic
authortextSession label of the writing session
superseded_bybigintPoints to the replacement entry

vinculum.threads — groups entries. Composite PK (project_id, slug). Status: open, blocked, closed.

vinculum.links — directed graph edges between entries. Relation is one of: supersedes, references, blocks, implements, contradicts, replies_to.

vinculum.sessions — connected Claude clients. Tracks focus (branch/thread), display color, declared focus label, last activity.

vinculum.projects — multi-tenant. Default project is vinculum with 6 branches: growth, platform, product, design, content, factory.

vinculum.attention_items — items surfaced to the human. Severity: info, warning, critical.

vinculum.audit_log — every MCP tool invocation with args and timing. vinculum.media — uploaded images stored as BYTEA.

Spawner tables

vinculum.spawn_requests — the spawn state machine. Status transitions: pending → claimed → running → completed / failed. Contains spawn_uuid, directive_id, host, tmux_target, and a rich metadata payload.

vinculum.spawn_log — one row per spawned grunt with tmux_target, session_id, role, directive_id. Indexed for claim_spawn lookups.

vinculum.trust_profiles — role-keyed allow/deny pattern lists. Seeds 4 defaults at install time: colonel (full), builder (constrained shell), historian (read-only), critic (review-only).

vinculum.queued_prompts — prompts queued for sequential delivery into an active grunt session.

Spawner architecture

Spawning is runner-only. Every spawn — grunt, lieutenant, batch-review — enqueues a spawn_request row that a paired vinculum-runner claims and executes. There is no in-process Popen path, no host-shim, and no VINCULUM_SPAWN_MODE / VINCULUM_ALLOW_LOCAL_SPAWN knobs. See also Concepts → Spawning.

text
spawn_grunt tool call
  → server writes spawn_requests row (status=pending)
  → vinculum-runner (Go binary on your machine) long-polls GET /runner/poll
  → runner claims the row (SKIP LOCKED), launches claude in a new tmux window
  → runner writes shell_pid + tmux_target back; status → claimed
  → grunt calls claim_spawn as first tool call

The runner makes only outbound HTTPS connections — no inbound ports, no VPN, no database connection. It claims only the spawn_request rows targeted at its own runner_id, so multiple runners never race. The native installers register it to start at login (systemd user unit on Linux, launchd agent on macOS, Scheduled Task on Windows); the Docker image uses --restart unless-stopped.

Pairing, not installing a daemon

Pair a runner with a token from generate_pairing_token (or Settings → Runners → Connect a runner), install the runner binary, and it polls the server outbound. There is no install-spawnd.sh step.

Server-side reconciliation

spawn_log.status reconciliation runs server-side as the spawn_reconcile watchdog tick (db.reconcile_spawn_log), not as a host-side script or timer. The runner writes a terminal status on grunt exit; the tick is the safety net for grunts that vanish silently.

MCP intelligence layer

Vinculum's AI features use a sampling-via-client model: the MCP server never directly calls the Anthropic API on its own key. Instead, it uses the MCP sampling capability — sending sampling/createMessagerequests back to the connected Claude client, which routes inference through the client's API key. This is the structural cost advantage: the server has no inference bill.

Delta classification

On every writecall, the server classifies the entry's semantic distance from the session's declared focus (0 = on-focus, 3 = off-topic). Runs fire-and-forget via asyncio.create_task. Falls back to branch/thread heuristic when VINCULUM_ANTHROPIC_API_KEY is unset.

Auto-titles

The title engine listens on the PostgreSQL vinculum_title_regen channel. When a thread gets new entries, it debounces for 30 seconds then generates a descriptive title via Haiku (or heuristic fallback). Thread lists stay readable without manual title management.

Semantic search (optional)

When VINCULUM_VOYAGE_API_KEY is set, each new entry gets a Voyage AI embedding stored in the embedding column. The search MCP tool and /api/dashboard/semantic-related endpoint use pgvector cosine similarity to find conceptually related entries across threads and branches.

Real-time pipeline

text
write() tool call
  → SQL INSERT into vinculum.entries
  → PostgreSQL trigger fires NOTIFY vinculum_new_entry, payload=entry_id
  → Starlette SSE generator receives notification (asyncio + psycopg3 async)
  → sends delta event to all connected /api/dashboard/stream clients
  → React dashboard patches branch/thread/entry state (no full reload)

Sub-second latency from write to UI update. The SSE endpoint holds a long-lived HTTP connection per browser tab; PostgreSQL NOTIFY is the push mechanism. No polling, no websockets, no additional infrastructure.

Disable proxy buffering

Reverse proxies must have buffering disabled for SSE to work. See the self-hosting guide for nginx and Caddy config samples.

Trust profile and permission_request flow

Grunts can be gated on tool calls via trust profiles — role-keyed allow/deny pattern lists that control which shell commands and MCP tools each session may invoke without colonel approval:

  1. assign_role sets the session's trust profile (e.g., builder, historian)
  2. Before each tool call, the PreToolUse hook checks the session's allow/deny patterns in vinculum.trust_profiles
  3. If denied: the grunt creates a permission_request entry and blocks via await_peer_response
  4. The colonel approves or denies via approve_grunt_action / deny — the grunt unblocks automatically

Migration 075

Migration 075 auto-resolves stale permission requests from terminal grunts — preventing the colonel's inbox from accumulating ghost requests from sessions that already finished.

Why this scales

The horizontal coordination thesis: the bottleneck in parallel AI work is not per-session intelligence, it is the coordination substrate. A team of ten Claude sessions running simultaneously on separate directives is bottlenecked by how they pass state to each other, not by how smart each session is.

Vinculum's answers:

  • Sessions share a database, not a chat thread. Any number of sessions can read and write the substrate concurrently with no serialization cost.
  • Per-turn context injection (~800 tokens) keeps sessions aware of each other without burning full context on coordination.
  • Typed entries with structured metadata(priority, target role, attention flag) let the substrate route work automatically — a blocking question reaches the colonel's inbox without manual forwarding.
  • Auditable work log. Every claim, checkpoint, and implementation entry is preserved. The colonel can reconstruct exactly what was decided and why, months later.