architecture brief · v3.11 · 2026-04
Port Daddy: a coordination substrate for agents that live and die.
One local daemon. SQLite of record. Two sockets — HTTP for humans, MessagePack-binary for the agent hot path. Every primitive is small, advisory by default, and survives the agent that wrote it.
1. The whole thing in one diagram
~/.port-daddy/daemon.ipc] routes[(routes/*)] libs[(lib/*)] fleetd[Fleet daemon
auto-starts on boot] arb[Arbiter
invariant guard] end subgraph state[State of record] sqlite[(SQLite
port-registry.db)] keys[~/.port-daddy/master.key
AES-256-GCM] socks[~/.port-daddy/
daemon.sock
daemon.ipc
daemon.pid] end subgraph spawn[Spawned work] agents[fleet agents
claude-cli · ollama · custom] work[user agents
Claude Code · Cursor · CLIs] end cli & sdk & mcp & fb & dash --> fast work & agents --> ipc work & agents -.HTTP fallback.-> fast fast --> routes --> libs --> sqlite ipc --> libs libs --> arb fleetd --> agents libs --> keys daemon --- socks
Solid lines: in-band protocol. Dashed: fallback path. The daemon is the only writer to port-registry.db; everything else is a client.
2. The bones — what's load-bearing
SQLite is the truth
Every primitive — sessions, notes, locks, file claims, tuples, pheromones, salvage queue — persists to a single port-registry.db. Atomic ops are a single transaction. Restart loses nothing.
Two sockets, two latencies
HTTP for control (CLI, dashboard, ~200µs). Binary IPC over Unix socket for hot-path (heartbeats, pheromone, pub/sub, ~3µs). SDK falls back to HTTP if IPC is unavailable.
Identity is the address
project:stack:context. Same identity → same port, deterministically. Same identity → same harbor membership. Same identity → same registration namespace.
Notes are encrypted, append-only
AES-256-GCM at rest. Key in ~/.port-daddy/master.key (mode 0600). No edit, no delete — supersession by writing a new note. The audit trail is the contract.
Coordination is advisory
File claims warn, don't block. Two agents in the same file is a conversation, not a deadlock. For exclusion that must hold, use a lock.
Death is preserved
An agent that stops heartbeating becomes a salvage entry. Its session, notes, and file claims are visible to anyone with pd salvage. The next agent picks up where it died.
3. An agent's lifetime, one sequence
4. The primitive map — when each one
Six primitives cover almost all coordination. Picking the wrong one is the most expensive mistake; the table below is the decision.
| Primitive | Durability | Cardinality | Use when | Don't use when |
|---|---|---|---|---|
| Session | mutable, completes | 1 per agent | You are doing work that should be visible/salvageable. | Single one-off CLI invocation. |
| Note | append-only forever | many per session | You decided / shipped / found / blocked something. | Scratchpad. Use a different file. |
| File claim | session-bounded | many per file | You're about to edit a file (advisory warning). | You need guaranteed exclusion. Use a lock. |
| Lock | TTL-bounded | 1 holder | Critical section: deploy, migration, cache rebuild. | Anything you'd do twice. Locks are for exactly-once. |
| Pub/sub | ephemeral | fan-out | "Database is ready" → many watchers run. | You need durable history. Use notes. |
| Tuple (Linda) | TTL or until taken | rd = fan-out, in = exactly-once | Typed shared memory, work-stealing queues. | Ordering matters. Tuples are unordered. |
| Pheromone | geometric decay | scalar field | "Where is everyone?" heat maps, contention. | You need an answer. Pheromones give gradients. |
5. Fleets — coordination as a config file
A pd-fleet.yml declares background agents. The daemon watches the file, hot-reloads on change, and runs the agents under spawn quotas. Triggers fan in: cron, pub/sub, tuple-shape match. Side effects fan out: on_success: publish <channel> chains agents into pipelines.
claude-cli] ch1 --> docs[Documentarian
ollama] qa -- on_success --> ch2((qa:clean)) qa -- on_failure --> ch3((qa:findings)) ch2 --> deploy[Deploy gate] ch3 --> cart[Cartographer] cron([cron */30]) --> cart cart -- writes --> notes[(immutable notes)] qa -- writes --> notes
Channels are the typed messaging fabric. Producers publish; consumers subscribe. The topology validator walks this graph at fleet startup and warns on dead-end channels.
fleet.name in pd-fleet.yml must equal basename(projectDir). The post-commit hook publishes to project:<basename>:<hash>:git:committed; if the name drifts, every trigger: git:committed agent goes silently dormant. Validate with scripts/fleet-validate.sh.
6. Death & salvage
The least flashy feature, the most load-bearing one. An agent's heartbeat is its sign of life. Stop heartbeating for 20 minutes and you're enqueued for salvage. Your session, notes, file claims, and identity all survive. Another agent reads your handoff note and continues.
Salvage is note-passing, not state recovery. Code-state is not preserved. The dying agent's handoff note is the only contract. This is why the per-milestone progress note discipline matters — without it, the salvager is reading tea leaves.
7. The honesty contract
Three properties make Port Daddy useful instead of decorative:
- Advisory by default. Claims, notes, pheromones — none of them block another agent. They make the world legible. If you want exclusion, use a lock and own the consequences.
- Daemon is the only writer. Race conditions live inside SQLite transactions, not in agent code. Agents see consistent reads.
- Notes are forever. You cannot rewrite history. This is what lets the audit trail be load-bearing — agents can trust what they read.
8. Where the layers live
| Layer | What it owns | Source |
|---|---|---|
| HTTP routes | Validation, request shape, JSON serialization | routes/*.ts |
| IPC routing | Frame parsing, FIPA performatives, subscription replay | lib/ipc-*.ts |
| Domain modules | Atomic primitives, SQLite transactions | lib/sessions.ts, lib/tuples.ts, lib/locks.ts, … |
| Identity / trie | O(k) lookup; pattern matching | lib/identity.ts, lib/trie.ts |
| Fleet engine | YAML parsing, trigger dispatch, spawn quotas, respawn | lib/fleet-engine.ts, lib/fleet-daemon.ts |
| Arbiter | Invariant enforcement (PID squat, heartbeat freshness, note monotonicity) | lib/arbiter.ts |
| Spawner | Subprocess management for fleet agents | lib/spawner.ts |
| Cost & counters | Per-spawn cost recording, ODS metrics, golden signals | lib/cost-tracker.ts, lib/counters.ts |
9. What Port Daddy is NOT
- Not a service mesh. No mTLS, no traffic shaping, no upstream/downstream routing. Use Caddy / Envoy / Linkerd.
- Not a job queue. Tuples come close, but no retry policy, no dead-letter, no priority. Use a real queue if you need queue semantics.
- Not a deploy tool. Locks help guard a deploy; PD doesn't run one.
- Not a checkpointing system. Salvage preserves notes, not code state. Re-derive from git.
- Not multi-host. Local daemon, single machine. Mesh-mode is on the roadmap; today PD is per-developer.
10. Coda — why it works
Coordination problems in agent fleets are almost never about CAP or consensus. They are about two agents who don't know about each other. PD makes that impossible: the moment you pd begin, every other agent on the daemon can see you, your purpose, your files, your notes. Everything else — fleets, tuples, pheromones — is sugar on top of "make agents legible to each other."
The price is small: one daemon, one SQLite file, a few seconds of session-start ritual, and the discipline to write a note when you decide something. The return is that two agents in the same repo no longer destroy each other's work.