- type: session-note -> session-notes (4 files: 2 vault-integration-test, 2 postgres-consolidation phases) - [[Sources/Homelab/ob1-jsonl-watcher]] -> actual filename (5 links across ob1, ai-memory synthesis, Homelab index) - [[Sources/Dev/env-file-hardening]] -> Sources/Homelab/env-file-hardening (gitea entity) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
253 lines
12 KiB
Markdown
253 lines
12 KiB
Markdown
---
|
|
created: '2026-05-15'
|
|
path: Sources/Homelab
|
|
project: postgres-consolidation-reflection-layer
|
|
status: active
|
|
tags:
|
|
- ob1
|
|
- mcp
|
|
- pgvector
|
|
- embeddings
|
|
- session-note
|
|
type: session-notes
|
|
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://<role>:<pw>@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-<UTC-timestamp>`. 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=<row
|
|
id>`, `embedding=<existing embedding column value>`,
|
|
`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.
|