Status: Accepted
Date: 2026-07-20
Operators need an optional PostgreSQL control plane for compliance / externalized state, while keeping zero-friction SQLite as the default. Agent memory (harness-memory) already supports a postgres backend; Octop must be able to pass it through without forking harness-*.
Layer 1 — Octop control plane
users / agents / channels / cron / settings / secrets
→ SQLite file OR PostgreSQL (psycopg3 pool 1–8)
→ Setup wizard: greenfield defers open until database step; first bind
(hot rebind only if swapping while user_count == 0)
Layer 2 — Harness checkpoints
default: {workspace}/checkpoints.sqlite
when memory is postgres: reuse Memory / PostgresSaver (harness-agent)
Layer 3 — Agent memory (harness-memory)
default on SQLite control plane: {workspace}/memory.sqlite
default on PostgreSQL control plane: same DSN, schema agent_<id>
override: config_json.memory.backend = { type: "sqlite" }
or { type: "postgres", dsn } / { use_control_plane_dsn: true }
→ not merged into control-plane migrations
Workspace content files (SOUL, skills, inbound, JSONL) remain files via BackendWorkspace.
- Repos keep
?placeholders;PostgresPoolrewrites to%sat the boundary. - Parallel migrations:
NNN_*.sql(SQLite) andNNN_*.pg.sql(PostgreSQL). - Sync pool from async (same pattern as today). Optional
asyncio.to_threadhelpers are out of scope (follow-up “1b”). - Single active Octop writer; no multi-instance write promise.
- Greenfield only — no SQLite→PG data migrator.
- Backup: SQLite file snapshot vs
pg_dump -Fc; manifest carriesdatabase_driver+database_dump_format; refuse cross-engine restore. - Default dependency includes
psycopg[binary]. Live PG tests are gated by@pytest.mark.postgresql+OCTOP_TEST_DATABASE_URL. - Hot rebind during empty setup:
rebind_control_planeopens the new pool, thenAppRuntime.replace_servicesretargets singletons via each component’s publicreplace_*API (db layer does not poke runtime private fields).
| Concern | Owner | Location |
|---|---|---|
| Control-plane tables / indexes | Octop migrate | migrations/NNN_*.sql and NNN_*.pg.sql |
| Connection / session settings | Pool connect | SQLite: PRAGMA in SqlitePool; PG: defaults (FK on) — not in migrations |
Instance extensions (vector, …) |
Ops / docker | docker/postgres/init-vector.sql via initdb.d, or DBA on managed PG |
| Agent memory DDL | harness-memory | Runtime _init_schema per agent_* schema |
Hard rule: Octop control-plane *.pg.sql must not run CREATE EXTENSION. Many managed Postgres roles cannot create extensions; the control plane also does not need vector (plain types only). harness-memory today uses built-in tsvector, not pgvector. Keep CREATE EXTENSION vector in docker/ops for optional future embedding use.
Octop passes memory_backend into HarnessAgentConfig and opens dashboard Memory with the same spec. Portable pack/adopt for postgres is refused (use pg_dump on the memory schema).
DatabasePoolProtocol;open_database()returns SQLite or PG.- Setup first step must succeed before admin creation when operators choose PG.
- Operators may colocate control plane + memory on one Postgres (separate schemas) or split DSNs.
- ADR 001’s “one process” model is unchanged; PG is an optional backing store, not a second Octop instance.