diff --git a/Sources/Homelab/2026-05-15-postgres-consolidation-phase0-recon.md b/Sources/Homelab/2026-05-15-postgres-consolidation-phase0-recon.md new file mode 100644 index 0000000..6febb1e --- /dev/null +++ b/Sources/Homelab/2026-05-15-postgres-consolidation-phase0-recon.md @@ -0,0 +1,298 @@ +--- +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 +--- + +# Postgres Consolidation — Worker 2, Phase 0 recon + +Snapshot of the homelab Postgres instance before any consolidation +work. Recorded from `homelab-postgres` (pgvector/pgvector:pg17), +container in `/opt/projects/docker/postgres/`, port `127.0.0.1:5433`, +volume `ob1_data` (external). + +## Postgres instance + +- Container: `homelab-postgres` (image `pgvector/pgvector:pg17`) +- Volume: `ob1_data` (external — survives stack down) +- Network: `homelab` (alias `homelab-postgres`, also `postgres`) +- Healthy, up 40 hours. +- POSTGRES_DB default = `openbrain`. + +## Databases on instance + +| Name | Owner | Size | pgvector | Notes | +|-------------|-----------------|---------|----------|-------| +| `herbylab` | `herbylab_user` | 7958 kB | NO | Holds the herby-dev artifact store | +| `openbrain` | `postgres` | 8838 kB | YES (0.8.2) | OB1 captures live here | +| `trellis` | `trellis_user` | 8262 kB | NO | Trellis MCP threads/events | +| `postgres` | `postgres` | 7478 kB | - | default; not touched | + +No other user databases. Plan's "instance hosts only wiki, ob1, trellis" +holds — under the actual names `herbylab` / `openbrain` / `trellis`. + +## Existing roles + +- `postgres` — Superuser, Create role, Create DB, Replication, Bypass RLS +- `herbylab_user` — owns `herbylab.herbylab` schema +- `trellis_user` — owns `trellis.trellis` schema + +No `lovebug` role exists. No `ob1_mcp` / `vault_mcp` / `trellis_mcp` +service users yet — these come in Phase 2. + +## Schemas, tables, row counts + +### `herbylab` DB +- Schemas: `herbylab` (owner `herbylab_user`), `public`. +- Tables in `herbylab`: + - `projects` — 0 rows (8192 B; one empty heap page) + - `artifacts` — 0 rows (8192 B; one empty heap page) + - `alembic_version` — 1 row (`9c7e3f1a2b4d`) +- Custom enums: `herbylab.project_status`, `herbylab.artifact_type`, `herbylab.project_domain`. +- Triggers / functions: `projects_notify` (NOTIFY on row change), `artifacts_set_updated_at`. +- No vector columns. + +**Why empty (verified, not a missed lookup):** the schema was created +by alembic migration `9c7e3f1a2b4d` on the unmerged branch +`claude/competent-knuth-de9483` (commit `90bc6a9 Phase 1: herbylab +Postgres schema infrastructure`). That branch also has commits +`8e408c5 Phase 2: polymorphic artifact tools backed by Postgres + +vault` and `78ec094 Phase 3: Trello projection via n8n` which would +have wired writes into the schema, but **the branch is not merged to +`main`.** + +The deployed `herbydev-mcp` runs `main`'s code, which has zero DB +access: +- `pyproject.toml` declares no DB driver (no psycopg / sqlalchemy / + asyncpg). +- `mcp_server/tools/memory.py` and `mcp_server/tools/tasks.py` are + empty stub files marked "Phase 2. Do not implement until workflow + patterns are established." +- `mcp_server/tools/vault.py` writes via `core/git_writer.py` → + filesystem + git commit + push to gitea. No database insertion. +- `HERBYLAB_DATABASE_URL` is plumbed through compose env but never + read by the running code. + +So the DB schema exists from an integration-test run of the unmerged +branch's migration; no code path on `main` ever populates it. + +Implication for consolidation: the `wiki` schema in `petalbrain` is +genuinely empty content-wise. Worker 4's vault-indexing source is the +filesystem (`/opt/projects/wiki-vault/`), not a `wiki.notes` / +`wiki.artifacts` table. If `competent-knuth-de9483` lands later, its +artifact tooling will need to be re-pointed at `petalbrain.wiki.*` +instead of `herbylab.herbylab.*`, and the Phase 4 reflection design +should account for `wiki.artifacts` becoming a populated source over +time. + +### `openbrain` DB +- Schemas: `public` only. +- Tables in `public`: + - `thoughts` — 70 rows (live; daemons writing today) +- pgvector 0.8.2 in `public`. + +`public.thoughts` shape (relevant columns): + +``` +id bigint PK (nextval thoughts_id_seq) +content text NOT NULL +embedding vector(768) +metadata jsonb DEFAULT '{}' +user_id text NOT NULL DEFAULT 'travis' +created_at timestamptz DEFAULT CURRENT_TIMESTAMP +updated_at timestamptz DEFAULT CURRENT_TIMESTAMP +``` + +Existing indexes on `public.thoughts`: +- `thoughts_pkey` (id) +- `idx_thoughts_created_at` (created_at DESC) +- `idx_thoughts_embedding_hnsw` — **hnsw (embedding vector_cosine_ops) WITH (m=16, ef_construction=64)** — already matches the locked Phase 4 spec for `public.embeddings`. +- `idx_thoughts_fingerprint` — UNIQUE (md5(content), user_id) +- `idx_thoughts_metadata` — gin (metadata) +- `idx_thoughts_user_created` (user_id, created_at DESC) +- `idx_thoughts_user_id` (user_id) + +Additional objects: +- Function `match_thoughts(query_embedding vector, match_threshold float, match_count int, filter jsonb, p_user_id text)` — bare-`vector` param (no dim), so dimension changes flow through unchanged. +- Trigger `thoughts_updated_at` -> `update_updated_at()`. + +Live writer state: 70 rows total, 0 with NULL embedding, 2 still flagged `metadata->>'pending' = 'true'` (enricher backlog). Most recent row id 255 at `2026-05-16 02:07:23 UTC`. Watcher and enricher daemons confirmed live. + +### `trellis` DB +- Schemas: `trellis` (owner `trellis_user`), `public`. +- Tables in `trellis`: + - `threads` — 13 rows + - `events` — 50 rows + - `tags` — 42 rows + - `thread_tags` — 55 rows + - `alembic_version` — 1 row +- No vector columns. + +## OB1 embedding model + +From `ob1-mcp` container env (and `/opt/projects/homelab/ob1-deploy/compose.yml`): + +- `EMBEDDING_PROVIDER=ollama` +- `OLLAMA_URL=http://ollama:11434` +- `EMBEDDING_MODEL=nomic-embed-text` (768-d output) +- `CHAT_MODEL=openai/gpt-4o-mini` (mcp side) +- `CHAT_MODEL=llama3.2:3b` (enricher side) + +Migration trail: +- `init.sql` originally declared `vector(1536)` for `text-embedding-3-small`. +- `migrations/001_embedding_dim_768.sql` (applied 2026-05-14) dropped the + HNSW index, altered the column to `vector(768)`, recreated the index. + Required `thoughts` to be empty pre-migration. + +**Conclusion:** the shared `public.embeddings` table goes in as +`vector(768)` to match `nomic-embed-text`, no model switch needed. HNSW +index spec from Travis's overrides (`vector_cosine_ops, m=16, +ef_construction=64`) matches the existing OB1 convention exactly. + +## MCP server connection configurations + +| Container | Image | DB host / DB / user | Live? | +|-----------------|--------------------|---------------------|-------| +| `herbydev-mcp` | `herbydev-mcp` | `homelab-postgres:5432/herbylab` as `herbylab_user` | Up 40h | +| `vault-mcp` | `vault-mcp` | **`vault-postgres:5432/herbylab` as `herbylab_user`** | Up 59m | +| `ob1-mcp` | `ob1-mcp` | `homelab-postgres:5432/openbrain` as `postgres` | Up 14h | +| `ob1-watcher` | `ob1-watcher` | (talks to ob1-mcp HTTP, not DB directly) | Up 26h | +| `ob1-enricher` | `ob1-enricher` | (uses `OLLAMA_URL`; DB password set but no direct connect string visible) | Up 14h | +| `trellis-mcp` | `trellis-trellis-mcp` | `homelab-postgres:5432/trellis` as `trellis_user` | Up 40h healthy | + +Connection strings live in: +- `vault-mcp` / `herbydev-mcp` — `/opt/projects/homelab/herby-dev/deploy/.env` (`HERBYLAB_DATABASE_URL`) +- `ob1-mcp` / `ob1-enricher` — `/opt/projects/homelab/ob1-deploy/.env` (`POSTGRES_PASSWORD` + per-service `DB_*` env in `compose.yml`) +- `trellis-mcp` — `/opt/projects/homelab/trellis-mcp/` (compose env, `DATABASE_URL`) + +**Anomaly:** `vault-mcp` references hostname `vault-postgres` which +does not resolve on the `homelab` network (alias is `homelab-postgres` ++ `postgres`). This is consistent with Travis's mid-flight rename +rollout — the container is currently running OLD code per the +priming, and the connection-string update is part of the unfinished +rollout. Not blocking Phase 1, but Phase 3 must coordinate with the +rename completion. + +## Backup destination + +**Local-only on herbydev** (Travis decision, 2026-05-15). The NAS +reference in the plan was unintended — disregard it. + +- Destination directory: `/opt/backups/postgres-consolidation/` + (does not exist yet; Phase 1 step zero is `mkdir -p` it). +- Local disk on `/`: 14 G free of 97 G (86% used). +- Combined DB size is ~25 MB, so headroom is ample for both Phase 1 + bulk dumps and Phase 2-start fresh dumps. +- Format: `pg_dumpall` (full instance) + per-database `pg_dump` in + custom format (`-Fc`), gzipped. Filenames include UTC timestamp. +- No NAS / Tailscale / off-host destination involved. + +## Coordination state — running daemons + +`ob1-watcher` and `ob1-enricher` are both live and writing to +`openbrain.public.thoughts` (latest write ~5 minutes before this +note). Per priming, the Phase 2 `ALTER TABLE public.thoughts SET +SCHEMA ob1` step is the cutover gate: daemons must be paused, table +moved + ob1.thoughts shape rewrite by Worker 3, then daemons resumed. +Worker 2 stops at the announcement of readiness for that move and +waits. + +## Naming reconciliation (plan vs. reality) + +Plan vocabulary `wiki / ob1 / trellis` will land as the **schema** +names in the consolidated database, per Travis's locked decisions. +Source DB → target schema mapping: + +- `herbylab` DB content → `wiki` schema (owned by `vault_mcp` per + override; was `herby_mcp` in the plan) +- `openbrain` DB content → `ob1` schema; `public.thoughts` becomes + `ob1.thoughts` (preserving `thoughts` vocabulary per override; plan + said `ob1.captures`) +- `trellis` DB content → `trellis` schema (owned by `trellis_mcp`) + +Plus shared `public.embeddings` per Phase 4. + +**Open: target database name itself.** OB1's database is currently +`openbrain`. The plan says "if OB1's database is the target and is +named appropriately, use in place. Otherwise, rename or create new." +`openbrain` becomes a misleading name once it also holds wiki + +trellis schemas. Worth a deliberate rename (e.g., `homelab`, +`petalbrain`, or similar) before Phase 2 work begins. + +## Phase 1 readiness assessment + +Ready: +- Source DB sizes are trivial; dumps will be small and fast. +- pgvector already present in target DB (`openbrain`); no extension + install needed. +- Embedding model + dim are confirmed and stable (`nomic-embed-text` / + 768) — no re-embed forced. +- Existing HNSW spec on `public.thoughts` matches the locked + `public.embeddings` HNSW spec exactly. + +Not ready / needs decision: +- ~~NAS backup destination~~ **Resolved 2026-05-15**: local-only at + `/opt/backups/postgres-consolidation/`. No NAS involved. +- ~~Target database name~~ **Resolved 2026-05-15**: `petalbrain`. + `openbrain` renamed before Phase 2 destructive ops begin. +- **`wiki` schema content scope** — `herbylab` DB schema came from the + unmerged `claude/competent-knuth-de9483` branch's migration + `9c7e3f1a2b4d`; `main`'s deployed MCP doesn't open a DB connection + at all. Wiki content is filesystem-only under + `/opt/projects/wiki-vault/`. Worker 4's vault-indexing source must + walk the filesystem and write into `public.embeddings` directly. + See the herbylab-DB section above for the full evidence trail. +- **Connection-string anomaly in `vault-mcp`** (`vault-postgres` + hostname). Tied to Travis's rename rollout, blocks Phase 3 cutover + for that MCP only. Phase 1/2 unaffected. + +Conservative call: gate before Phase 1 until the NAS destination and +target DB name decisions are made. + +## Addendum — Phase 2-start fresh backup (Travis directive, 2026-05-15) + +**In addition to Phase 1 bulk backups**, Phase 2 must begin with a +fresh `pg_dump` of each source database immediately before the first +destructive operation (no `CREATE SCHEMA`, no `ALTER TABLE`, no +restore — nothing destructive precedes this step). + +Rationale: `ob1-watcher` is writing to `public.thoughts` continuously +(latest write ~5 min before this note). Time elapses between Phase 1 +dumps and Phase 2 cutover, especially with gating. Any rows captured +in that window would be lost on rollback if Phase 1 dumps are the +only restore point. + +Specifics: +- Run fresh `pg_dump` on `openbrain` first — it's the live writer and + the most critical. Then `herbylab` and `trellis` for symmetry. +- Destination: `/opt/backups/postgres-consolidation/` (same as Phase + 1), gzipped custom format. +- Filename suffix: `-phase2-start-` to distinguish from + Phase 1 backups. +- Verify each fresh dump is readable via test restore to a scratch + database (same pattern as Phase 1). +- Only after the three fresh dumps are verified does Phase 2's first + destructive operation run. + +This is not a replacement for Phase 1 dumps — Phase 1 remains the +broad safety net (full instance `pg_dumpall` + per-DB granular +dumps). Phase 2-start dumps are the just-before-disruption snapshot +that captures any in-flight writer activity. + +Operational note: this is also the natural point to pause +`ob1-watcher` and `ob1-enricher`. Sequence: +1. Pause both daemons (`docker compose stop ob1-watcher ob1-enricher`). +2. Confirm no in-flight writes (`pg_stat_activity` quiet on `openbrain`). +3. Take the fresh per-DB dumps + verify. +4. Begin Phase 2 destructive work. +5. (Worker 3 resumes daemons after `ob1.thoughts` cutover lands.) diff --git a/Sources/Homelab/2026-05-15-postgres-consolidation-phase2a-result.md b/Sources/Homelab/2026-05-15-postgres-consolidation-phase2a-result.md new file mode 100644 index 0000000..0807f4a --- /dev/null +++ b/Sources/Homelab/2026-05-15-postgres-consolidation-phase2a-result.md @@ -0,0 +1,252 @@ +--- +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.