@tekmidian/pai 0.54.0 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{aibroker-client-CNfIBrl5.mjs → aibroker-client-B8Wtasez.mjs} +1 -1
- package/dist/{aibroker-client-DzkTpVjx.mjs → aibroker-client-BK1h--W_.mjs} +3 -3
- package/dist/{aibroker-client-DzkTpVjx.mjs.map → aibroker-client-BK1h--W_.mjs.map} +1 -1
- package/dist/{indexer-backend-CdvCJCkD.mjs → async-CLvuY7b-.mjs} +7 -7
- package/dist/async-CLvuY7b-.mjs.map +1 -0
- package/dist/auto-route-CF8WNaoy.mjs +100 -0
- package/dist/auto-route-CF8WNaoy.mjs.map +1 -0
- package/dist/{chain-BcBDJ1Id.mjs → chain-DogidBji.mjs} +4 -4
- package/dist/{chain-BcBDJ1Id.mjs.map → chain-DogidBji.mjs.map} +1 -1
- package/dist/cli/index.mjs +22 -23
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/program.d.mts.map +1 -1
- package/dist/cli/program.mjs +22 -23
- package/dist/{clusters-Cw_P6lO-.mjs → clusters-Ci1mseh2.mjs} +4 -36
- package/dist/clusters-Ci1mseh2.mjs.map +1 -0
- package/dist/{config-B3EPzRjf.mjs → config-B93YS8tP.mjs} +3 -3
- package/dist/{config-B3EPzRjf.mjs.map → config-B93YS8tP.mjs.map} +1 -1
- package/dist/{config-Bn9jpblO.mjs → config-D1omG_UU.mjs} +3 -3
- package/dist/{config-Bn9jpblO.mjs.map → config-D1omG_UU.mjs.map} +1 -1
- package/dist/{config-RjeVGJ7K.mjs → config-DCYWFaEi.mjs} +1 -1
- package/dist/{context-handover-cache-CXyH6QsC.mjs → context-handover-cache-k2kWbE6G.mjs} +82 -99
- package/dist/context-handover-cache-k2kWbE6G.mjs.map +1 -0
- package/dist/daemon/index.mjs +18 -24
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/daemon-B7Y8GQnt.mjs +19 -0
- package/dist/{daemon-BkJNFWr9.mjs → daemon-Cgrdvdo6.mjs} +97 -466
- package/dist/daemon-Cgrdvdo6.mjs.map +1 -0
- package/dist/daemon-mcp/index.mjs +10 -10
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/{detector-DWXkeTlk.mjs → detector-3g7XuU1r.mjs} +5 -12
- package/dist/detector-3g7XuU1r.mjs.map +1 -0
- package/dist/detector-DgDn56j-.mjs +3 -0
- package/dist/{embeddings-ClqKtLZ8.mjs → embeddings-BNv3TQC5.mjs} +1 -1
- package/dist/{embeddings-DvRKcQyt.mjs → embeddings-DfrYNIng.mjs} +1 -1
- package/dist/{embeddings-DvRKcQyt.mjs.map → embeddings-DfrYNIng.mjs.map} +1 -1
- package/dist/factory-C9wGUe30.mjs +3 -0
- package/dist/factory-DiuntvwG.mjs +250 -0
- package/dist/factory-DiuntvwG.mjs.map +1 -0
- package/dist/{fallback-CPfyTSTx.mjs → fallback-DS6AEJsU.mjs} +190 -132
- package/dist/fallback-DS6AEJsU.mjs.map +1 -0
- package/dist/federation-db-CJDw_Pwq.mjs +3 -0
- package/dist/{db-DJ8rk0VM.mjs → federation-db-w8e-sKjp.mjs} +5 -5
- package/dist/federation-db-w8e-sKjp.mjs.map +1 -0
- package/dist/{helpers-crDEr6S2.mjs → helpers-D9WkIf9R.mjs} +1 -1
- package/dist/{helpers-crDEr6S2.mjs.map → helpers-D9WkIf9R.mjs.map} +1 -1
- package/dist/hooks/capture-all-events.mjs.map +1 -1
- package/dist/hooks/capture-session-summary.mjs.map +1 -1
- package/dist/hooks/capture-tool-output.mjs.map +1 -1
- package/dist/hooks/context-compression-hook.mjs.map +1 -1
- package/dist/hooks/load-core-context.mjs +5 -0
- package/dist/hooks/load-core-context.mjs.map +2 -2
- package/dist/hooks/load-project-context.mjs.map +1 -1
- package/dist/hooks/post-compact-inject.mjs.map +1 -1
- package/dist/hooks/route-agents-to-worker.mjs.map +1 -1
- package/dist/hooks/route-edits-to-worker.mjs +1 -1
- package/dist/hooks/route-edits-to-worker.mjs.map +2 -2
- package/dist/hooks/security-validator.mjs +8367 -19
- package/dist/hooks/security-validator.mjs.map +4 -4
- package/dist/hooks/stop-hook.mjs.map +2 -2
- package/dist/hooks/whisper-rules.mjs.map +2 -2
- package/dist/hooks/worker-proxy.mjs.map +1 -1
- package/dist/hooks/worker-status-line.mjs.map +2 -2
- package/dist/hooks/worker-supervision.mjs.map +1 -1
- package/dist/index.d.mts +327 -100
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +7 -11
- package/dist/indexer-backend-Cc-_NK3q.mjs +1 -0
- package/dist/indexer-backend-DcDtin1O.mjs +5 -0
- package/dist/{ipc-client-C6NryMMb.mjs → ipc-client-Dasm515c.mjs} +2 -2
- package/dist/{ipc-client-C6NryMMb.mjs.map → ipc-client-Dasm515c.mjs.map} +1 -1
- package/dist/kg-entity-Cj8Q6YmU.mjs +29 -0
- package/dist/kg-entity-Cj8Q6YmU.mjs.map +1 -0
- package/dist/{latent-ideas-BfL4nCQ3.mjs → latent-ideas-BRy6HNwg.mjs} +4 -4
- package/dist/{latent-ideas-BfL4nCQ3.mjs.map → latent-ideas-BRy6HNwg.mjs.map} +1 -1
- package/dist/{link-boost-C5PaNdzm.mjs → link-boost-BptbAPuv.mjs} +1 -1
- package/dist/{link-boost-C5PaNdzm.mjs.map → link-boost-BptbAPuv.mjs.map} +1 -1
- package/dist/{main-resolver-Bt1f6jJc.mjs → main-resolver-Dx-pxic1.mjs} +56 -31
- package/dist/main-resolver-Dx-pxic1.mjs.map +1 -0
- package/dist/main-resolver-uDtK9p6e.mjs +7 -0
- package/dist/merge-BncjJTfb.mjs +3 -0
- package/dist/merge-xoyfZQ4W.mjs +6 -0
- package/dist/merge-xoyfZQ4W.mjs.map +1 -0
- package/dist/{migrate-CfERwXhY.mjs → migrate-BD7D8EEh.mjs} +21 -54
- package/dist/migrate-BD7D8EEh.mjs.map +1 -0
- package/dist/{neighborhood-CETR_P4y.mjs → neighborhood-D8uscnGy.mjs} +4 -25
- package/dist/neighborhood-D8uscnGy.mjs.map +1 -0
- package/dist/{note-context-DCcRWBoN.mjs → note-context-CgffaUMh.mjs} +3 -23
- package/dist/note-context-CgffaUMh.mjs.map +1 -0
- package/dist/{pai-home-ChAOupZc.mjs → pai-home-CeOIYltV.mjs} +2 -2
- package/dist/pai-home-CeOIYltV.mjs.map +1 -0
- package/dist/{pai-marker-B20KqhA8.mjs → pai-marker-D1MMswkz.mjs} +1 -1
- package/dist/{pai-marker-B20KqhA8.mjs.map → pai-marker-D1MMswkz.mjs.map} +1 -1
- package/dist/{planner-BLbCtBUa.mjs → planner-C2wWnZrZ.mjs} +7 -7
- package/dist/{planner-BLbCtBUa.mjs.map → planner-C2wWnZrZ.mjs.map} +1 -1
- package/dist/{postgres-CUSXWgTG.mjs → postgres-nvU5pShF.mjs} +863 -4
- package/dist/postgres-nvU5pShF.mjs.map +1 -0
- package/dist/{program-DYux0CW2.mjs → program-Cb-9J5Ie.mjs} +2260 -1399
- package/dist/program-Cb-9J5Ie.mjs.map +1 -0
- package/dist/query-feedback-COYCnvHQ.mjs +3 -0
- package/dist/{query-feedback-CTs_jx6i.mjs → query-feedback-xz5axTj3.mjs} +3 -3
- package/dist/query-feedback-xz5axTj3.mjs.map +1 -0
- package/dist/{db-Ddns80yf.mjs → registry-db-Bkpbp7Gf.mjs} +5 -5
- package/dist/registry-db-Bkpbp7Gf.mjs.map +1 -0
- package/dist/registry-db-DwXpf7yF.mjs +3 -0
- package/dist/registry-postgres-B07-hmn8.mjs +795 -0
- package/dist/registry-postgres-B07-hmn8.mjs.map +1 -0
- package/dist/registry-sqlite-CkGND_tm.mjs +593 -0
- package/dist/registry-sqlite-CkGND_tm.mjs.map +1 -0
- package/dist/{reranker-B5qUTamW.mjs → reranker-BS5gqGeF.mjs} +1 -1
- package/dist/{reranker-HHmhZk6v.mjs → reranker-CRVawoEh.mjs} +1 -1
- package/dist/{reranker-HHmhZk6v.mjs.map → reranker-CRVawoEh.mjs.map} +1 -1
- package/dist/router-BXOf_e9e.mjs +3 -0
- package/dist/{router-DBAZfE7X.mjs → router-BdxdsAvs.mjs} +2 -2
- package/dist/{router-DBAZfE7X.mjs.map → router-BdxdsAvs.mjs.map} +1 -1
- package/dist/{run-C7EDdVAB.mjs → run-DkTrZejF.mjs} +268 -53
- package/dist/run-DkTrZejF.mjs.map +1 -0
- package/dist/{runtime-paths-B0P1TvUr.mjs → runtime-paths-BRwDeBEg.mjs} +1 -1
- package/dist/{runtime-paths-B0P1TvUr.mjs.map → runtime-paths-BRwDeBEg.mjs.map} +1 -1
- package/dist/search-BQHL5wDr.mjs +3 -0
- package/dist/search-BaTbyWjK.mjs +107 -0
- package/dist/search-BaTbyWjK.mjs.map +1 -0
- package/dist/{server-CGiHdaUp.mjs → server-CGuZFOeR.mjs} +4 -4
- package/dist/{server-CGiHdaUp.mjs.map → server-CGuZFOeR.mjs.map} +1 -1
- package/dist/{session-keepalive-DC2JGggK.mjs → session-keepalive-C5_69Av6.mjs} +7 -7
- package/dist/{session-keepalive-DC2JGggK.mjs.map → session-keepalive-C5_69Av6.mjs.map} +1 -1
- package/dist/skills/Plan/SKILL.md +1 -1
- package/dist/{sources-KBGEA_E6.mjs → sources-BBe3AZ-W.mjs} +8 -27
- package/dist/sources-BBe3AZ-W.mjs.map +1 -0
- package/dist/sqlite-D9p5a4qe.mjs +935 -0
- package/dist/sqlite-D9p5a4qe.mjs.map +1 -0
- package/dist/state-CwlGAKgF.mjs +3 -0
- package/dist/{state-DwrsOoMi.mjs → state-n13uPK5B.mjs} +6 -5
- package/dist/state-n13uPK5B.mjs.map +1 -0
- package/dist/{stop-words-BaMEGVeY.mjs → stop-words-ClCwO8mZ.mjs} +1 -1
- package/dist/{stop-words-BaMEGVeY.mjs.map → stop-words-ClCwO8mZ.mjs.map} +1 -1
- package/dist/{themes-BVoAnLZj.mjs → themes-B2v7RNnA.mjs} +3 -3
- package/dist/{themes-BVoAnLZj.mjs.map → themes-B2v7RNnA.mjs.map} +1 -1
- package/dist/{tools-BEqiJt_j.mjs → tools-Diro2s8J.mjs} +157 -398
- package/dist/tools-Diro2s8J.mjs.map +1 -0
- package/dist/{tools-BwsOTmjh.mjs → tools-KgL1rkPD.mjs} +4 -4
- package/dist/{trace-Cy8quQTm.mjs → trace-BRflVyBi.mjs} +1 -1
- package/dist/{trace-Cy8quQTm.mjs.map → trace-BRflVyBi.mjs.map} +1 -1
- package/dist/{utils-BD8g8oSf.mjs → utils-BrX9FLeK.mjs} +1 -1
- package/dist/{utils-CJymHkYm.mjs → utils-C9HsYDpO.mjs} +1 -1
- package/dist/{utils-CJymHkYm.mjs.map → utils-C9HsYDpO.mjs.map} +1 -1
- package/dist/{vault-indexer-C5i-OAyr.mjs → vault-indexer-DHYBpRZ5.mjs} +2 -2
- package/dist/{vault-indexer-C5i-OAyr.mjs.map → vault-indexer-DHYBpRZ5.mjs.map} +1 -1
- package/dist/{wakeup-Yk5lQ0jb.mjs → wakeup-D0uvqLsE.mjs} +95 -4
- package/dist/wakeup-D0uvqLsE.mjs.map +1 -0
- package/dist/work-queue-worker-DEbWsrDQ.mjs +11 -0
- package/dist/{work-queue-worker-utPp4hUv.mjs → work-queue-worker-DufMOtrL.mjs} +9 -9
- package/dist/work-queue-worker-DufMOtrL.mjs.map +1 -0
- package/dist/{zettelkasten-DezbXghl.mjs → zettelkasten-BPxSOalU.mjs} +4 -4
- package/dist/{zettelkasten-DezbXghl.mjs.map → zettelkasten-BPxSOalU.mjs.map} +1 -1
- package/docker/init.sql +5 -1
- package/docs/commands/README.md +5 -0
- package/docs/commands/audit.md +1 -1
- package/docs/commands/backup.md +1 -1
- package/docs/commands/clear-names.md +1 -1
- package/docs/commands/config.md +4 -4
- package/docs/commands/daemon.md +1 -1
- package/docs/commands/db.md +15 -1
- package/docs/commands/end.md +1 -1
- package/docs/commands/help.md +1 -1
- package/docs/commands/hooks-db.md +47 -0
- package/docs/commands/identity.md +1 -1
- package/docs/commands/kg.md +1 -1
- package/docs/commands/launch.md +1 -1
- package/docs/commands/mcp.md +1 -1
- package/docs/commands/memory.md +1 -1
- package/docs/commands/notify.md +1 -1
- package/docs/commands/observation.md +1 -1
- package/docs/commands/obsidian.md +1 -1
- package/docs/commands/pause.md +1 -1
- package/docs/commands/project.md +1 -1
- package/docs/commands/projects.md +1 -1
- package/docs/commands/registry.md +1 -1
- package/docs/commands/restore.md +1 -1
- package/docs/commands/session.md +2 -2
- package/docs/commands/sessions.md +1 -1
- package/docs/commands/setup.md +1 -1
- package/docs/commands/shell-init.md +1 -1
- package/docs/commands/skill.md +1 -1
- package/docs/commands/task.md +1 -1
- package/docs/commands/topic.md +1 -1
- package/docs/commands/update.md +1 -1
- package/docs/commands/worker.md +1 -1
- package/docs/commands/zettel.md +1 -1
- package/docs/design/postgres-only.md +438 -0
- package/docs/design/registry-method-map.md +174 -0
- package/package.json +1 -1
- package/src/hooks/pre-compact.sh +2 -40
- package/src/hooks/session-hooks-db.test.ts +93 -0
- package/src/hooks/session-stop.sh +2 -26
- package/src/hooks/ts/lib/edit-gate.test.ts +9 -0
- package/src/hooks/ts/lib/edit-gate.ts +5 -2
- package/src/hooks/ts/lib/plutil-guard.test.ts +54 -0
- package/src/hooks/ts/lib/plutil-guard.ts +43 -0
- package/src/hooks/ts/lib/worker-git-guard.test.ts +134 -0
- package/src/hooks/ts/lib/worker-git-guard.ts +103 -0
- package/src/hooks/ts/pre-tool-use/security-validator.test.ts +68 -0
- package/src/hooks/ts/pre-tool-use/security-validator.ts +69 -21
- package/src/hooks/ts/session-start/load-core-context.ts +13 -0
- package/src/hooks/ts/session-start/session-start-worker-guard.test.ts +13 -0
- package/dist/auto-route-CcD0OWWB.mjs +0 -86
- package/dist/auto-route-CcD0OWWB.mjs.map +0 -1
- package/dist/clusters-Cw_P6lO-.mjs.map +0 -1
- package/dist/context-handover-cache-CXyH6QsC.mjs.map +0 -1
- package/dist/daemon-BkJNFWr9.mjs.map +0 -1
- package/dist/daemon-CiythMfR.mjs +0 -25
- package/dist/db-BOBfewFH.mjs +0 -3
- package/dist/db-DJ8rk0VM.mjs.map +0 -1
- package/dist/db-DbY2O8QO.mjs +0 -3
- package/dist/db-Ddns80yf.mjs.map +0 -1
- package/dist/detect-Bf2z-oKB.mjs +0 -86
- package/dist/detect-Bf2z-oKB.mjs.map +0 -1
- package/dist/detector-CkwkXuGh.mjs +0 -5
- package/dist/detector-DWXkeTlk.mjs.map +0 -1
- package/dist/factory-Ba8j9K7q.mjs +0 -3
- package/dist/factory-Bt7TWQje.mjs +0 -150
- package/dist/factory-Bt7TWQje.mjs.map +0 -1
- package/dist/fallback-CPfyTSTx.mjs.map +0 -1
- package/dist/indexer-backend-CdvCJCkD.mjs.map +0 -1
- package/dist/kg-entity-BSW7ra8i.mjs +0 -176
- package/dist/kg-entity-BSW7ra8i.mjs.map +0 -1
- package/dist/main-resolver-Bt1f6jJc.mjs.map +0 -1
- package/dist/main-resolver-DYHhw5Ig.mjs +0 -7
- package/dist/migrate-CfERwXhY.mjs.map +0 -1
- package/dist/neighborhood-CETR_P4y.mjs.map +0 -1
- package/dist/note-context-DCcRWBoN.mjs.map +0 -1
- package/dist/pai-home-ChAOupZc.mjs.map +0 -1
- package/dist/postgres-CUSXWgTG.mjs.map +0 -1
- package/dist/program-DYux0CW2.mjs.map +0 -1
- package/dist/query-feedback-Bm6XvCmg.mjs +0 -3
- package/dist/query-feedback-CTs_jx6i.mjs.map +0 -1
- package/dist/router-CremHKTI.mjs +0 -3
- package/dist/run-C7EDdVAB.mjs.map +0 -1
- package/dist/search-B69cQwDB.mjs +0 -303
- package/dist/search-B69cQwDB.mjs.map +0 -1
- package/dist/search-fb-5ovj4.mjs +0 -4
- package/dist/sources-KBGEA_E6.mjs.map +0 -1
- package/dist/sqlite-By6r2YhZ.mjs +0 -271
- package/dist/sqlite-By6r2YhZ.mjs.map +0 -1
- package/dist/state-Ca0ltLgI.mjs +0 -3
- package/dist/state-DwrsOoMi.mjs.map +0 -1
- package/dist/sync-Btcb6xA4.mjs +0 -310
- package/dist/sync-Btcb6xA4.mjs.map +0 -1
- package/dist/tools-BEqiJt_j.mjs.map +0 -1
- package/dist/wakeup-Yk5lQ0jb.mjs.map +0 -1
- package/dist/work-queue-worker-DsdJx-jn.mjs +0 -14
- package/dist/work-queue-worker-utPp4hUv.mjs.map +0 -1
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
# Postgres-only storage — design
|
|
2
|
+
|
|
3
|
+
Goal: when `storageBackend: postgres`, no code path opens or writes SQLite. Everything that
|
|
4
|
+
today lives only in `federation.db` (`kg_entities`) or `registry.db` (projects, sessions,
|
|
5
|
+
aliases, tags, session_tags, links, compaction_log) moves to Postgres, verified, with the
|
|
6
|
+
SQLite files kept as renamed backups — never deleted. Measured state (2026-09-22) and the
|
|
7
|
+
prior `StorageBackend` abstraction (`src/storage/{interface,sqlite,postgres}.ts`) are taken
|
|
8
|
+
as given; this document extends that layer rather than replacing it.
|
|
9
|
+
|
|
10
|
+
Hard constraint (added 2026-09-22, from the container definition): Postgres runs in
|
|
11
|
+
`pai-pgvector` (`docker/docker-compose.yml`, `pgvector/pgvector:pg17`, `127.0.0.1:5432`)
|
|
12
|
+
with a **bind mount**, not a named volume — confirmed via `docker inspect`:
|
|
13
|
+
`~/.pai/pgdata -> /var/lib/postgresql/data`, rw, type `bind`. Every step in
|
|
14
|
+
this design (schema init, migration, verification) goes through the normal `pg` connection
|
|
15
|
+
on 5432; nothing writes into the container filesystem or a Docker-managed volume, and no
|
|
16
|
+
step uses `docker cp`. The migration command's pre-flight check and rollback step (§4) both
|
|
17
|
+
depend on this bind mount being present on the host, not on anything inside the container.
|
|
18
|
+
|
|
19
|
+
Open question, not resolved by this change: `~/.pai/pgdata` sits outside `PAI_HOME`
|
|
20
|
+
(`~/.claude/pai`), i.e. Postgres data isn't backed up/relocated the way the rest of PAI's
|
|
21
|
+
state is. Out of scope here — flagging it, not moving it.
|
|
22
|
+
|
|
23
|
+
## 1. Inventory — every non-test file that opens or touches SQLite
|
|
24
|
+
|
|
25
|
+
### 1a. Storage-layer definition files (own the schema/opener today)
|
|
26
|
+
|
|
27
|
+
| File | Role |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `src/memory/db.ts` | defines `openFederation()` — `new BetterSqlite3(federation.db)`, sync |
|
|
30
|
+
| `src/memory/schema.ts` | `federation.db` DDL: `memory_files`, `memory_chunks`, `memory_fts`, **`kg_entities`** |
|
|
31
|
+
| `src/registry/db.ts` | defines `openRegistry()` — `new BetterSqlite3(registry.db)`, sync |
|
|
32
|
+
| `src/registry/schema.ts` | `registry.db` DDL: `projects`, `sessions`, `tags`, `project_tags`, `session_tags`, `aliases`, `compaction_log`, `links`, `schema_version` |
|
|
33
|
+
|
|
34
|
+
`grep -rn "new Database(" src` outside these two openers only matches two test files
|
|
35
|
+
(`project/here.test.ts`, `project/mcp-tools.test.ts`, both `:memory:` — fine, tests are
|
|
36
|
+
out of scope).
|
|
37
|
+
|
|
38
|
+
### 1b. Direct call sites that acquire a `Database` handle (sync, better-sqlite3)
|
|
39
|
+
|
|
40
|
+
| File:line | Opens | Tables reached |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `src/cli/program.ts:83` | `openRegistry()` → cached in module-level `getDb()`, passed to **~60** CLI command modules (§1e) | all registry tables |
|
|
43
|
+
| `src/daemon/daemon/server.ts:138` | `openRegistry()` → `setRegistryDb()`, shared with daemon handlers | all registry tables |
|
|
44
|
+
| `src/daemon/daemon/state.ts` | imports `openRegistry` (daemon state init) | all registry tables |
|
|
45
|
+
| `src/workers/project-config.ts:44` | `openRegistry(dbPath)` | `projects`, `sessions`, `session_config` column |
|
|
46
|
+
| `src/cli/commands/task.ts:125,302` | `openRegistry()` | `aliases`, `projects` |
|
|
47
|
+
| `src/cli/commands/session/commands.ts:594` | `openRegistry()` | `sessions`, `projects` |
|
|
48
|
+
| `src/memory/kg-backfill.ts:184,187` | `openFederation()` **and** `openRegistry()` in the same pass | `kg_entities`, `projects` |
|
|
49
|
+
| `src/daemon/daemon/dispatcher.ts:174,184` | `openFederation()` | `memory_files`, `memory_chunks`, `kg_entities` (indirectly) |
|
|
50
|
+
| `src/daemon/daemon/scheduler.ts:295` | `openFederation()` | passed into `indexAll()` |
|
|
51
|
+
| `src/daemon/session-summary-worker.ts:950` | `openFederation()` | passed into `kg-extraction.ts` |
|
|
52
|
+
| `src/cli/commands/db.ts:32` | `openFederation()` | `pai db` stats/inspect command |
|
|
53
|
+
| `src/cli/commands/zettel/utils.ts:20` | `openFederation()` | zettelkasten commands (legacy path; most zettelkasten modules already take `StorageBackend`, see §1e) |
|
|
54
|
+
| `src/cli/commands/memory/{stats,embed,index-cmd,search}.ts` | `openFederation()` (fallback path — the primary path in these files already goes through `createStorageBackend()`) | `memory_files`, `memory_chunks` |
|
|
55
|
+
| `src/storage/factory.ts:214` | `openFederation()` — **this is the SQLite branch of the existing abstraction**, expected to remain for `storageBackend: sqlite` | wraps in `SQLiteBackend` |
|
|
56
|
+
|
|
57
|
+
### 1c. Shell hooks (sync, `sqlite3` CLI subprocess — not better-sqlite3, but still opens SQLite)
|
|
58
|
+
|
|
59
|
+
| File | Tables |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `src/hooks/session-stop.sh:118,122,132` | reads/writes `projects`, `sessions` |
|
|
62
|
+
| `src/hooks/pre-compact.sh:47,51,60,71,75` | reads/writes `projects`, `sessions`, `compaction_log` |
|
|
63
|
+
|
|
64
|
+
These run as plain bash subprocesses with no Node runtime — they cannot use a `pg` pool
|
|
65
|
+
directly (see §6, risk: hooks under Postgres).
|
|
66
|
+
|
|
67
|
+
### 1d. `kg_entities` consumers (sync, take a `Database` handle as a parameter)
|
|
68
|
+
|
|
69
|
+
| File | Functions | Sync |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `src/memory/kg-entity.ts` | `upsertKgEntity`, `findKgEntity`, `listKgEntities`, `updateEntityFeedbackWeight` — all `db.prepare(...)` | sync |
|
|
72
|
+
| `src/memory/kg-extraction.ts` | calls `upsertKgEntity` | sync |
|
|
73
|
+
| `src/memory/kg-search.ts` | calls `listKgEntities` | sync |
|
|
74
|
+
| `src/mcp/tools/feedback.ts` | calls `listKgEntities`, `updateEntityFeedbackWeight` | sync |
|
|
75
|
+
|
|
76
|
+
Note: `kg_triples` (the edges) is **already Postgres-only** (`src/storage/postgres/backend.ts`
|
|
77
|
+
`runMigrations()` creates it directly in Postgres, no SQLite equivalent exists). Only
|
|
78
|
+
`kg_entities` is still SQLite-only — it is the smaller of the two gaps in the architecture.
|
|
79
|
+
|
|
80
|
+
### 1e. `registry.db` table consumers (sync, reached via `getDb()` from `src/cli/program.ts` or an
|
|
81
|
+
equivalent daemon/worker singleton)
|
|
82
|
+
|
|
83
|
+
64 non-test files contain direct `db.prepare(...)` SQL against `projects` / `sessions` /
|
|
84
|
+
`aliases` / `tags` / `session_tags` / `links` / `compaction_log`. Grouped by module (every
|
|
85
|
+
file in the grep match is listed; none omitted):
|
|
86
|
+
|
|
87
|
+
- **CLI project commands** (registry: `projects`, `aliases`, `tags`, `project_tags`):
|
|
88
|
+
`src/cli/commands/project/{index,commands,here,helpers,health,merge,unregister,projects-index,session-config}.ts`,
|
|
89
|
+
`src/cli/commands/main-resolver.ts`, `src/cli/commands/pick.ts`, `src/cli/commands/detect.ts`
|
|
90
|
+
- **CLI session commands** (registry: `sessions`, `session_tags`, `compaction_log`):
|
|
91
|
+
`src/cli/commands/session/{index,commands,autosave,pause,handover,goto,end,helpers,sessions-index}.ts`,
|
|
92
|
+
`src/cli/commands/session-cleanup/{index,scanner,executor}.ts`, `src/cli/lib/{session-scan,dedup-sessions}.ts`
|
|
93
|
+
- **CLI registry commands** (registry: all tables, dedup/scan/migrate tooling):
|
|
94
|
+
`src/cli/commands/registry/{index,scan,reconnect,dedupe,migrate,utils}.ts`, `src/registry/merge.ts`,
|
|
95
|
+
`src/registry/migrate.ts` (`migrateFromJson` writes `projects`/`sessions`)
|
|
96
|
+
- **CLI memory/obsidian/backup/config**: `src/cli/commands/{memory/stats,memory/search,memory/index-cmd,
|
|
97
|
+
memory/embed,backup,restore,config,obsidian}.ts`, `src/obsidian/sync/{generate,master,symlinks}.ts`,
|
|
98
|
+
`src/obsidian/status.ts`, `src/obsidian/vault-fixer.ts`
|
|
99
|
+
- **MCP tools** (registry reads for tool responses): `src/mcp/tools/{registry,projects,sessions,wakeup,types}.ts`
|
|
100
|
+
- **Daemon**: `src/daemon/daemon/{dispatcher,handler,scheduler,server,state,types}.ts`,
|
|
101
|
+
`src/daemon/session-summary-worker.ts` — `handler.ts` is the clearest existing example of the
|
|
102
|
+
target failure mode: it already mixes `registryDb.prepare(...)` (sync SQLite) and
|
|
103
|
+
`pool.query(...)` (async Postgres, via `PostgresBackendWithPool`) in the same file.
|
|
104
|
+
- **Session/task/topic routing**: `src/session/{auto-route,promote}.ts`, `src/tasks/resolver.ts`,
|
|
105
|
+
`src/topics/detector.ts` (note: `detector.ts` primarily takes `StorageBackend`; its one direct
|
|
106
|
+
registry touch is a `sessions`-adjacent lookup, confirm during implementation unit 3)
|
|
107
|
+
- **Workers**: `src/workers/project-config.ts`
|
|
108
|
+
|
|
109
|
+
`grep -n "getDb(" src/cli` (17 files) shows every one of the above CLI files receives the
|
|
110
|
+
handle from `program.ts`'s singleton rather than opening its own — they are "opens SQLite"
|
|
111
|
+
in the sense the boundary test (§3) must catch (they import `Database` from `better-sqlite3`
|
|
112
|
+
and call `.prepare()`), even though the physical `openRegistry()` call happens once, upstream.
|
|
113
|
+
|
|
114
|
+
### Summary
|
|
115
|
+
|
|
116
|
+
| Data | Only copy today | Direct openers | Downstream consumers |
|
|
117
|
+
|---|---|---|---|
|
|
118
|
+
| `kg_entities` | `federation.db` (23,343 rows, live writes) | 1 (`kg-backfill.ts`) + factory/dispatcher opens for other reasons | 4 files (§1d) |
|
|
119
|
+
| `projects, sessions, aliases, tags, session_tags, links, compaction_log` | `registry.db` (278/6,892/26/5/5/3/664) | ~10 call sites (§1b) + 2 shell hooks (§1c) | 64 files (§1e) |
|
|
120
|
+
|
|
121
|
+
## 2. Target Postgres schema
|
|
122
|
+
|
|
123
|
+
Follows the existing pattern exactly: base DDL lives in `docker/init.sql` (applied by
|
|
124
|
+
`PostgresBackend.ensureDatabase()` on a fresh database), incremental additions go through
|
|
125
|
+
`PostgresBackend.runMigrations()` in `src/storage/postgres/backend.ts` — the same place
|
|
126
|
+
`kg_triples` was added. `kg_entities` and the registry tables are added the same way,
|
|
127
|
+
**appended to `runMigrations()`** rather than `init.sql`, so existing Postgres databases
|
|
128
|
+
(which already have `pai_files`/`pai_chunks`/`kg_triples`/vault tables) pick them up without
|
|
129
|
+
a fresh `CREATE DATABASE`.
|
|
130
|
+
|
|
131
|
+
```sql
|
|
132
|
+
-- appended to PostgresBackend.runMigrations(), guarded by information_schema checks
|
|
133
|
+
-- exactly like the existing kg_triples migration
|
|
134
|
+
|
|
135
|
+
CREATE TABLE IF NOT EXISTS kg_entities (
|
|
136
|
+
entity_id TEXT PRIMARY KEY,
|
|
137
|
+
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
138
|
+
name TEXT NOT NULL,
|
|
139
|
+
type TEXT NOT NULL DEFAULT 'unknown',
|
|
140
|
+
description TEXT,
|
|
141
|
+
first_seen BIGINT,
|
|
142
|
+
last_seen BIGINT,
|
|
143
|
+
mention_count INTEGER NOT NULL DEFAULT 1,
|
|
144
|
+
feedback_weight REAL NOT NULL DEFAULT 0.5,
|
|
145
|
+
UNIQUE (tenant_id, entity_id)
|
|
146
|
+
);
|
|
147
|
+
CREATE INDEX IF NOT EXISTS idx_kge_tenant ON kg_entities(tenant_id);
|
|
148
|
+
CREATE INDEX IF NOT EXISTS idx_kge_name ON kg_entities(tenant_id, name);
|
|
149
|
+
CREATE INDEX IF NOT EXISTS idx_kge_type ON kg_entities(tenant_id, type);
|
|
150
|
+
|
|
151
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
152
|
+
id SERIAL PRIMARY KEY,
|
|
153
|
+
slug TEXT NOT NULL UNIQUE,
|
|
154
|
+
display_name TEXT NOT NULL,
|
|
155
|
+
root_path TEXT NOT NULL UNIQUE,
|
|
156
|
+
encoded_dir TEXT NOT NULL UNIQUE,
|
|
157
|
+
type TEXT NOT NULL DEFAULT 'local'
|
|
158
|
+
CHECK (type IN ('local','central','obsidian-linked','external')),
|
|
159
|
+
status TEXT NOT NULL DEFAULT 'active'
|
|
160
|
+
CHECK (status IN ('active','archived','migrating')),
|
|
161
|
+
parent_id INTEGER REFERENCES projects(id),
|
|
162
|
+
obsidian_link TEXT,
|
|
163
|
+
claude_notes_dir TEXT,
|
|
164
|
+
session_config TEXT,
|
|
165
|
+
created_at BIGINT NOT NULL,
|
|
166
|
+
updated_at BIGINT NOT NULL,
|
|
167
|
+
archived_at BIGINT
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
171
|
+
id SERIAL PRIMARY KEY,
|
|
172
|
+
project_id INTEGER NOT NULL REFERENCES projects(id),
|
|
173
|
+
number INTEGER NOT NULL,
|
|
174
|
+
date TEXT NOT NULL,
|
|
175
|
+
slug TEXT NOT NULL,
|
|
176
|
+
title TEXT NOT NULL,
|
|
177
|
+
filename TEXT NOT NULL,
|
|
178
|
+
status TEXT NOT NULL DEFAULT 'open'
|
|
179
|
+
CHECK (status IN ('open','completed','compacted')),
|
|
180
|
+
claude_session_id TEXT,
|
|
181
|
+
token_count INTEGER,
|
|
182
|
+
created_at BIGINT NOT NULL,
|
|
183
|
+
closed_at BIGINT,
|
|
184
|
+
UNIQUE (project_id, number)
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
CREATE TABLE IF NOT EXISTS tags (
|
|
188
|
+
id SERIAL PRIMARY KEY,
|
|
189
|
+
name TEXT NOT NULL UNIQUE
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
CREATE TABLE IF NOT EXISTS project_tags (
|
|
193
|
+
project_id INTEGER NOT NULL REFERENCES projects(id),
|
|
194
|
+
tag_id INTEGER NOT NULL REFERENCES tags(id),
|
|
195
|
+
PRIMARY KEY (project_id, tag_id)
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
CREATE TABLE IF NOT EXISTS session_tags (
|
|
199
|
+
session_id INTEGER NOT NULL REFERENCES sessions(id),
|
|
200
|
+
tag_id INTEGER NOT NULL REFERENCES tags(id),
|
|
201
|
+
PRIMARY KEY (session_id, tag_id)
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
CREATE TABLE IF NOT EXISTS aliases (
|
|
205
|
+
alias TEXT PRIMARY KEY,
|
|
206
|
+
project_id INTEGER NOT NULL REFERENCES projects(id)
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
CREATE TABLE IF NOT EXISTS compaction_log (
|
|
210
|
+
id SERIAL PRIMARY KEY,
|
|
211
|
+
project_id INTEGER NOT NULL REFERENCES projects(id),
|
|
212
|
+
session_id INTEGER REFERENCES sessions(id),
|
|
213
|
+
trigger TEXT NOT NULL CHECK (trigger IN ('precompact','manual','end-session')),
|
|
214
|
+
files_written TEXT NOT NULL,
|
|
215
|
+
token_count INTEGER,
|
|
216
|
+
created_at BIGINT NOT NULL
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
CREATE TABLE IF NOT EXISTS links (
|
|
220
|
+
id SERIAL PRIMARY KEY,
|
|
221
|
+
session_id INTEGER NOT NULL REFERENCES sessions(id),
|
|
222
|
+
target_project_id INTEGER NOT NULL REFERENCES projects(id),
|
|
223
|
+
link_type TEXT NOT NULL DEFAULT 'related'
|
|
224
|
+
CHECK (link_type IN ('related','follow-up','reference')),
|
|
225
|
+
created_at BIGINT NOT NULL,
|
|
226
|
+
UNIQUE (session_id, target_project_id)
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
CREATE INDEX IF NOT EXISTS idx_projects_slug ON projects(slug);
|
|
230
|
+
CREATE INDEX IF NOT EXISTS idx_projects_status ON projects(status);
|
|
231
|
+
CREATE INDEX IF NOT EXISTS idx_projects_type ON projects(type);
|
|
232
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
|
|
233
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_date ON sessions(date);
|
|
234
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
235
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_claude ON sessions(claude_session_id);
|
|
236
|
+
CREATE INDEX IF NOT EXISTS idx_pc_project ON project_tags(project_id);
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Notes on the translation:
|
|
240
|
+
- `INTEGER PRIMARY KEY AUTOINCREMENT` → `SERIAL PRIMARY KEY` (Postgres idiom already used
|
|
241
|
+
by `kg_triples`, `pai_files` etc.).
|
|
242
|
+
- SQLite's `INTEGER` epoch-ms timestamp columns → `BIGINT` (Postgres `INTEGER` is 32-bit
|
|
243
|
+
and `created_at`/`updated_at` are `Date.now()` ms since epoch, which overflows it).
|
|
244
|
+
`pai_chunks.updated_at` already uses this convention — consistent with existing tables.
|
|
245
|
+
Do **not** use `TIMESTAMP` here: callers construct these values with `Date.now()`, not
|
|
246
|
+
`NOW()`, and converting the value shape is out of scope for a storage-backend swap.
|
|
247
|
+
`kg_triples.created_at` is the one existing exception (`TIMESTAMP DEFAULT
|
|
248
|
+
CURRENT_TIMESTAMP`) — inconsistent with the rest of the schema but pre-existing; not
|
|
249
|
+
changed here.
|
|
250
|
+
- FK constraints are declared inline (Postgres allows forward + circular-safe ordering
|
|
251
|
+
here since `projects.parent_id` self-references after the table exists); SQLite's
|
|
252
|
+
`PRAGMA foreign_keys = ON` behavior is enforced unconditionally in Postgres, no pragma
|
|
253
|
+
needed.
|
|
254
|
+
- `schema_version` (SQLite-only bookkeeping table) has no Postgres equivalent — Postgres
|
|
255
|
+
migrations are tracked the same way `kg_triples` is: an `information_schema` existence
|
|
256
|
+
check in `runMigrations()`, not a version counter. Do not port `schema_version`.
|
|
257
|
+
|
|
258
|
+
## 3. Data-access design
|
|
259
|
+
|
|
260
|
+
**Extend the existing pattern**, don't invent a new one. `src/storage/interface.ts` /
|
|
261
|
+
`sqlite.ts` / `postgres/backend.ts` already do exactly this for the federation/vault data.
|
|
262
|
+
Do the same for the registry and for `kg_entities`:
|
|
263
|
+
|
|
264
|
+
- Add `RegistryBackend` (new file `src/storage/registry-interface.ts`, mirroring
|
|
265
|
+
`interface.ts`) with async methods for every distinct query used across the 64 files in
|
|
266
|
+
§1e: `getProject`, `listProjects`, `createProject`, `updateProject`, `findProjectBySlug`,
|
|
267
|
+
`resolveAlias`, `addAlias`, `removeAlias`, `createSession`, `getSession`, `listSessions`,
|
|
268
|
+
`updateSessionStatus`, `addTag`/`listTags`, `addSessionTag`, `addLink`, `listLinks`,
|
|
269
|
+
`appendCompactionLog`. Enumerating the full method list against every one of the 64 call
|
|
270
|
+
sites is implementation work (unit 3, §5), not a design decision — the shape is the same
|
|
271
|
+
"one interface, one method per operation, both backends implement it" pattern already
|
|
272
|
+
proven by `StorageBackend`.
|
|
273
|
+
- Add `kg_entities` methods (`upsertKgEntity`, `findKgEntity`, `listKgEntities`,
|
|
274
|
+
`updateEntityFeedbackWeight`) to the **existing** `StorageBackend` interface — it already
|
|
275
|
+
owns `kg_triples`-adjacent search and the federation DB; `kg_entities` belongs there, not
|
|
276
|
+
in a third interface. `SQLiteBackend` wraps today's `kg-entity.ts` functions; add a
|
|
277
|
+
`kg_entities` section to `src/storage/postgres/backend.ts` (same file that already has
|
|
278
|
+
`kg_triples` migrations).
|
|
279
|
+
- `src/storage/factory.ts` already returns the right backend for `storageBackend`; add a
|
|
280
|
+
matching `createRegistryBackend()` next to `createStorageBackend()` — same retry/outage
|
|
281
|
+
logic (§ existing `connectPostgres`), same "never silently fall back to SQLite" rule.
|
|
282
|
+
|
|
283
|
+
**Async conversion.** The registry API is 100% synchronous today (`better-sqlite3`). The
|
|
284
|
+
choice is between (a) making `RegistryBackend` async everywhere, converting all ~64 sync
|
|
285
|
+
callers to `await`, or (b) keeping registry sync-only by wrapping a Postgres client behind a
|
|
286
|
+
synchronous facade (not possible with `pg`'s async driver without a blocking bridge, which
|
|
287
|
+
`better-sqlite3`-style sync Postgres clients do not offer in Node without native blocking
|
|
288
|
+
I/O hacks that are worse than the migration itself). Pick (a): **make `RegistryBackend`
|
|
289
|
+
async**, matching `StorageBackend`, for one reason — `StorageBackend` already made this
|
|
290
|
+
choice for exactly the same problem (SQLite is sync, Postgres is not) and every caller of
|
|
291
|
+
`StorageBackend` in this codebase is already `async`/`await`-shaped (MCP tool handlers are
|
|
292
|
+
Promise-returning by SDK contract; daemon dispatcher and scheduler are already async; CLI
|
|
293
|
+
commander actions in this codebase are already declared `async (opts) => {...}` even where
|
|
294
|
+
they currently call sync registry functions inside — converting the inner call to `await`
|
|
295
|
+
is a one-line change per call site, not a structural rewrite). The only category that isn't
|
|
296
|
+
already async is the two shell hooks (§1c, addressed in §6) — everything else is already
|
|
297
|
+
sitting in an async function body waiting for the `await` to be added.
|
|
298
|
+
- Sqlite backend behavior is unchanged: `RegistryBackend`'s SQLite implementation wraps the
|
|
299
|
+
existing sync `db.prepare()` calls in `Promise.resolve(...)` (or plain `async` functions
|
|
300
|
+
that call the sync code) — no behavior change for `storageBackend: sqlite`, satisfying
|
|
301
|
+
"existing behaviour stays."
|
|
302
|
+
- No dual-write, no sync-from-sqlite at runtime — the factory picks one backend per process
|
|
303
|
+
start, exactly like `createStorageBackend()` does today.
|
|
304
|
+
|
|
305
|
+
## 4. One-shot migration command: `pai db migrate-to-postgres`
|
|
306
|
+
|
|
307
|
+
New file `src/cli/commands/db-migrate.ts`, registered under the existing `pai db` command
|
|
308
|
+
group (`src/cli/commands/db.ts`). Idempotent (every write is `ON CONFLICT DO UPDATE`,
|
|
309
|
+
re-running after a partial failure resumes safely — same pattern as `docker/migrate-sqlite.ts`,
|
|
310
|
+
which this command supersedes and should absorb rather than duplicate).
|
|
311
|
+
|
|
312
|
+
Steps, in order, each one gated on the previous succeeding:
|
|
313
|
+
|
|
314
|
+
1. **Pre-flight: bind-mount check.** Before touching any data: confirm
|
|
315
|
+
`docker inspect pai-pgvector --format '{{json .Mounts}}'` reports a `"Type":"bind"` mount
|
|
316
|
+
at `/var/lib/postgresql/data` with `Source` under the host `~/.pai/pgdata` path (not a
|
|
317
|
+
named volume, not absent). Refuse to proceed — exit non-zero, write nothing — if the
|
|
318
|
+
container isn't running, the mount type isn't `bind`, or the source path doesn't match
|
|
319
|
+
config. This is a hard gate: a named-volume or missing-mount Postgres means the "kept
|
|
320
|
+
as a backup" guarantee for the *destination* doesn't hold either.
|
|
321
|
+
2. **Pre-flight: host-path backup (rollback point).** Run `pg_dump` against the target
|
|
322
|
+
database to a host path (e.g. `~/.pai/backups/pai_<user>-pre-migrate-<date>.dump`,
|
|
323
|
+
custom format `-Fc`) *before* any migration write. This is the rollback path: if
|
|
324
|
+
migration verification fails, `pg_restore --clean` from this dump undoes it. Refuse to
|
|
325
|
+
proceed if `pg_dump` fails or produces a zero-byte file.
|
|
326
|
+
3. **Row-count baseline.** `SELECT COUNT(*)` on SQLite `kg_entities` and each registry
|
|
327
|
+
table; record alongside the equivalent Postgres counts (0 or pre-existing, since this
|
|
328
|
+
command is also idempotent against a partially-migrated database).
|
|
329
|
+
4. **Migrate `kg_entities`** (federation.db → Postgres): batch `UPSERT` by `entity_id`,
|
|
330
|
+
same batching/transaction pattern as `docker/migrate-sqlite.ts`'s chunk migration.
|
|
331
|
+
5. **Migrate registry tables**, in FK order: `projects` → `tags` → `sessions` →
|
|
332
|
+
`project_tags` → `session_tags` → `aliases` → `links` → `compaction_log`. `SERIAL` ids on
|
|
333
|
+
the Postgres side must preserve the SQLite integer ids (`INSERT ... (id, ...) VALUES
|
|
334
|
+
(...)` with explicit id, then `SELECT setval(...)` on each sequence afterward) — foreign
|
|
335
|
+
keys throughout the registry and in session/task-resolution code are stored as bare
|
|
336
|
+
integers, not slugs, so silently reassigning ids would break every existing reference.
|
|
337
|
+
6. **Verify.** Re-run the `COUNT(*)` on both sides per table; if source count ≠ destination
|
|
338
|
+
count for anything, **stop, do not rename anything, exit non-zero** with the mismatching
|
|
339
|
+
table names. Add a spot check: for `kg_entities` and `projects`, pull 20 random ids from
|
|
340
|
+
SQLite and confirm every field matches the Postgres row byte-for-byte.
|
|
341
|
+
7. **Stale-data reconciliation** (`memory_files`/`vault_files` — see superset check below):
|
|
342
|
+
report counts; do not silently drop rows that look like real content. Only rows that
|
|
343
|
+
match the indexer's *current* ignore patterns (dotfile/cache directories under a
|
|
344
|
+
registered project root) are treated as cruft and excluded from the "must preserve"
|
|
345
|
+
count — everything else migrates.
|
|
346
|
+
8. **Rename, only after step 6 passes**: `federation.db` → `federation.db.migrated-<date>`,
|
|
347
|
+
`registry.db` → `registry.db.migrated-<date>`. Never `rm`. This is the point after which
|
|
348
|
+
`storageBackend: postgres` is safe to set in config — but this command does **not**
|
|
349
|
+
flip that config value itself; that stays a manual, explicit step for the operator.
|
|
350
|
+
|
|
351
|
+
### Superset check (run 2026-09-22, read-only — SQLite via `better-sqlite3 readonly: true`,
|
|
352
|
+
Postgres via plain `SELECT`)
|
|
353
|
+
|
|
354
|
+
This is **not** a clean superset — flagging the actual numbers rather than the assumption in
|
|
355
|
+
the measured-state note:
|
|
356
|
+
|
|
357
|
+
| Comparison | SQLite | Postgres | SQLite paths absent from Postgres |
|
|
358
|
+
|---|---|---|---|
|
|
359
|
+
| `memory_files` vs `pai_files` (path-only, ignoring `project_id`) | 22,266 distinct paths | 32,722 distinct paths | **19,744** |
|
|
360
|
+
| `vault_files` vs Postgres `vault_files` | 8,254 | 84,645 | **3,371** |
|
|
361
|
+
|
|
362
|
+
Postgres has far more rows overall in both cases (different/additional projects indexed
|
|
363
|
+
since), but SQLite still contains real gaps. Diagnosis for `memory_files`: 2,835 of the
|
|
364
|
+
19,744 missing paths sit under indexer-scope cruft (`.Trash/`, `.bun/install/cache/`,
|
|
365
|
+
`.npm/`, `.cargo/`, `.pyenv/`, other dotfile directories at a project root) — almost
|
|
366
|
+
certainly from before the indexer's ignore patterns were tightened; these are candidates for
|
|
367
|
+
exclusion, not migration. The remaining ~16,900 were not individually triaged in this
|
|
368
|
+
pass — the migration command (step 7) must classify each by running it through the
|
|
369
|
+
*current* indexer ignore-pattern check, not assume. Diagnosis for `vault_files`: the 3,371
|
|
370
|
+
missing paths were spot-checked and are not obviously cruft (real note paths under project
|
|
371
|
+
directories that exist in both systems) — likely genuinely stale (older vault-indexer run,
|
|
372
|
+
not yet re-indexed into Postgres), so these should be included in the migration rather than
|
|
373
|
+
dropped, pending a fresher `pai obsidian sync` pass which may supersede them naturally.
|
|
374
|
+
|
|
375
|
+
## 5. Work breakdown (independent units, parallelizable — no overlapping files)
|
|
376
|
+
|
|
377
|
+
| Unit | Files | Proof |
|
|
378
|
+
|---|---|---|
|
|
379
|
+
| **1. Postgres DDL** | `src/storage/postgres/backend.ts` (`runMigrations()` additions for `kg_entities` + 7 registry tables) | `src/storage/postgres/init-sql.test.ts`-style test: fresh Postgres database, run migrations twice (idempotency), assert all 8 tables + indexes exist via `information_schema`. Before/after: `\dt` table count. |
|
|
380
|
+
| **2. `kg_entities` on `StorageBackend`** | `src/storage/interface.ts` (+4 methods), `src/storage/sqlite.ts` (wrap `kg-entity.ts`), `src/storage/postgres/backend.ts` (+4 methods), `src/memory/kg-entity.ts` callers (`kg-extraction.ts`, `kg-search.ts`, `mcp/tools/feedback.ts`) switched from raw `Database` param to `StorageBackend` param | Vitest: upsert same entity twice, assert `mention_count` increments, on both backends against a temp SQLite file and a temp Postgres database/schema (not the live one). |
|
|
381
|
+
| **3. `RegistryBackend` interface + SQLite impl** | new `src/storage/registry-interface.ts`, `src/storage/registry-sqlite.ts` (wraps existing `registry/db.ts` + `registry/merge.ts` logic); convert `src/cli/program.ts`'s `getDb()` to return a `RegistryBackend` | Vitest against a temp SQLite file: create project, add session, resolve alias — assert round-trip. This is the largest unit (defines the full method surface against all 64 call sites) — do this before unit 4. |
|
|
382
|
+
| **4. `RegistryBackend` Postgres impl** | `src/storage/registry-postgres.ts`, `src/storage/factory.ts` (`createRegistryBackend()`) | Same test suite as unit 3, run against temp Postgres schema — assert identical results to the SQLite impl (shared test file, backend passed as parameter). |
|
|
383
|
+
| **5. Caller conversion (registry)** | the 64 files in §1e — mechanical `db.prepare(...)` → `await registryBackend.method(...)`, one PR-sized slice per subdirectory (`cli/commands/project/*`, `cli/commands/session/*`, `cli/commands/registry/*`, `daemon/*`, `mcp/tools/*`) | Existing command-level tests (`project/here.test.ts`, `project/mcp-tools.test.ts`, `registry/merge.test.ts`, `workers/project-config.test.ts`) pass unmodified against both backends. |
|
|
384
|
+
| **6. Migration command** | `src/cli/commands/db-migrate.ts`, absorbs/retires `docker/migrate-sqlite.ts` | Dry run against a copy of the real `federation.db`/`registry.db` into a scratch Postgres database; assert row counts match and re-run is a no-op (idempotency). |
|
|
385
|
+
| **7. Boundary enforcement test** | new `src/storage/boundary.test.ts` | Scans `src/**/*.ts` excluding `*.test.ts` and everything under `src/storage/`; fails if any file imports `better-sqlite3` or `pg`, imports `openFederation`/`openRegistry`, or contains the literal strings `federation.db`/`registry.db`. Concretely: `readdirSync` walk + regex on file text, `expect(violations).toEqual([])`. This test only passes once units 1–6 are complete — it is the final gate, not something to add early and skip. |
|
|
386
|
+
| **8. Hooks (`session-stop.sh`, `pre-compact.sh`)** | see §6 — needs a decision, not purely mechanical | `pai db registry-touch --session-stop <args>` (or equivalent) exercised manually against both backends; hook script diffed for behavior parity. |
|
|
387
|
+
|
|
388
|
+
Units 1–2 and 3 can start in parallel (disjoint files). Unit 4 depends on 3. Unit 5 depends
|
|
389
|
+
on 3+4. Unit 6 depends on 1–4. Unit 7 depends on everything. Unit 8 can start any time after
|
|
390
|
+
unit 4 (needs the Postgres registry methods to exist) but is independent of unit 5's caller
|
|
391
|
+
sweep.
|
|
392
|
+
|
|
393
|
+
## 6. Risks
|
|
394
|
+
|
|
395
|
+
- **Daemon restart ordering.** The daemon currently opens both `federation.db` and
|
|
396
|
+
`registry.db` at startup (`server.ts`, `dispatcher.ts`). Under Postgres, the daemon's
|
|
397
|
+
`waitForPostgres: true` retry loop already exists for the federation backend
|
|
398
|
+
(`src/storage/factory.ts`) — extend the same loop to gate registry backend creation too,
|
|
399
|
+
so the daemon doesn't start serving with a `RegistryBackend` pointed at a not-yet-ready
|
|
400
|
+
Postgres. Boot ordering (Docker Desktop before launchd-started daemon) is the same
|
|
401
|
+
pre-existing race the current retry logic already handles; no new mechanism needed, just
|
|
402
|
+
applying the existing one to a second backend.
|
|
403
|
+
- **Concurrent writers during migration.** `federation.db` is called out as "ONLY copy,
|
|
404
|
+
still being written live" for `kg_entities` — the daemon and any running session are
|
|
405
|
+
actively upserting entities. The migration command must require the daemon stopped (`pai
|
|
406
|
+
daemon stop`) before running, and check for it (refuse to proceed if `pai daemon status`
|
|
407
|
+
reports running), otherwise rows written during the row-count baseline (step 3) are
|
|
408
|
+
invisible to the migration but land in SQLite after — a source of silent data loss the
|
|
409
|
+
verification step (step 6) would not catch, since it re-reads SQLite counts fresh but a
|
|
410
|
+
row written after step 4's read and before step 8's rename would never be copied.
|
|
411
|
+
- **Tests that assume SQLite.** `project/here.test.ts` and `project/mcp-tools.test.ts`
|
|
412
|
+
construct `new Database(":memory:")` directly — these become tests of the SQLite
|
|
413
|
+
`RegistryBackend` implementation (unit 3), not of raw `better-sqlite3` usage in the
|
|
414
|
+
caller. `workers/project-config.test.ts` similarly. All three need their setup swapped to
|
|
415
|
+
go through the new interface rather than instantiating `better-sqlite3` inline — small,
|
|
416
|
+
mechanical, but must happen in lockstep with unit 3/5, not after, or they start failing
|
|
417
|
+
the boundary test (unit 7) as soon as it's added.
|
|
418
|
+
- **Shell hooks (§1c) have no Postgres client.** `sqlite3` is a CLI binary; there is no
|
|
419
|
+
equivalent zero-dependency `psql`-in-bash story without assuming `psql` is installed
|
|
420
|
+
(it isn't a stated PAI dependency today). Two options, not resolved here: (a) require
|
|
421
|
+
`psql` as a new host dependency for `storageBackend: postgres` deployments, or (b) add a
|
|
422
|
+
tiny `pai` subcommand the hooks shell out to (`pai hooks session-stop-write ...`), paying
|
|
423
|
+
a Node startup cost on every session-stop/pre-compact event. (b) is more consistent with
|
|
424
|
+
"no file outside `src/storage/` touches the database directly" and avoids a new external
|
|
425
|
+
dependency — recommended, but flagged as a decision for whoever picks up unit 8, not
|
|
426
|
+
decided unilaterally here.
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
## 8. Decisions and finish sequence (owner-approved, 2026-09-22)
|
|
430
|
+
|
|
431
|
+
- Unit 8 decision: option (b). The two shell hooks shell out to a `pai` subcommand; no file outside `src/storage/` touches a database.
|
|
432
|
+
- Finish sequence, in order, nothing skipped:
|
|
433
|
+
1. Stop `com.pai.pai-daemon` and `com.pai.task-scheduler` (launchd) so nothing writes SQLite or Postgres during the copy. Check `com.pai.claudaemon` and the per-session `dist/daemon-mcp` shims for SQLite access first.
|
|
434
|
+
2. Preflight: `pai-pgvector` bind mount `~/.pai/pgdata -> /var/lib/postgresql/data` present; `pg_dump` of the configured database to a host path.
|
|
435
|
+
3. `pai db migrate-to-postgres`: kg_entities, all registry tables, and the SQLite-only memory_files/memory_chunks (19,744 paths) and vault rows (3,371 paths) absent from Postgres. Refuse on any count mismatch.
|
|
436
|
+
4. Verify: per-table counts (SQLite rows == Postgres rows for migrated sets, Postgres superset for memory/vault paths), spot checks, sequences setval'd.
|
|
437
|
+
5. Cleanup: delete `~/.claude/pai/federation.db` (+ -wal/-shm) and `~/.claude/pai/registry.db` only after step 4 passes. Rollback = the step-2 pg_dump plus the CCC backup volume copy.
|
|
438
|
+
6. Restart `com.pai.pai-daemon`, `com.pai.task-scheduler` (and `claudaemon` if it used SQLite); confirm the daemon log shows the Postgres backend and no SQLite file is recreated (`ls ~/.claude/pai/*.db` stays empty after a full scheduler cycle). Open sessions pick up the new MCP shim on their next restart.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Registry method map — unit 3 → unit 5
|
|
2
|
+
|
|
3
|
+
`RegistryBackend` (`src/storage/registry-interface.ts`) enumerates every distinct
|
|
4
|
+
registry query used by the 64 caller files inventoried in `postgres-only.md` §1e.
|
|
5
|
+
This table is the mechanical checklist for unit 5: for each file, the methods it
|
|
6
|
+
will call once its `db.prepare(...)` calls are replaced with
|
|
7
|
+
`await registryBackend.method(...)`.
|
|
8
|
+
|
|
9
|
+
Method naming matches `registry-interface.ts` exactly. A file listed with
|
|
10
|
+
"(no direct registry access)" either takes an already-resolved row as a
|
|
11
|
+
parameter or only touches `federation.db` — out of scope for the registry
|
|
12
|
+
conversion.
|
|
13
|
+
|
|
14
|
+
## Method list (82 methods)
|
|
15
|
+
|
|
16
|
+
**Projects — reads:** `getProjectById`, `getProjectBySlug`, `getProjectByAlias`,
|
|
17
|
+
`getProjectByRootPath`, `getProjectByEncodedDir`, `findProjectByCwdPrefix`,
|
|
18
|
+
`listProjects`, `listProjectsByPathLengthDesc`, `listProjectsWithSessionStats`,
|
|
19
|
+
`listNamedProjects`, `searchProjects`, `countProjects`,
|
|
20
|
+
`getMostRecentProjectUpdatedAt`, `countChildProjects`,
|
|
21
|
+
`findSiblingProjectsBySlugPattern`
|
|
22
|
+
|
|
23
|
+
**Projects — writes:** `createProject`, `createProjectWithSlugRetry`,
|
|
24
|
+
`updateProjectPath`, `updateProjectStatus`, `updateProjectDisplayName`,
|
|
25
|
+
`updateProjectType`, `updateProjectSessionConfig`, `updateProjectClaudeNotesDir`,
|
|
26
|
+
`updateProjectObsidianLink`, `updateProjectSlug`, `reassignProjectParent`,
|
|
27
|
+
`deleteProject`, `deleteProjectCascade`
|
|
28
|
+
|
|
29
|
+
**Tags:** `listTagsForProject`, `listAllTags`, `upsertTag`, `addProjectTag`,
|
|
30
|
+
`projectHasTag`, `deleteProjectTag`, `reassignProjectTag`, `listProjectTagIds`,
|
|
31
|
+
`countProjectTagsForProject`, `copyProjectTags`, `deleteProjectTagsForProject`
|
|
32
|
+
|
|
33
|
+
**Aliases:** `resolveAlias`, `listAliasesForProject`, `addAlias`, `removeAlias`,
|
|
34
|
+
`countAliasesForProject`, `moveProjectAliases`, `reassignAlias`, `listAliasMap`
|
|
35
|
+
|
|
36
|
+
**Sessions:** `getSessionById`, `getSessionByNumber`, `getLatestSessionForProject`,
|
|
37
|
+
`getMaxSessionNumber`, `listSessionsForProject`, `listSessions`,
|
|
38
|
+
`findSessionByFilename`, `sessionNumberTaken`, `createSession`,
|
|
39
|
+
`upsertSessionIfAbsent`, `updateSessionMeta`, `updateSessionNumber`,
|
|
40
|
+
`updateSessionFilename`, `moveSessionToProject`, `deleteSession`,
|
|
41
|
+
`backfillFoldedSession`, `countSessionsForProject`, `getMostRecentSessionDate`,
|
|
42
|
+
`getMostRecentSessionCreatedAt`
|
|
43
|
+
|
|
44
|
+
**Session tags:** `listSessionTagIds`, `sessionHasTag`, `addSessionTag`,
|
|
45
|
+
`deleteSessionTag`, `reassignSessionTag`, `listTagsForSession`
|
|
46
|
+
|
|
47
|
+
**Links:** `addLink`, `listLinksForSession`, `listLinksForProject`, `linkExists`,
|
|
48
|
+
`deleteLink`, `deleteLinksTargetingProject`, `deleteLinksFromProjectSessions`,
|
|
49
|
+
`retargetLink`, `moveLinkToSession`, `reassignLinksTarget`,
|
|
50
|
+
`countLinksForProject`, `deleteSelfLinksForProject`
|
|
51
|
+
|
|
52
|
+
**Compaction log:** `appendCompactionLog`, `countCompactionLogsForProject`,
|
|
53
|
+
`moveCompactionLogsByProject`, `moveCompactionLogsBySession`,
|
|
54
|
+
`deleteCompactionLogsForProject`
|
|
55
|
+
|
|
56
|
+
**Merge:** `planProjectMerge`, `applyProjectMerge` (wrap `src/registry/merge.ts`
|
|
57
|
+
unchanged)
|
|
58
|
+
|
|
59
|
+
## Per-caller mapping
|
|
60
|
+
|
|
61
|
+
### CLI project commands
|
|
62
|
+
|
|
63
|
+
| File | Methods |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `src/cli/commands/project/index.ts` | (no direct registry access) |
|
|
66
|
+
| `src/cli/commands/project/commands.ts` | `getProjectBySlug`(conflict check), `listProjects`, `createProject`, `listProjectsWithSessionStats`, `countProjects`, `listSessionsForProject`, `updateProjectStatus`, `updateProjectPath`, `projectHasTag`, `addProjectTag`, `getProjectBySlug`(alias conflict), `addAlias`, `updateProjectDisplayName`, `updateProjectType`, `getProjectByEncodedDir` |
|
|
67
|
+
| `src/cli/commands/project/here.ts` | `listProjects`, `getProjectByEncodedDir`, `updateProjectPath`, `getProjectBySlug`, `createProject` |
|
|
68
|
+
| `src/cli/commands/project/helpers.ts` | `getProjectBySlug`, `getProjectByAlias`, `listProjects`, `listTagsForProject`, `listAliasesForProject`, `countSessionsForProject`, `getMostRecentSessionDate`, `upsertTag` |
|
|
69
|
+
| `src/cli/commands/project/health.ts` | `listProjectsWithSessionStats`, `updateProjectPath`, `updateProjectStatus` |
|
|
70
|
+
| `src/cli/commands/project/merge.ts` | `planProjectMerge`, `getProjectById`, `applyProjectMerge` |
|
|
71
|
+
| `src/cli/commands/project/unregister.ts` | `getProjectBySlug`+`countSessionsForProject` (or a dedicated read), `deleteProjectCascade` |
|
|
72
|
+
| `src/cli/commands/project/projects-index.ts` | delegates to `main-resolver.ts`/`helpers.ts`; direct: `updateProjectPath` |
|
|
73
|
+
| `src/cli/commands/project/session-config.ts` | `updateProjectSessionConfig`, `getProjectBySlug`(conflict), `resolveAlias`, `addAlias`, `removeAlias`, `countAliasesForProject`, `listNamedProjects` |
|
|
74
|
+
| `src/cli/commands/main-resolver.ts` | `listProjectsWithSessionStats` |
|
|
75
|
+
| `src/cli/commands/pick.ts` | `listProjectsWithSessionStats`, `updateProjectStatus` (resolve slug→id first) |
|
|
76
|
+
| `src/cli/commands/detect.ts` | `listProjectsByPathLengthDesc`, `countSessionsForProject`+`getMostRecentSessionDate` (or new combined read) |
|
|
77
|
+
|
|
78
|
+
### CLI session commands
|
|
79
|
+
|
|
80
|
+
| File | Methods |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `src/cli/commands/session/index.ts` | (no direct registry access) |
|
|
83
|
+
| `src/cli/commands/session/commands.ts` | `listSessions`, `updateSessionMeta`, `sessionHasTag`, `addSessionTag`, `getProjectBySlug`, `addLink`, `getProjectByEncodedDir` |
|
|
84
|
+
| `src/cli/commands/session/autosave.ts` | `getLatestSessionForProject` |
|
|
85
|
+
| `src/cli/commands/session/pause.ts` | `findProjectByCwdPrefix`, `getLatestSessionForProject`, `findSiblingProjectsBySlugPattern` |
|
|
86
|
+
| `src/cli/commands/session/handover.ts` | `getProjectBySlug`, `getLatestSessionForProject`, `getSessionByNumber` |
|
|
87
|
+
| `src/cli/commands/session/goto.ts` | via `session-scan.ts`: `listProjects` |
|
|
88
|
+
| `src/cli/commands/session/end.ts` | `findProjectByCwdPrefix` |
|
|
89
|
+
| `src/cli/commands/session/helpers.ts` | `getProjectBySlug`, `getLatestSessionForProject`, `getSessionByNumber`, `upsertTag`, `listTagsForSession` |
|
|
90
|
+
| `src/cli/commands/session/sessions-index.ts` | (no direct registry access) |
|
|
91
|
+
| `src/cli/commands/session-cleanup/index.ts` | via `scanner.ts`: `getProjectBySlug`, `listProjects` |
|
|
92
|
+
| `src/cli/commands/session-cleanup/scanner.ts` | `listProjects`, `getProjectBySlug`, `listSessionsForProject` |
|
|
93
|
+
| `src/cli/commands/session-cleanup/executor.ts` | `deleteSession`, `updateSessionMeta`, `updateSessionNumber`, `updateSessionFilename` |
|
|
94
|
+
| `src/cli/lib/session-scan.ts` | `listProjects` |
|
|
95
|
+
| `src/cli/lib/dedup-sessions.ts` | (no direct registry access) |
|
|
96
|
+
|
|
97
|
+
### CLI registry commands + registry/merge.ts + registry/migrate.ts
|
|
98
|
+
|
|
99
|
+
| File | Methods |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `src/cli/commands/registry/index.ts` | `countProjects`, `countSessionsForProject`(overall — new use of general count with no filter is `SELECT COUNT(*) FROM sessions`, map to a plain count call), `listAllTags`(count), `getMostRecentProjectUpdatedAt`, `getMostRecentSessionCreatedAt`, full wipe (`db.exec` of 8 DELETEs — stays a raw backend-level `resetRegistry()` composite if unit 5 needs it, not yet in the interface — flag for unit 5), `getProjectByRootPath` |
|
|
102
|
+
| `src/cli/commands/registry/scan.ts` | `updateProjectClaudeNotesDir`, `listProjects`, `getProjectByRootPath`, `getProjectBySlug`, `getProjectByEncodedDir`, `updateProjectPath`, `listProjects`(display_name=slug legacy — filter client-side), `updateProjectDisplayName`; plus `createProjectWithSlugRetry`/`upsertProjectByPath`-shaped calls via `utils.ts` |
|
|
103
|
+
| `src/cli/commands/registry/reconnect.ts` | `listProjectsWithSessionStats`, `updateProjectPath`(encoded_dir only) |
|
|
104
|
+
| `src/cli/commands/registry/dedupe.ts` | `countSessionsForProject`, `countCompactionLogsForProject`, `countAliasesForProject`, `countLinksForProject`, `countProjectTagsForProject`, `countChildProjects`, `listProjects`(all columns, order id), `listSessionTagIds`, `sessionHasTag`, `deleteSessionTag`, `reassignSessionTag`, `listLinksForSession`, `linkExists`, `deleteLink`, `moveLinkToSession`, `moveCompactionLogsBySession`, `backfillFoldedSession`, `deleteSession`, `getMaxSessionNumber`, `listSessionsForProject`, `findSessionByFilename`, `sessionNumberTaken`, `moveSessionToProject`, `moveCompactionLogsByProject`, `listAliasesForProject`, `resolveAlias`, `reassignAlias`, `listLinksForProject`, `deleteLink`, `retargetLink`, `listProjectTagIds`, `projectHasTag`, `deleteProjectTag`, `reassignProjectTag`, `reassignProjectParent`, `updateProjectClaudeNotesDir`, `updateProjectSessionConfig`, `deleteProject`, `updateProjectStatus`, `getProjectBySlug`(candidate check), `updateProjectSlug`, `getProjectByRootPath`, `updateProjectPath`, `listProjects` |
|
|
105
|
+
| `src/cli/commands/registry/migrate.ts` | via `utils.ts` upsert helpers |
|
|
106
|
+
| `src/cli/commands/registry/utils.ts` | `getProjectByEncodedDir`, `getProjectByRootPath`, `updateProjectPath`, `createProjectWithSlugRetry`, `getSessionByNumber`(existence via `sessionNumberTaken`), `createSession` |
|
|
107
|
+
| `src/registry/merge.ts` | internals of `planProjectMerge`/`applyProjectMerge` — not converted; called by `registry-sqlite.ts` directly |
|
|
108
|
+
| `src/registry/migrate.ts` | `createProjectWithSlugRetry`(slug-retry insert), `getProjectBySlug`, `getProjectByRootPath`, `upsertSessionIfAbsent` |
|
|
109
|
+
|
|
110
|
+
### CLI memory/obsidian/backup/config
|
|
111
|
+
|
|
112
|
+
| File | Methods |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `src/cli/commands/memory/stats.ts` | `getProjectBySlug`, `getProjectById`(batch — loop or new `getProjectsByIds`, flag for unit 5) |
|
|
115
|
+
| `src/cli/commands/memory/search.ts` | `getProjectBySlug` |
|
|
116
|
+
| `src/cli/commands/memory/index-cmd.ts` | `getProjectBySlug`(status filter via opts) |
|
|
117
|
+
| `src/cli/commands/memory/embed.ts` | `getProjectBySlug` |
|
|
118
|
+
| `src/cli/commands/backup.ts` | (no direct registry access) |
|
|
119
|
+
| `src/cli/commands/restore.ts` | (no direct registry access) |
|
|
120
|
+
| `src/cli/commands/config.ts` | (no direct registry access) |
|
|
121
|
+
| `src/cli/commands/obsidian.ts` | (no direct registry access — delegates) |
|
|
122
|
+
| `src/obsidian/sync/generate.ts` | `listProjectsWithSessionStats`, `listTagsForProject`, `listAllTags`, `listProjectsWithSessionStats`(tagId) |
|
|
123
|
+
| `src/obsidian/sync/master.ts` | `listProjects`(status active, order slug) ×2 |
|
|
124
|
+
| `src/obsidian/sync/symlinks.ts` | `listProjects`(order status,slug), `updateProjectObsidianLink` |
|
|
125
|
+
| `src/obsidian/status.ts` | `listProjects`(status active) |
|
|
126
|
+
| `src/obsidian/vault-fixer.ts` | (no direct registry access — federation.db only) |
|
|
127
|
+
|
|
128
|
+
### MCP tools
|
|
129
|
+
|
|
130
|
+
| File | Methods |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `src/mcp/tools/registry.ts` | `searchProjects` |
|
|
133
|
+
| `src/mcp/tools/projects.ts` | `getProjectById`, `listProjects`(status/tag/limit), `listProjectsWithSessionStats` |
|
|
134
|
+
| `src/mcp/tools/sessions.ts` | `listSessions`(projectId/status/limit, no join needed — trim columns client-side) |
|
|
135
|
+
| `src/mcp/tools/wakeup.ts` | `getProjectBySlug` |
|
|
136
|
+
| `src/mcp/tools/types.ts` | `getProjectBySlug`(id lookup), `resolveAlias`, `getProjectByRootPath`, `listProjectsByPathLengthDesc`, `countSessionsForProject`, `getMostRecentSessionDate`, `listTagsForProject`, `listAliasesForProject` |
|
|
137
|
+
|
|
138
|
+
### Daemon
|
|
139
|
+
|
|
140
|
+
| File | Methods |
|
|
141
|
+
|---|---|
|
|
142
|
+
| `src/daemon/daemon/dispatcher.ts` | (no direct registry access) |
|
|
143
|
+
| `src/daemon/daemon/handler.ts` | `countProjects`, `findProjectByCwdPrefix`, `getProjectBySlug` |
|
|
144
|
+
| `src/daemon/daemon/scheduler.ts` | `getProjectByRootPath`, `listProjects`(status active) |
|
|
145
|
+
| `src/daemon/daemon/server.ts` | `openRegistry`/`getRegistryBackend` only (no table queries) |
|
|
146
|
+
| `src/daemon/daemon/state.ts` | `openRegistry`/`getRegistryBackend` only |
|
|
147
|
+
| `src/daemon/daemon/types.ts` | (no direct registry access) |
|
|
148
|
+
| `src/daemon/session-summary-worker.ts` | `getProjectBySlug` |
|
|
149
|
+
|
|
150
|
+
### Session/task/topic routing + workers
|
|
151
|
+
|
|
152
|
+
| File | Methods |
|
|
153
|
+
|---|---|
|
|
154
|
+
| `src/session/auto-route.ts` | `getProjectBySlug`(status filter) ×2 |
|
|
155
|
+
| `src/session/promote.ts` | `getProjectBySlug`+`getProjectByRootPath`+`getProjectByEncodedDir`(conflict check), `createProject` |
|
|
156
|
+
| `src/tasks/resolver.ts` | `listAliasMap` |
|
|
157
|
+
| `src/topics/detector.ts` | (no direct registry access — takes `StorageBackend`) |
|
|
158
|
+
| `src/workers/project-config.ts` | `listProjectsByPathLengthDesc`(excludeArchived) |
|
|
159
|
+
|
|
160
|
+
## Gaps found during mapping (flag for unit 5, not fixed here)
|
|
161
|
+
|
|
162
|
+
- `registry/index.ts`'s full-wipe (`db.exec` of 8 `DELETE`s for `pai registry
|
|
163
|
+
rebuild`) has no single interface method yet — it is a rare, deliberately
|
|
164
|
+
destructive whole-table operation and does not belong next to the granular
|
|
165
|
+
CRUD methods above. Unit 5 should add one explicit `resetRegistry()` method
|
|
166
|
+
when it converts that file, not before.
|
|
167
|
+
- `memory/stats.ts`'s batch "project ids → slugs" lookup (`WHERE id IN (...)`)
|
|
168
|
+
is not in the interface; unit 5 can loop `getProjectById` (small N — global
|
|
169
|
+
stats span a handful of projects) or add a batch method if profiling shows
|
|
170
|
+
it matters.
|
|
171
|
+
- `unregister.ts` and `dedupe.ts` currently read `session_count` via a
|
|
172
|
+
correlated subquery in the same `SELECT`; the mapping above splits that into
|
|
173
|
+
a separate `countSessionsForProject` call. Behaviourally identical, one more
|
|
174
|
+
round trip per call site.
|