Postgres 16 — the durable spine
The local fleet's canonical store is the subagentjobs-postgres container, image
postgres:16-alpine. Role gotcha, stated plainly: the documented DSN
(postgresql://subagentjobs:subagentjobs@localhost/subagentjobs) is wrong on this Mac — the
container was initialized with POSTGRES_USER=wbr (trust auth) and the subagentjobs role is
never created locally (the role-creation block in session-start.sh is gated on the remote
container). The connection that actually works, carried in both CLAUDE.md's pinned-tooling section
and the 008 migration header itself: psql -h 127.0.0.1 -U wbr -d subagentjobs.
baked 2026-07-10 from docker ps --format '{{.Names}}\t{{.Image}}' → subagentjobs-postgres · postgres:16-alpine
The estate — migrations 001→010
Spine baked 2026-07-10 from ls crates/durable-store/migrations/postgres/; row counts from
psql -h 127.0.0.1 -U wbr -d subagentjobs -c "SELECT count(*) FROM <table>" per table.
agent_dispatch_events shows its real bake-time count (2 — two routed dispatches had already
been drained by the Stop hook), not a rounded-down zero. The dashed box is honest too: the job-board
crawler tables exist in the schema but are empty on this local instance — the crawl pipeline fills
them in the Cloudflare deployment, not here.
The ledger is the state, not the transcript
Rule 3 of the fleet-expansion skill (plugins/cwc-deploy/skills/fleet-expansion/SKILL.md)
names the principle this whole estate runs on: "The ledger is the state, not the transcript.
One fact_fleet_deploy row per domain … A crashed run resumes with ledger.sh resume."
Agents coordinating a deploy wave read and write rows, not conversation context — the 38 rows in
fact_fleet_deploy are the deploy history, replayable by any future session with zero tokens
spent reconstructing what happened. agent_dispatch_events (009) applies the same principle to
model routing: every dispatch decision the hooks enqueue lands as a durable row, so routing policy
can be audited from SQL rather than from transcripts.
Full-text search is a habit here, not a feature
Five migrations ship GIN FTS indexes, each verified in the SQL (USING GIN): 002 over job
posting title+company, 004 over vendor-file symbol docs (dim_file_ast.doc_tsv, plus GIN
array indexes on symbols/imports/exports), 006 over the ecosystem catalog
(fact_repositories.fts, dim_packages.fts), 007 over crawled doc pages, and 010
over the repo file index (fact_repo_files.fts, a stored generated tsvector column). The
pattern compounds: by 010, adding a searchable corpus meant copying 007's proven
sha-CDC-plus-GIN shape, not designing one.
What lives here vs. redis
Postgres holds everything that must survive a crash or be true across sessions: the vendor file
index (004), the ecosystem catalog (006), the deploy ledger (008), dispatch telemetry (009), and
the repo content index (010). Redis holds only what these tables can regenerate —
read-through caches with TTL safety nets and per-run scratch. The one deliberate exception is the
docs-rag index: docs/docs (9,454 md files) is owned by sqlite FTS5, and consolidating it into
Postgres is an explicitly deferred decision, written into the 010 migration header — see
/indexdb for that composition story.
Companion pages: /indexdb (what 010 indexes and why) and /redis (the fast path in front of this spine).