--- created: '2026-05-15' path: Sources/Homelab project: postgres-consolidation-reflection-layer status: active tags: - ob1 - mcp - pgvector - embeddings - session-note type: session-note updated: '2026-05-15' worker: 2 phase: 2a --- # Postgres Consolidation — Worker 2, Phase 2a result Light Phase 2a complete. Disruption-free: no consumer connections broken, no live writers paused, no schema rename. `openbrain` is still the live DB name; `petalbrain` rename is Worker 3's cutover. ## What landed Inside `openbrain` (the existing DB): ### New roles (no grants on `ob1` yet — that schema doesn't exist) | Role | Schema-level grants | search_path | |---------------|----------------------------------------------------------------------------|--------------------------------| | `vault_mcp` | OWNER of `wiki` | `wiki, public` | | `trellis_mcp` | OWNER of `trellis` | `trellis, public` | | `ob1_mcp` | USAGE + SELECT on `wiki` and `trellis` | `ob1, public` (ob1 not yet created) | | `lovebug` | USAGE + SELECT + INSERT/UPDATE/DELETE on `wiki` and `trellis` | `wiki, ob1, trellis, public` | All four: LOGIN, NOSUPERUSER, NOCREATEDB, NOCREATEROLE, NOREPLICATION, NOBYPASSRLS, INHERIT. Default privileges configured so future tables owned by `vault_mcp` (in `wiki`) and `trellis_mcp` (in `trellis`) inherit the same grant model automatically. Credentials at `/opt/backups/postgres-consolidation/credentials.env` (mode 0600, herbyadmin-only). 64-char hex passwords. For KeePassXC transcription. ### Schemas in `openbrain` - `wiki` — restored from Phase 1 `herbylab-pg_dump`, then `ALTER SCHEMA herbylab RENAME TO wiki`. Contains: - tables `alembic_version` (1 row), `artifacts` (0 rows), `projects` (0 rows) - sequences `artifacts_id_seq`, `projects_id_seq` (linked to IDENTITY columns) - enums `artifact_type`, `project_status`, `project_domain` - functions `artifacts_set_updated_at`, `notify_project_change` - triggers `artifacts_updated_at_bump`, `projects_notify` - FK constraint `projects.artifact_id` → `artifacts.id` - all OID references updated transparently across the rename - `trellis` — restored from Phase 1 `trellis-pg_dump` (same name in source and target, no rename). Contains: - tables `alembic_version` (1), `events` (50), `tags` (42), `thread_tags` (55), `threads` (13) - sequences `events_id_seq`, `tags_id_seq`, `threads_id_seq` (OWNED BY linked) - FK chain across `events ⇒ threads`, `thread_tags ⇒ threads`, `thread_tags ⇒ tags`, `threads.parent_id ⇒ threads` - `public` — **untouched**. `public.thoughts` still owned by `postgres`, still receiving writes from `ob1-watcher` (75 rows at end of Phase 2a, vs 74 at Phase 1 dump start, vs 70 at Phase 0 recon — `ob1-watcher` is healthy). Worker 3 moves this to `ob1`. - `public.embeddings` — **new, empty**. Shared cross-schema embedding store per locked spec: ``` public.embeddings ( id BIGSERIAL PRIMARY KEY, source_schema TEXT NOT NULL, source_table TEXT NOT NULL, source_id BIGINT NOT NULL, embedding VECTOR(768) NOT NULL, embedding_model TEXT NOT NULL, embedded_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (source_schema, source_table, source_id, embedding_model) ) ``` Indexes: - `embeddings_pkey` (id) - `embeddings_source_uniq` UNIQUE (source_schema, source_table, source_id, embedding_model) - `idx_embeddings_hnsw` HNSW (embedding vector_cosine_ops) WITH (m=16, ef_construction=64) — matches the existing `idx_thoughts_embedding_hnsw` convention exactly - `idx_embeddings_source` (source_schema, source_table, source_id) — for orphan cleanup + source-aware queries - `idx_embeddings_model` (embedding_model) — for the multi-model coexistence case the plan calls out Owned by `postgres` (neutral shared infrastructure). All four service users have SELECT+INSERT+UPDATE+DELETE on the table and SELECT+USAGE on the `embeddings_id_seq` sequence. No FK — orphan cleanup is a separate periodic job (out of scope here per plan). ## What Phase 2a did NOT do - Did not rename `openbrain` → `petalbrain`. That's a destructive cutover (live consumers must disconnect for `ALTER DATABASE RENAME`). Deferred to Worker 3. - Did not create the `ob1` schema. Worker 3 owns the `ALTER TABLE public.thoughts SET SCHEMA ob1` step + accompanying shape rewrite. - Did not touch `public.thoughts` in any way. Still in `public`, still writable, still being written to. - Did not pause `ob1-watcher` or `ob1-enricher`. Both still live. - Did not modify any MCP container connection strings or env files. All MCPs still connect to `openbrain` as before. - Did not touch the source `herbylab` or `trellis` databases. Both still exist on the instance with their original content; cleanup is Phase 8 of the plan. ## Verification done ### Row counts in openbrain match dump-source counts exactly | Schema/Table | Live count | Dump-source count | |---------------------------|-----------:|------------------:| | `wiki.projects` | 0 | 0 (herbylab) | | `wiki.artifacts` | 0 | 0 (herbylab) | | `wiki.alembic_version` | 1 | 1 (herbylab) | | `trellis.threads` | 13 | 13 | | `trellis.events` | 50 | 50 | | `trellis.tags` | 42 | 42 | | `trellis.thread_tags` | 55 | 55 | | `trellis.alembic_version` | 1 | 1 | | `public.thoughts` | 75 | (live; was 74 at dump time) | | `public.embeddings` | 0 | (new) | Source `herbylab` DB and source `trellis` DB confirmed unchanged. ### Permission boundary smoke tests (positive isolation per plan Phase 3) All ran via direct postgres connection on `postgresql://:@localhost:5432/openbrain`: | Test | Expected | Result | |--------------------------------------------------|----------|--------| | `vault_mcp` SELECT `wiki.artifacts` | succeed | OK | | `vault_mcp` INSERT `trellis.threads` | DENY | `ERROR: permission denied for schema trellis` | | `trellis_mcp` SELECT `wiki.artifacts` | DENY | `ERROR: permission denied for schema wiki` | | `ob1_mcp` SELECT `wiki.artifacts` + `trellis.threads` | succeed | OK (counts 0 + 13) | | `ob1_mcp` INSERT `wiki.artifacts` | DENY | `ERROR: permission denied for table artifacts` | | `vault_mcp` INSERT `public.embeddings` | succeed | OK (id 1) | | `ob1_mcp` INSERT `public.embeddings` | succeed | OK (id 2) | | `trellis_mcp` INSERT `public.embeddings` | succeed | OK (id 3) | | `lovebug` INSERT `public.embeddings` | succeed | OK (id 4) | Smoke-test rows (4) deleted after verification; `public.embeddings` back to 0 rows. ## Surprises / notes - **Sequence ownership followed table.** First pass on `ALTER OWNER` failed with `cannot change owner of sequence "artifacts_id_seq" — Sequence "artifacts_id_seq" is linked to table "artifacts"`. The herbylab dump uses `GENERATED ALWAYS AS IDENTITY`, whose sequence is owned by the table and cannot be reassigned independently. Fixed by excluding `relkind='S'` from the table loop; sequence ownership now follows the table automatically. Trellis sequences use SERIAL/OWNED BY which has the same behavior. Caught + handled in one retry; final state correct. - **`ob1_mcp.search_path = ob1, public`** is set now even though the `ob1` schema doesn't exist yet. Once Worker 3 creates the schema in the same DB (whatever its final name), unqualified table refs from `ob1-mcp`'s connection will resolve there automatically. - **No `vault-mcp` env update** was needed for Phase 2a. The current `HERBYLAB_DATABASE_URL` in `vault-mcp` points at hostname `vault-postgres` (still anomalous — part of Travis's rename rollout) and DB `herbylab`. Neither is wired into the consolidated state yet. Phase 3 of the original plan handles MCP connection cutover. ## Worker 3 readiness assessment Worker 3's cutover window is the next destructive step. From the priming, Worker 3 must: 1. Pause `ob1-watcher` + `ob1-enricher` + `ob1-mcp`. 2. Take Phase 2-start fresh `pg_dump` of `openbrain`, `herbylab`, `trellis` to `/opt/backups/postgres-consolidation/` with suffix `-phase2-start-`. Verify via test restore. 3. `ALTER DATABASE openbrain RENAME TO petalbrain` (now safe — no live connections). 4. Connect to `petalbrain` and `ALTER TABLE public.thoughts SET SCHEMA ob1`, plus all dependent objects (indexes are intra-table so they follow; the `match_thoughts` function references `thoughts` directly without schema qualification so search_path must include `ob1` when ob1_mcp calls it — already set; the `thoughts_updated_at` trigger follows the table; `update_updated_at()` function lives in `public` and should be moved or be referenced cross-schema). 5. Create `ob1` schema if `SET SCHEMA` didn't auto-create it (`SET SCHEMA` does create the schema if it didn't exist — but actually it requires the schema to pre-exist; Worker 3 should `CREATE SCHEMA ob1 AUTHORIZATION ob1_mcp` first). 6. ob1-specific grants: `ob1_mcp` owns `ob1.*`; `lovebug` gets full RW; `vault_mcp` and `trellis_mcp` get nothing. 7. Backfill embeddings: for each of the 75-ish rows in `ob1.thoughts`, insert into `public.embeddings` with `source_schema='ob1'`, `source_table='thoughts'`, `source_id=`, `embedding=`, `embedding_model='nomic-embed-text'`. 8. Decide: keep the `ob1.thoughts.embedding` column post-backfill (redundant with `public.embeddings`) or drop it. Plan says all embeddings live in `public.embeddings`; suggests dropping. But that changes the row shape for `ob1-mcp`'s code. Worker 3 call. 9. Rewrite `ob1-mcp`'s `add_thought` and `search_thoughts` for the new shape (target table `ob1.thoughts`, embedding pushed to `public.embeddings` separately, search joins). 10. Update connection strings on `ob1-mcp`, `ob1-watcher`, `ob1-enricher` from DB `openbrain` to DB `petalbrain`. Watcher talks to ob1-mcp HTTP only, so its env may not need DB host change — verify. 11. Update `trellis-mcp` and `herbydev-mcp` / `vault-mcp` connection strings from their current DBs (`trellis`, `herbylab`) to the consolidated `petalbrain` with their service-user credentials from `credentials.env`. 12. Resume `ob1-watcher` + `ob1-enricher`. 13. Verify: row counts, semantic search query roundtrip, NOTIFY trigger on `wiki.projects` still emits. Phase 2a outputs Worker 3 will need: - Service user passwords in `/opt/backups/postgres-consolidation/credentials.env`. - Phase 1 backup set in `/opt/backups/postgres-consolidation/` plus the rollback runbook there. - This session note. ## Holding pattern `ob1-watcher` and `ob1-enricher` continue writing to `public.thoughts`. Each minute that passes, the gap between the Phase 1 dump's `thoughts` snapshot (74) and the cutover-time count grows. Phase 2-start fresh dump shrinks that gap to seconds when Worker 3 runs. Until then: no degradation, no risk; just normal capture. Worker 2 standing by. No further actions until Worker 3 spawns or Travis redirects.