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

flowchart LR subgraph clients[Clients] cli[pd CLI] sdk[SDK / lib/client.ts] mcp[MCP server] fb[FleetBar.app] dash[Dashboard browser] end subgraph daemon[Port Daddy daemon · localhost:9876] fast[Fastify HTTP API] ipc[Binary IPC
~/.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

sequenceDiagram autonumber participant A as Agent (Claude Code) participant D as PD daemon participant S as SQLite participant B as Other agents A->>D: pd whoami D->>S: SELECT current session for PID D-->>A: (none) or session_id A->>D: pd sitrep --since 60 D-->>A: activity + notes + salvage + spawned A->>D: pd salvage --project myapp alt dead agents present A->>D: pd salvage claim <dead-id> D-->>A: inherits session, notes, file claims else fresh start A->>D: pd begin --identity myapp:api:feat-x --purpose "..." D->>S: INSERT session, agent D-->>A: session_id, agent_id end A->>D: pd session files claim src/auth/jwt.ts D->>S: INSERT file_claim D-->>B: dashboard SSE event loop every 5 minutes A->>D: heartbeat (IPC fast path) end A->>D: pd note --type progress "..." D->>S: INSERT (encrypted) note A->>D: pd note --type handoff "..." A->>D: pd done D->>S: UPDATE session status D-->>A: bye

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.

PrimitiveDurabilityCardinalityUse whenDon't use when
Sessionmutable, completes1 per agentYou are doing work that should be visible/salvageable.Single one-off CLI invocation.
Noteappend-only forevermany per sessionYou decided / shipped / found / blocked something.Scratchpad. Use a different file.
File claimsession-boundedmany per fileYou're about to edit a file (advisory warning).You need guaranteed exclusion. Use a lock.
LockTTL-bounded1 holderCritical section: deploy, migration, cache rebuild.Anything you'd do twice. Locks are for exactly-once.
Pub/subephemeralfan-out"Database is ready" → many watchers run.You need durable history. Use notes.
Tuple (Linda)TTL or until takenrd = fan-out, in = exactly-onceTyped shared memory, work-stealing queues.Ordering matters. Tuples are unordered.
Pheromonegeometric decayscalar 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.

flowchart TD hook([git post-commit hook]) -->|publishes| ch1((git:committed)) ch1 --> qa[QA agent
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.

The footgun. 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.

stateDiagram-v2 [*] --> alive: pd begin alive --> alive: heartbeat (5m) alive --> stale: no heartbeat (10m) stale --> alive: heartbeat returns stale --> dead: still silent (20m) dead --> enqueued: reaper sweep enqueued --> claimed: pd salvage claim claimed --> completed: pd salvage complete claimed --> enqueued: pd salvage abandon completed --> [*]

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:

  1. 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.
  2. Daemon is the only writer. Race conditions live inside SQLite transactions, not in agent code. Agents see consistent reads.
  3. 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

LayerWhat it ownsSource
HTTP routesValidation, request shape, JSON serializationroutes/*.ts
IPC routingFrame parsing, FIPA performatives, subscription replaylib/ipc-*.ts
Domain modulesAtomic primitives, SQLite transactionslib/sessions.ts, lib/tuples.ts, lib/locks.ts, …
Identity / trieO(k) lookup; pattern matchinglib/identity.ts, lib/trie.ts
Fleet engineYAML parsing, trigger dispatch, spawn quotas, respawnlib/fleet-engine.ts, lib/fleet-daemon.ts
ArbiterInvariant enforcement (PID squat, heartbeat freshness, note monotonicity)lib/arbiter.ts
SpawnerSubprocess management for fleet agentslib/spawner.ts
Cost & countersPer-spawn cost recording, ODS metrics, golden signalslib/cost-tracker.ts, lib/counters.ts

9. What Port Daddy is NOT

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.