@tekmidian/pai 0.54.1 → 0.56.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-CZKlI0hq.mjs → chain-26f7uGUs.mjs} +4 -4
- package/dist/{chain-CZKlI0hq.mjs.map → chain-26f7uGUs.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-BOWol-LF.mjs +19 -0
- package/dist/{daemon-aJTxzg20.mjs → daemon-BsuM0UdV.mjs} +97 -466
- package/dist/daemon-BsuM0UdV.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-CCFIpPha.mjs → fallback-CVe3sb94.mjs} +192 -134
- package/dist/fallback-CVe3sb94.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-D3keWys5.mjs → planner-ye7wrrfB.mjs} +7 -7
- package/dist/{planner-D3keWys5.mjs.map → planner-ye7wrrfB.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-B3eRMhff.mjs → program-DuC1RcaO.mjs} +2268 -1402
- package/dist/program-DuC1RcaO.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-DLG25bzt.mjs → run-CqRvXGjd.mjs} +309 -49
- package/dist/run-CqRvXGjd.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-DklS8VmU.mjs → session-keepalive-BQ6AQ9ez.mjs} +7 -7
- package/dist/{session-keepalive-DklS8VmU.mjs.map → session-keepalive-BQ6AQ9ez.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-CknQ6YGZ.mjs +0 -25
- package/dist/daemon-aJTxzg20.mjs.map +0 -1
- 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-CCFIpPha.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-B3eRMhff.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-DLG25bzt.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
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { d as sha256 } from "./helpers-D9WkIf9R.mjs";
|
|
2
|
+
import { t as STOP_WORDS } from "./stop-words-ClCwO8mZ.mjs";
|
|
3
|
+
import { n as indexAllWithBackend } from "./async-CLvuY7b-.mjs";
|
|
4
|
+
import { t as entityContentId } from "./kg-entity-Cj8Q6YmU.mjs";
|
|
2
5
|
import { readFileSync } from "node:fs";
|
|
3
6
|
import { dirname, join } from "node:path";
|
|
4
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -505,6 +508,574 @@ async function searchVaultNameIndex(pool, query, limit = 100) {
|
|
|
505
508
|
return (await pool.query("SELECT DISTINCT vault_path FROM vault_name_index WHERE lower(name) LIKE lower($1) LIMIT $2", [`%${query}%`, limit])).rows.map((row) => row.vault_path);
|
|
506
509
|
}
|
|
507
510
|
|
|
511
|
+
//#endregion
|
|
512
|
+
//#region src/storage/postgres/tunnels.ts
|
|
513
|
+
/**
|
|
514
|
+
* Postgres implementation of cross-project concept "tunnel" detection —
|
|
515
|
+
* moved from memory/tunnels.ts alongside its SQLite counterpart
|
|
516
|
+
* (storage/sqlite/tunnels.ts) so raw SQL against the federation database
|
|
517
|
+
* lives entirely under src/storage/.
|
|
518
|
+
*
|
|
519
|
+
* Uses ts_stat() + plainto_tsquery to efficiently find terms that appear
|
|
520
|
+
* across multiple projects.
|
|
521
|
+
*/
|
|
522
|
+
async function findTunnelsForSlugMap(pool, slugMap, opts) {
|
|
523
|
+
const projectIds = Object.keys(slugMap).map(Number);
|
|
524
|
+
if (projectIds.length < 2) return {
|
|
525
|
+
tunnels: [],
|
|
526
|
+
projects_analyzed: projectIds.length,
|
|
527
|
+
total_concepts_evaluated: 0
|
|
528
|
+
};
|
|
529
|
+
let candidateTerms = (await pool.query(`SELECT word, ndoc, nentry
|
|
530
|
+
FROM ts_stat(
|
|
531
|
+
'SELECT to_tsvector(''simple'', text) FROM memory_chunks WHERE project_id = ANY($1)'
|
|
532
|
+
)
|
|
533
|
+
WHERE length(word) >= 3
|
|
534
|
+
AND nentry >= $2
|
|
535
|
+
ORDER BY ndoc DESC
|
|
536
|
+
LIMIT 500`, [projectIds, opts.min_occurrences])).rows.map((r) => r.word).filter((t) => !STOP_WORDS.has(t));
|
|
537
|
+
if (candidateTerms.length === 0) return {
|
|
538
|
+
tunnels: [],
|
|
539
|
+
projects_analyzed: projectIds.length,
|
|
540
|
+
total_concepts_evaluated: 0
|
|
541
|
+
};
|
|
542
|
+
candidateTerms = candidateTerms.slice(0, 200);
|
|
543
|
+
const valuesClause = candidateTerms.map((t, i) => `($${i + 2}::text)`).join(", ");
|
|
544
|
+
const batchResult = await pool.query(`SELECT v.concept, c.project_id::text, COUNT(*) AS cnt,
|
|
545
|
+
MIN(c.updated_at) AS first_seen,
|
|
546
|
+
MAX(c.updated_at) AS last_seen
|
|
547
|
+
FROM (VALUES ${valuesClause}) AS v(concept)
|
|
548
|
+
JOIN memory_chunks c
|
|
549
|
+
ON to_tsvector('simple', c.text) @@ plainto_tsquery('simple', v.concept)
|
|
550
|
+
AND c.project_id = ANY($1)
|
|
551
|
+
GROUP BY v.concept, c.project_id`, [projectIds, ...candidateTerms]);
|
|
552
|
+
const byConceptMap = /* @__PURE__ */ new Map();
|
|
553
|
+
for (const row of batchResult.rows) {
|
|
554
|
+
const existing = byConceptMap.get(row.concept) ?? {
|
|
555
|
+
projects: /* @__PURE__ */ new Set(),
|
|
556
|
+
occurrences: 0,
|
|
557
|
+
firstSeen: Infinity,
|
|
558
|
+
lastSeen: -Infinity
|
|
559
|
+
};
|
|
560
|
+
existing.projects.add(parseInt(row.project_id, 10));
|
|
561
|
+
existing.occurrences += parseInt(row.cnt, 10);
|
|
562
|
+
const fs = parseInt(row.first_seen, 10);
|
|
563
|
+
const ls = parseInt(row.last_seen, 10);
|
|
564
|
+
if (fs < existing.firstSeen) existing.firstSeen = fs;
|
|
565
|
+
if (ls > existing.lastSeen) existing.lastSeen = ls;
|
|
566
|
+
byConceptMap.set(row.concept, existing);
|
|
567
|
+
}
|
|
568
|
+
const tunnels = [];
|
|
569
|
+
for (const [concept, data] of byConceptMap) {
|
|
570
|
+
if (data.projects.size < opts.min_projects) continue;
|
|
571
|
+
if (data.occurrences < opts.min_occurrences) continue;
|
|
572
|
+
const projects = [...data.projects].map((id) => slugMap[id] ?? String(id)).filter(Boolean);
|
|
573
|
+
tunnels.push({
|
|
574
|
+
concept,
|
|
575
|
+
projects,
|
|
576
|
+
occurrences: data.occurrences,
|
|
577
|
+
first_seen: data.firstSeen === Infinity ? 0 : data.firstSeen,
|
|
578
|
+
last_seen: data.lastSeen === -Infinity ? 0 : data.lastSeen
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
tunnels.sort((a, b) => {
|
|
582
|
+
const byProjects = b.projects.length - a.projects.length;
|
|
583
|
+
if (byProjects !== 0) return byProjects;
|
|
584
|
+
return b.occurrences - a.occurrences;
|
|
585
|
+
});
|
|
586
|
+
return {
|
|
587
|
+
tunnels: tunnels.slice(0, opts.limit),
|
|
588
|
+
projects_analyzed: projectIds.length,
|
|
589
|
+
total_concepts_evaluated: candidateTerms.length
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
/** Find cross-project concept tunnels against a Postgres federation database. */
|
|
593
|
+
async function findTunnelsPostgres(pool, registry, options) {
|
|
594
|
+
const opts = {
|
|
595
|
+
min_projects: options?.min_projects ?? 2,
|
|
596
|
+
min_occurrences: options?.min_occurrences ?? 3,
|
|
597
|
+
limit: options?.limit ?? 20
|
|
598
|
+
};
|
|
599
|
+
const projectRows = await registry.listProjects({ excludeArchived: true });
|
|
600
|
+
const slugMap = {};
|
|
601
|
+
for (const { id, slug } of projectRows) slugMap[id] = slug;
|
|
602
|
+
return findTunnelsForSlugMap(pool, slugMap, opts);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
//#endregion
|
|
606
|
+
//#region src/storage/postgres/sources.ts
|
|
607
|
+
const num = (v) => Number(v ?? 0);
|
|
608
|
+
async function getMemorySourcesReportPostgres(pool) {
|
|
609
|
+
return {
|
|
610
|
+
composition: (await pool.query(`SELECT source, tier, COUNT(*) AS chunks, COUNT(embedding) AS embedded
|
|
611
|
+
FROM pai_chunks GROUP BY source, tier ORDER BY COUNT(*) DESC`)).rows.map((r) => ({
|
|
612
|
+
source: r.source,
|
|
613
|
+
tier: r.tier,
|
|
614
|
+
chunks: num(r.chunks),
|
|
615
|
+
embedded: num(r.embedded)
|
|
616
|
+
})),
|
|
617
|
+
paths: (await pool.query(`SELECT path, COUNT(*) AS chunks FROM pai_chunks GROUP BY path`)).rows.map((r) => ({
|
|
618
|
+
path: r.path,
|
|
619
|
+
chunks: num(r.chunks)
|
|
620
|
+
})),
|
|
621
|
+
churn: (await pool.query(`SELECT to_char(to_timestamp(updated_at/1000),'YYYY-MM-DD') AS day,
|
|
622
|
+
COUNT(*) AS chunks, COUNT(embedding) AS embedded
|
|
623
|
+
FROM pai_chunks GROUP BY day ORDER BY day DESC LIMIT 10`)).rows.map((r) => ({
|
|
624
|
+
day: r.day,
|
|
625
|
+
chunks: num(r.chunks),
|
|
626
|
+
embedded: num(r.embedded)
|
|
627
|
+
}))
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
//#endregion
|
|
632
|
+
//#region src/storage/postgres/kg-triples.ts
|
|
633
|
+
function rowToTriple(row) {
|
|
634
|
+
return {
|
|
635
|
+
id: row.id,
|
|
636
|
+
subject: row.subject,
|
|
637
|
+
predicate: row.predicate,
|
|
638
|
+
object: row.object,
|
|
639
|
+
project_id: row.project_id,
|
|
640
|
+
source_session: row.source_session,
|
|
641
|
+
valid_from: new Date(row.valid_from),
|
|
642
|
+
valid_to: row.valid_to ? new Date(row.valid_to) : void 0,
|
|
643
|
+
confidence: row.confidence,
|
|
644
|
+
created_at: new Date(row.created_at)
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
async function addKgTriple(pool, params) {
|
|
648
|
+
const confidence = params.confidence ?? "EXTRACTED";
|
|
649
|
+
return rowToTriple((await pool.query(`INSERT INTO kg_triples
|
|
650
|
+
(subject, predicate, object, project_id, source_session, confidence)
|
|
651
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
|
652
|
+
RETURNING *`, [
|
|
653
|
+
params.subject,
|
|
654
|
+
params.predicate,
|
|
655
|
+
params.object,
|
|
656
|
+
params.project_id ?? null,
|
|
657
|
+
params.source_session ?? null,
|
|
658
|
+
confidence
|
|
659
|
+
])).rows[0]);
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Query triples by subject, predicate, object, and/or project.
|
|
663
|
+
* Supports point-in-time queries via as_of.
|
|
664
|
+
* By default only returns currently-valid triples (valid_to IS NULL).
|
|
665
|
+
*/
|
|
666
|
+
async function queryKgTriples(pool, params) {
|
|
667
|
+
const conditions = [];
|
|
668
|
+
const values = [];
|
|
669
|
+
let idx = 1;
|
|
670
|
+
if (params.subject !== void 0) {
|
|
671
|
+
conditions.push(`subject = $${idx++}`);
|
|
672
|
+
values.push(params.subject);
|
|
673
|
+
}
|
|
674
|
+
if (params.predicate !== void 0) {
|
|
675
|
+
conditions.push(`predicate = $${idx++}`);
|
|
676
|
+
values.push(params.predicate);
|
|
677
|
+
}
|
|
678
|
+
if (params.object !== void 0) {
|
|
679
|
+
conditions.push(`object = $${idx++}`);
|
|
680
|
+
values.push(params.object);
|
|
681
|
+
}
|
|
682
|
+
if (params.project_id !== void 0) {
|
|
683
|
+
conditions.push(`project_id = $${idx++}`);
|
|
684
|
+
values.push(params.project_id);
|
|
685
|
+
}
|
|
686
|
+
if (params.as_of !== void 0) {
|
|
687
|
+
conditions.push(`valid_from <= $${idx++}`);
|
|
688
|
+
values.push(params.as_of);
|
|
689
|
+
conditions.push(`(valid_to IS NULL OR valid_to > $${idx++})`);
|
|
690
|
+
values.push(params.as_of);
|
|
691
|
+
} else if (!params.include_invalidated) conditions.push(`valid_to IS NULL`);
|
|
692
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
693
|
+
return (await pool.query(`SELECT * FROM kg_triples ${where} ORDER BY valid_from DESC`, values)).rows.map(rowToTriple);
|
|
694
|
+
}
|
|
695
|
+
/** Invalidate a triple by setting valid_to = NOW(). Does not delete the row — preserves history. */
|
|
696
|
+
async function invalidateKgTriple(pool, tripleId) {
|
|
697
|
+
await pool.query(`UPDATE kg_triples SET valid_to = NOW() WHERE id = $1 AND valid_to IS NULL`, [tripleId]);
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Find contradictions: cases where the same (subject, predicate) pair has
|
|
701
|
+
* multiple currently-valid objects.
|
|
702
|
+
*/
|
|
703
|
+
async function getKgContradictions(pool, subject) {
|
|
704
|
+
return (await pool.query(`SELECT subject, predicate, array_agg(object ORDER BY object) AS objects
|
|
705
|
+
FROM kg_triples
|
|
706
|
+
WHERE subject = $1
|
|
707
|
+
AND valid_to IS NULL
|
|
708
|
+
GROUP BY subject, predicate
|
|
709
|
+
HAVING COUNT(*) > 1`, [subject])).rows.map((row) => ({
|
|
710
|
+
subject: row.subject,
|
|
711
|
+
predicate: row.predicate,
|
|
712
|
+
objects: row.objects
|
|
713
|
+
}));
|
|
714
|
+
}
|
|
715
|
+
async function getKgStats(pool) {
|
|
716
|
+
const totals = await pool.query(`SELECT
|
|
717
|
+
COUNT(*)::text AS total,
|
|
718
|
+
COUNT(*) FILTER (WHERE valid_to IS NULL)::text AS valid,
|
|
719
|
+
COUNT(*) FILTER (WHERE valid_to IS NOT NULL)::text AS invalidated,
|
|
720
|
+
COUNT(DISTINCT subject)::text AS subjects,
|
|
721
|
+
COUNT(DISTINCT predicate)::text AS predicates
|
|
722
|
+
FROM kg_triples`);
|
|
723
|
+
const contradictions = await pool.query(`SELECT COUNT(*)::text AS count FROM (
|
|
724
|
+
SELECT subject, predicate
|
|
725
|
+
FROM kg_triples
|
|
726
|
+
WHERE valid_to IS NULL
|
|
727
|
+
GROUP BY subject, predicate
|
|
728
|
+
HAVING COUNT(*) > 1
|
|
729
|
+
) c`);
|
|
730
|
+
const row = totals.rows[0];
|
|
731
|
+
return {
|
|
732
|
+
total: parseInt(row?.total ?? "0", 10),
|
|
733
|
+
valid: parseInt(row?.valid ?? "0", 10),
|
|
734
|
+
invalidated: parseInt(row?.invalidated ?? "0", 10),
|
|
735
|
+
subjects: parseInt(row?.subjects ?? "0", 10),
|
|
736
|
+
predicates: parseInt(row?.predicates ?? "0", 10),
|
|
737
|
+
contradictions: parseInt(contradictions.rows[0]?.count ?? "0", 10)
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
//#endregion
|
|
742
|
+
//#region src/storage/postgres/observations.ts
|
|
743
|
+
/**
|
|
744
|
+
* Inlined schema DDL — avoids runtime file reads that break in bundled code
|
|
745
|
+
* (the bundler puts this in a shared chunk whose __dirname differs from src/).
|
|
746
|
+
*/
|
|
747
|
+
const SCHEMA_SQL = `
|
|
748
|
+
CREATE TABLE IF NOT EXISTS pai_observations (
|
|
749
|
+
id SERIAL PRIMARY KEY,
|
|
750
|
+
session_id TEXT NOT NULL,
|
|
751
|
+
project_id INTEGER,
|
|
752
|
+
project_slug TEXT,
|
|
753
|
+
type TEXT NOT NULL CHECK (type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change')),
|
|
754
|
+
title TEXT NOT NULL,
|
|
755
|
+
narrative TEXT,
|
|
756
|
+
tool_name TEXT,
|
|
757
|
+
tool_input_summary TEXT,
|
|
758
|
+
files_read JSONB DEFAULT '[]'::jsonb,
|
|
759
|
+
files_modified JSONB DEFAULT '[]'::jsonb,
|
|
760
|
+
concepts JSONB DEFAULT '[]'::jsonb,
|
|
761
|
+
content_hash TEXT,
|
|
762
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
763
|
+
);
|
|
764
|
+
|
|
765
|
+
CREATE INDEX IF NOT EXISTS idx_obs_project ON pai_observations(project_id);
|
|
766
|
+
CREATE INDEX IF NOT EXISTS idx_obs_session ON pai_observations(session_id);
|
|
767
|
+
CREATE INDEX IF NOT EXISTS idx_obs_type ON pai_observations(type);
|
|
768
|
+
CREATE INDEX IF NOT EXISTS idx_obs_created ON pai_observations(created_at DESC);
|
|
769
|
+
CREATE INDEX IF NOT EXISTS idx_obs_hash ON pai_observations(content_hash);
|
|
770
|
+
|
|
771
|
+
CREATE TABLE IF NOT EXISTS pai_session_summaries (
|
|
772
|
+
id SERIAL PRIMARY KEY,
|
|
773
|
+
session_id TEXT NOT NULL UNIQUE,
|
|
774
|
+
project_id INTEGER,
|
|
775
|
+
project_slug TEXT,
|
|
776
|
+
request TEXT,
|
|
777
|
+
investigated TEXT,
|
|
778
|
+
learned TEXT,
|
|
779
|
+
completed TEXT,
|
|
780
|
+
next_steps TEXT,
|
|
781
|
+
observation_count INTEGER DEFAULT 0,
|
|
782
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
783
|
+
);
|
|
784
|
+
|
|
785
|
+
CREATE INDEX IF NOT EXISTS idx_ss_project ON pai_session_summaries(project_id);
|
|
786
|
+
CREATE INDEX IF NOT EXISTS idx_ss_session ON pai_session_summaries(session_id);
|
|
787
|
+
`;
|
|
788
|
+
let _tablesEnsured = false;
|
|
789
|
+
/**
|
|
790
|
+
* Run schema DDL idempotently against the given pool.
|
|
791
|
+
* Uses a module-level flag so subsequent calls are no-ops within the same
|
|
792
|
+
* process lifetime (the SQL itself uses IF NOT EXISTS so it is safe to re-run).
|
|
793
|
+
*/
|
|
794
|
+
async function ensureObservationTables(pool) {
|
|
795
|
+
if (_tablesEnsured) return;
|
|
796
|
+
await pool.query(SCHEMA_SQL);
|
|
797
|
+
_tablesEnsured = true;
|
|
798
|
+
}
|
|
799
|
+
const SKILL_TELEMETRY_SCHEMA_SQL = `
|
|
800
|
+
CREATE TABLE IF NOT EXISTS pai_skill_telemetry (
|
|
801
|
+
id SERIAL PRIMARY KEY,
|
|
802
|
+
scope TEXT NOT NULL DEFAULT 'default',
|
|
803
|
+
skill_name TEXT NOT NULL,
|
|
804
|
+
source TEXT NOT NULL DEFAULT 'local',
|
|
805
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('trial','active','archived')),
|
|
806
|
+
trigger_count INTEGER NOT NULL DEFAULT 0,
|
|
807
|
+
accept_count INTEGER NOT NULL DEFAULT 0,
|
|
808
|
+
first_triggered TIMESTAMPTZ DEFAULT NOW(),
|
|
809
|
+
last_triggered TIMESTAMPTZ DEFAULT NOW(),
|
|
810
|
+
context_projects JSONB DEFAULT '[]'::jsonb,
|
|
811
|
+
hash TEXT,
|
|
812
|
+
audit_status TEXT,
|
|
813
|
+
last_audited TIMESTAMPTZ,
|
|
814
|
+
UNIQUE(scope, skill_name, source)
|
|
815
|
+
);
|
|
816
|
+
|
|
817
|
+
CREATE INDEX IF NOT EXISTS idx_skilltel_scope ON pai_skill_telemetry(scope);
|
|
818
|
+
CREATE INDEX IF NOT EXISTS idx_skilltel_status ON pai_skill_telemetry(scope, status);
|
|
819
|
+
CREATE INDEX IF NOT EXISTS idx_skilltel_last ON pai_skill_telemetry(last_triggered DESC);
|
|
820
|
+
`;
|
|
821
|
+
let _skillTelemetryEnsured = false;
|
|
822
|
+
async function ensureSkillTelemetryTable(pool) {
|
|
823
|
+
if (_skillTelemetryEnsured) return;
|
|
824
|
+
await pool.query(SKILL_TELEMETRY_SCHEMA_SQL);
|
|
825
|
+
_skillTelemetryEnsured = true;
|
|
826
|
+
}
|
|
827
|
+
/** Hash = SHA256(session_id + tool_name + title).slice(0, 16) */
|
|
828
|
+
function computeContentHash(sessionId, toolName, title) {
|
|
829
|
+
return sha256(sessionId + "\0" + toolName + "\0" + title).slice(0, 16);
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Insert an observation, skipping duplicates within a 30-second window.
|
|
833
|
+
* Returns the inserted row's id, or null if the insert was suppressed.
|
|
834
|
+
*/
|
|
835
|
+
async function storeObservation(pool, obs) {
|
|
836
|
+
await ensureObservationTables(pool);
|
|
837
|
+
const hash = computeContentHash(obs.session_id, obs.tool_name, obs.title);
|
|
838
|
+
const dupCheck = await pool.query(`SELECT id FROM pai_observations
|
|
839
|
+
WHERE content_hash = $1
|
|
840
|
+
AND session_id = $2
|
|
841
|
+
AND created_at >= NOW() - INTERVAL '30 seconds'
|
|
842
|
+
LIMIT 1`, [hash, obs.session_id]);
|
|
843
|
+
if (dupCheck.rowCount && dupCheck.rowCount > 0) return null;
|
|
844
|
+
return (await pool.query(`INSERT INTO pai_observations
|
|
845
|
+
(session_id, project_id, project_slug, type, title, narrative,
|
|
846
|
+
tool_name, tool_input_summary, files_read, files_modified, concepts, content_hash)
|
|
847
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10::jsonb, $11::jsonb, $12)
|
|
848
|
+
RETURNING id`, [
|
|
849
|
+
obs.session_id,
|
|
850
|
+
obs.project_id ?? null,
|
|
851
|
+
obs.project_slug ?? null,
|
|
852
|
+
obs.type,
|
|
853
|
+
obs.title,
|
|
854
|
+
obs.narrative ?? null,
|
|
855
|
+
obs.tool_name,
|
|
856
|
+
obs.tool_input_summary ?? null,
|
|
857
|
+
JSON.stringify(obs.files_read),
|
|
858
|
+
JSON.stringify(obs.files_modified),
|
|
859
|
+
JSON.stringify(obs.concepts),
|
|
860
|
+
hash
|
|
861
|
+
])).rows[0]?.id ?? null;
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Attribute an observation to the active project covering `cwd`, then store
|
|
865
|
+
* it. One shared sequence for every writer (IPC handler, cache keepalive) so
|
|
866
|
+
* the lookup query exists exactly once.
|
|
867
|
+
*/
|
|
868
|
+
async function storeObservationWithProject(pool, registry, obs) {
|
|
869
|
+
let project_id = null;
|
|
870
|
+
let project_slug = null;
|
|
871
|
+
if (obs.cwd) {
|
|
872
|
+
const row = await registry.findProjectByCwdPrefix(obs.cwd);
|
|
873
|
+
if (row) {
|
|
874
|
+
project_id = row.id;
|
|
875
|
+
project_slug = row.slug;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
return storeObservation(pool, {
|
|
879
|
+
...obs,
|
|
880
|
+
project_id,
|
|
881
|
+
project_slug
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
async function queryObservations(pool, opts = {}) {
|
|
885
|
+
await ensureObservationTables(pool);
|
|
886
|
+
const conditions = [];
|
|
887
|
+
const params = [];
|
|
888
|
+
let idx = 1;
|
|
889
|
+
if (opts.projectId !== void 0) {
|
|
890
|
+
conditions.push(`project_id = $${idx++}`);
|
|
891
|
+
params.push(opts.projectId);
|
|
892
|
+
}
|
|
893
|
+
if (opts.sessionId !== void 0) {
|
|
894
|
+
conditions.push(`session_id = $${idx++}`);
|
|
895
|
+
params.push(opts.sessionId);
|
|
896
|
+
}
|
|
897
|
+
if (opts.type !== void 0) {
|
|
898
|
+
conditions.push(`type = $${idx++}`);
|
|
899
|
+
params.push(opts.type);
|
|
900
|
+
}
|
|
901
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
902
|
+
const limit = opts.limit ?? 50;
|
|
903
|
+
const offset = opts.offset ?? 0;
|
|
904
|
+
params.push(limit, offset);
|
|
905
|
+
return (await pool.query(`SELECT id, session_id, project_id, project_slug, type, title, narrative,
|
|
906
|
+
tool_name, tool_input_summary,
|
|
907
|
+
files_read, files_modified, concepts,
|
|
908
|
+
content_hash, created_at
|
|
909
|
+
FROM pai_observations
|
|
910
|
+
${where}
|
|
911
|
+
ORDER BY created_at DESC
|
|
912
|
+
LIMIT $${idx++} OFFSET $${idx}`, params)).rows;
|
|
913
|
+
}
|
|
914
|
+
async function queryRecentObservations(pool, projectId, limit) {
|
|
915
|
+
await ensureObservationTables(pool);
|
|
916
|
+
return (await pool.query(`SELECT id, session_id, project_id, project_slug, type, title, narrative,
|
|
917
|
+
tool_name, tool_input_summary,
|
|
918
|
+
files_read, files_modified, concepts,
|
|
919
|
+
content_hash, created_at
|
|
920
|
+
FROM pai_observations
|
|
921
|
+
WHERE project_id = $1
|
|
922
|
+
ORDER BY created_at DESC
|
|
923
|
+
LIMIT $2`, [projectId, limit])).rows;
|
|
924
|
+
}
|
|
925
|
+
async function querySessionObservations(pool, sessionId) {
|
|
926
|
+
await ensureObservationTables(pool);
|
|
927
|
+
return (await pool.query(`SELECT id, session_id, project_id, project_slug, type, title, narrative,
|
|
928
|
+
tool_name, tool_input_summary,
|
|
929
|
+
files_read, files_modified, concepts,
|
|
930
|
+
content_hash, created_at
|
|
931
|
+
FROM pai_observations
|
|
932
|
+
WHERE session_id = $1
|
|
933
|
+
ORDER BY created_at ASC`, [sessionId])).rows;
|
|
934
|
+
}
|
|
935
|
+
async function getObservationStats(pool) {
|
|
936
|
+
await ensureObservationTables(pool);
|
|
937
|
+
const [totalRes, byTypeRes, byProjectRes, recentRes] = await Promise.all([
|
|
938
|
+
pool.query("SELECT COUNT(*) as count FROM pai_observations"),
|
|
939
|
+
pool.query("SELECT type, COUNT(*) as count FROM pai_observations GROUP BY type ORDER BY count DESC"),
|
|
940
|
+
pool.query("SELECT project_slug, COUNT(*) as count FROM pai_observations GROUP BY project_slug ORDER BY count DESC LIMIT 15"),
|
|
941
|
+
pool.query("SELECT created_at FROM pai_observations ORDER BY created_at DESC LIMIT 1")
|
|
942
|
+
]);
|
|
943
|
+
return {
|
|
944
|
+
total: parseInt(totalRes.rows[0]?.count ?? "0", 10),
|
|
945
|
+
by_type: byTypeRes.rows.map((r) => ({
|
|
946
|
+
type: r.type,
|
|
947
|
+
count: parseInt(r.count, 10)
|
|
948
|
+
})),
|
|
949
|
+
by_project: byProjectRes.rows.map((r) => ({
|
|
950
|
+
project_slug: r.project_slug,
|
|
951
|
+
count: parseInt(r.count, 10)
|
|
952
|
+
})),
|
|
953
|
+
most_recent: recentRes.rows[0]?.created_at ?? null
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Observation-type counts per vault path, for the graph_* handlers (note
|
|
958
|
+
* enrichment). Falls back to an empty map when the query fails — this is
|
|
959
|
+
* best-effort enrichment, never a hard dependency for graph responses.
|
|
960
|
+
*/
|
|
961
|
+
async function getObservationTypesForPaths(pool, filePaths, projectId) {
|
|
962
|
+
if (filePaths.length === 0) return /* @__PURE__ */ new Map();
|
|
963
|
+
try {
|
|
964
|
+
const params = [filePaths];
|
|
965
|
+
let projectFilter = "";
|
|
966
|
+
if (projectId !== void 0) {
|
|
967
|
+
params.push(projectId);
|
|
968
|
+
projectFilter = `AND project_id = $${params.length}`;
|
|
969
|
+
}
|
|
970
|
+
const result = await pool.query(`SELECT unnested_path AS path, type, COUNT(*) AS cnt
|
|
971
|
+
FROM pai_observations,
|
|
972
|
+
LATERAL unnest(files_modified || files_read) AS unnested_path
|
|
973
|
+
WHERE unnested_path = ANY($1::text[])
|
|
974
|
+
${projectFilter}
|
|
975
|
+
GROUP BY unnested_path, type`, params);
|
|
976
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
977
|
+
for (const row of result.rows) {
|
|
978
|
+
const existing = byPath.get(row.path) ?? {};
|
|
979
|
+
existing[row.type] = (existing[row.type] ?? 0) + parseInt(row.cnt, 10);
|
|
980
|
+
byPath.set(row.path, existing);
|
|
981
|
+
}
|
|
982
|
+
return byPath;
|
|
983
|
+
} catch {
|
|
984
|
+
return /* @__PURE__ */ new Map();
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
/** Upsert a session summary. Uses ON CONFLICT on session_id so re-calling with updated content is safe. */
|
|
988
|
+
async function storeSessionSummary(pool, summary) {
|
|
989
|
+
await ensureObservationTables(pool);
|
|
990
|
+
await pool.query(`INSERT INTO pai_session_summaries
|
|
991
|
+
(session_id, project_id, project_slug, request, investigated,
|
|
992
|
+
learned, completed, next_steps, observation_count)
|
|
993
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
994
|
+
ON CONFLICT (session_id) DO UPDATE SET
|
|
995
|
+
project_id = EXCLUDED.project_id,
|
|
996
|
+
project_slug = EXCLUDED.project_slug,
|
|
997
|
+
request = EXCLUDED.request,
|
|
998
|
+
investigated = EXCLUDED.investigated,
|
|
999
|
+
learned = EXCLUDED.learned,
|
|
1000
|
+
completed = EXCLUDED.completed,
|
|
1001
|
+
next_steps = EXCLUDED.next_steps,
|
|
1002
|
+
observation_count = EXCLUDED.observation_count`, [
|
|
1003
|
+
summary.session_id,
|
|
1004
|
+
summary.project_id ?? null,
|
|
1005
|
+
summary.project_slug ?? null,
|
|
1006
|
+
summary.request ?? null,
|
|
1007
|
+
summary.investigated ?? null,
|
|
1008
|
+
summary.learned ?? null,
|
|
1009
|
+
summary.completed ?? null,
|
|
1010
|
+
summary.next_steps ?? null,
|
|
1011
|
+
summary.observation_count ?? 0
|
|
1012
|
+
]);
|
|
1013
|
+
}
|
|
1014
|
+
async function queryRecentSummaries(pool, projectId, limit) {
|
|
1015
|
+
await ensureObservationTables(pool);
|
|
1016
|
+
return (await pool.query(`SELECT id, session_id, project_id, project_slug,
|
|
1017
|
+
request, investigated, learned, completed, next_steps,
|
|
1018
|
+
observation_count, created_at
|
|
1019
|
+
FROM pai_session_summaries
|
|
1020
|
+
WHERE project_id = $1
|
|
1021
|
+
ORDER BY created_at DESC
|
|
1022
|
+
LIMIT $2`, [projectId, limit])).rows;
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Record a single skill invocation. Upserts on (scope, skill_name, source):
|
|
1026
|
+
* increments trigger_count, refreshes last_triggered, and dedup-merges the
|
|
1027
|
+
* project slug into context_projects.
|
|
1028
|
+
*/
|
|
1029
|
+
async function recordSkillInvocation(pool, input) {
|
|
1030
|
+
await ensureSkillTelemetryTable(pool);
|
|
1031
|
+
const scope = input.scope ?? "default";
|
|
1032
|
+
const source = input.source ?? "local";
|
|
1033
|
+
const proj = input.project_slug ?? null;
|
|
1034
|
+
const initialProjects = JSON.stringify(proj ? [proj] : []);
|
|
1035
|
+
await pool.query(`INSERT INTO pai_skill_telemetry
|
|
1036
|
+
(scope, skill_name, source, trigger_count, context_projects)
|
|
1037
|
+
VALUES ($1, $2, $3, 1, $4::jsonb)
|
|
1038
|
+
ON CONFLICT (scope, skill_name, source) DO UPDATE SET
|
|
1039
|
+
trigger_count = pai_skill_telemetry.trigger_count + 1,
|
|
1040
|
+
last_triggered = NOW(),
|
|
1041
|
+
context_projects = CASE
|
|
1042
|
+
WHEN $5::text IS NULL THEN pai_skill_telemetry.context_projects
|
|
1043
|
+
WHEN pai_skill_telemetry.context_projects @> to_jsonb($5::text)
|
|
1044
|
+
THEN pai_skill_telemetry.context_projects
|
|
1045
|
+
ELSE pai_skill_telemetry.context_projects || to_jsonb($5::text)
|
|
1046
|
+
END`, [
|
|
1047
|
+
scope,
|
|
1048
|
+
input.skill_name,
|
|
1049
|
+
source,
|
|
1050
|
+
initialProjects,
|
|
1051
|
+
proj
|
|
1052
|
+
]);
|
|
1053
|
+
}
|
|
1054
|
+
/** List skill telemetry rows, most-triggered first. */
|
|
1055
|
+
async function querySkillTelemetry(pool, opts = {}) {
|
|
1056
|
+
await ensureSkillTelemetryTable(pool);
|
|
1057
|
+
const conditions = [];
|
|
1058
|
+
const params = [];
|
|
1059
|
+
let idx = 1;
|
|
1060
|
+
conditions.push(`scope = $${idx++}`);
|
|
1061
|
+
params.push(opts.scope ?? "default");
|
|
1062
|
+
if (opts.status !== void 0) {
|
|
1063
|
+
conditions.push(`status = $${idx++}`);
|
|
1064
|
+
params.push(opts.status);
|
|
1065
|
+
}
|
|
1066
|
+
const where = `WHERE ${conditions.join(" AND ")}`;
|
|
1067
|
+
const limit = opts.limit ?? 100;
|
|
1068
|
+
params.push(limit);
|
|
1069
|
+
return (await pool.query(`SELECT id, scope, skill_name, source, status,
|
|
1070
|
+
trigger_count, accept_count,
|
|
1071
|
+
first_triggered, last_triggered,
|
|
1072
|
+
context_projects, hash, audit_status, last_audited
|
|
1073
|
+
FROM pai_skill_telemetry
|
|
1074
|
+
${where}
|
|
1075
|
+
ORDER BY trigger_count DESC, last_triggered DESC
|
|
1076
|
+
LIMIT $${idx}`, params)).rows;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
508
1079
|
//#endregion
|
|
509
1080
|
//#region src/storage/postgres/backend.ts
|
|
510
1081
|
/**
|
|
@@ -520,6 +1091,7 @@ async function searchVaultNameIndex(pool, query, limit = 100) {
|
|
|
520
1091
|
const { Pool: PgPool } = pg;
|
|
521
1092
|
var PostgresBackend = class PostgresBackend {
|
|
522
1093
|
backendType = "postgres";
|
|
1094
|
+
supportsPostgresFeatures = true;
|
|
523
1095
|
pool;
|
|
524
1096
|
/**
|
|
525
1097
|
* Ensure the per-user database exists and has the required schema.
|
|
@@ -599,13 +1171,129 @@ var PostgresBackend = class PostgresBackend {
|
|
|
599
1171
|
await pool.query(`CREATE INDEX idx_kg_valid ON kg_triples(valid_from, valid_to)`);
|
|
600
1172
|
process.stderr.write("[pai-postgres] Migration: created kg_triples table\n");
|
|
601
1173
|
}
|
|
1174
|
+
await pool.query(`
|
|
1175
|
+
CREATE TABLE IF NOT EXISTS kg_entities (
|
|
1176
|
+
entity_id TEXT PRIMARY KEY,
|
|
1177
|
+
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
1178
|
+
name TEXT NOT NULL,
|
|
1179
|
+
type TEXT NOT NULL DEFAULT 'unknown',
|
|
1180
|
+
description TEXT,
|
|
1181
|
+
first_seen BIGINT,
|
|
1182
|
+
last_seen BIGINT,
|
|
1183
|
+
mention_count INTEGER NOT NULL DEFAULT 1,
|
|
1184
|
+
feedback_weight REAL NOT NULL DEFAULT 0.5,
|
|
1185
|
+
UNIQUE (tenant_id, entity_id)
|
|
1186
|
+
);
|
|
1187
|
+
CREATE INDEX IF NOT EXISTS idx_kge_tenant ON kg_entities(tenant_id);
|
|
1188
|
+
CREATE INDEX IF NOT EXISTS idx_kge_name ON kg_entities(tenant_id, name);
|
|
1189
|
+
CREATE INDEX IF NOT EXISTS idx_kge_type ON kg_entities(tenant_id, type);
|
|
1190
|
+
|
|
1191
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
1192
|
+
id SERIAL PRIMARY KEY,
|
|
1193
|
+
slug TEXT NOT NULL UNIQUE,
|
|
1194
|
+
display_name TEXT NOT NULL,
|
|
1195
|
+
root_path TEXT NOT NULL UNIQUE,
|
|
1196
|
+
encoded_dir TEXT NOT NULL UNIQUE,
|
|
1197
|
+
type TEXT NOT NULL DEFAULT 'local'
|
|
1198
|
+
CHECK (type IN ('local','central','obsidian-linked','external')),
|
|
1199
|
+
status TEXT NOT NULL DEFAULT 'active'
|
|
1200
|
+
CHECK (status IN ('active','archived','migrating')),
|
|
1201
|
+
parent_id INTEGER REFERENCES projects(id),
|
|
1202
|
+
obsidian_link TEXT,
|
|
1203
|
+
claude_notes_dir TEXT,
|
|
1204
|
+
session_config TEXT,
|
|
1205
|
+
created_at BIGINT NOT NULL,
|
|
1206
|
+
updated_at BIGINT NOT NULL,
|
|
1207
|
+
archived_at BIGINT
|
|
1208
|
+
);
|
|
1209
|
+
|
|
1210
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
1211
|
+
id SERIAL PRIMARY KEY,
|
|
1212
|
+
project_id INTEGER NOT NULL REFERENCES projects(id),
|
|
1213
|
+
number INTEGER NOT NULL,
|
|
1214
|
+
date TEXT NOT NULL,
|
|
1215
|
+
slug TEXT NOT NULL,
|
|
1216
|
+
title TEXT NOT NULL,
|
|
1217
|
+
filename TEXT NOT NULL,
|
|
1218
|
+
status TEXT NOT NULL DEFAULT 'open'
|
|
1219
|
+
CHECK (status IN ('open','completed','compacted')),
|
|
1220
|
+
claude_session_id TEXT,
|
|
1221
|
+
token_count INTEGER,
|
|
1222
|
+
created_at BIGINT NOT NULL,
|
|
1223
|
+
closed_at BIGINT,
|
|
1224
|
+
UNIQUE (project_id, number)
|
|
1225
|
+
);
|
|
1226
|
+
|
|
1227
|
+
CREATE TABLE IF NOT EXISTS tags (
|
|
1228
|
+
id SERIAL PRIMARY KEY,
|
|
1229
|
+
name TEXT NOT NULL UNIQUE
|
|
1230
|
+
);
|
|
1231
|
+
|
|
1232
|
+
CREATE TABLE IF NOT EXISTS project_tags (
|
|
1233
|
+
project_id INTEGER NOT NULL REFERENCES projects(id),
|
|
1234
|
+
tag_id INTEGER NOT NULL REFERENCES tags(id),
|
|
1235
|
+
PRIMARY KEY (project_id, tag_id)
|
|
1236
|
+
);
|
|
1237
|
+
|
|
1238
|
+
CREATE TABLE IF NOT EXISTS session_tags (
|
|
1239
|
+
session_id INTEGER NOT NULL REFERENCES sessions(id),
|
|
1240
|
+
tag_id INTEGER NOT NULL REFERENCES tags(id),
|
|
1241
|
+
PRIMARY KEY (session_id, tag_id)
|
|
1242
|
+
);
|
|
1243
|
+
|
|
1244
|
+
CREATE TABLE IF NOT EXISTS aliases (
|
|
1245
|
+
alias TEXT PRIMARY KEY,
|
|
1246
|
+
project_id INTEGER NOT NULL REFERENCES projects(id)
|
|
1247
|
+
);
|
|
1248
|
+
|
|
1249
|
+
CREATE TABLE IF NOT EXISTS compaction_log (
|
|
1250
|
+
id SERIAL PRIMARY KEY,
|
|
1251
|
+
project_id INTEGER NOT NULL REFERENCES projects(id),
|
|
1252
|
+
session_id INTEGER REFERENCES sessions(id),
|
|
1253
|
+
trigger TEXT NOT NULL CHECK (trigger IN ('precompact','manual','end-session')),
|
|
1254
|
+
files_written TEXT NOT NULL,
|
|
1255
|
+
token_count INTEGER,
|
|
1256
|
+
created_at BIGINT NOT NULL
|
|
1257
|
+
);
|
|
1258
|
+
|
|
1259
|
+
CREATE TABLE IF NOT EXISTS links (
|
|
1260
|
+
id SERIAL PRIMARY KEY,
|
|
1261
|
+
session_id INTEGER NOT NULL REFERENCES sessions(id),
|
|
1262
|
+
target_project_id INTEGER NOT NULL REFERENCES projects(id),
|
|
1263
|
+
link_type TEXT NOT NULL DEFAULT 'related'
|
|
1264
|
+
CHECK (link_type IN ('related','follow-up','reference')),
|
|
1265
|
+
created_at BIGINT NOT NULL,
|
|
1266
|
+
UNIQUE (session_id, target_project_id)
|
|
1267
|
+
);
|
|
1268
|
+
|
|
1269
|
+
CREATE INDEX IF NOT EXISTS idx_projects_slug ON projects(slug);
|
|
1270
|
+
CREATE INDEX IF NOT EXISTS idx_projects_status ON projects(status);
|
|
1271
|
+
CREATE INDEX IF NOT EXISTS idx_projects_type ON projects(type);
|
|
1272
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
|
|
1273
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_date ON sessions(date);
|
|
1274
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
1275
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_claude ON sessions(claude_session_id);
|
|
1276
|
+
CREATE INDEX IF NOT EXISTS idx_pc_project ON project_tags(project_id);
|
|
1277
|
+
`);
|
|
1278
|
+
const feedbackColCheck = await pool.query(`SELECT column_name FROM information_schema.columns
|
|
1279
|
+
WHERE table_name = 'pai_chunks' AND column_name IN ('relevance_score', 'last_accessed_at')`);
|
|
1280
|
+
const existingFeedbackCols = new Set(feedbackColCheck.rows.map((r) => r.column_name));
|
|
1281
|
+
if (!existingFeedbackCols.has("relevance_score")) {
|
|
1282
|
+
await pool.query("ALTER TABLE pai_chunks ADD COLUMN relevance_score REAL");
|
|
1283
|
+
process.stderr.write("[pai-postgres] Migration: added relevance_score column to pai_chunks\n");
|
|
1284
|
+
}
|
|
1285
|
+
if (!existingFeedbackCols.has("last_accessed_at")) {
|
|
1286
|
+
await pool.query("ALTER TABLE pai_chunks ADD COLUMN last_accessed_at BIGINT");
|
|
1287
|
+
process.stderr.write("[pai-postgres] Migration: added last_accessed_at column to pai_chunks\n");
|
|
1288
|
+
}
|
|
602
1289
|
}
|
|
603
1290
|
constructor(config) {
|
|
604
1291
|
this.pool = new PgPool({
|
|
605
1292
|
connectionString: config.connectionString ?? `postgresql://${config.user ?? "pai"}:${config.password ?? "pai"}@${config.host ?? "localhost"}:${config.port ?? 5432}/${config.database ?? "pai"}`,
|
|
606
1293
|
max: config.maxConnections ?? 5,
|
|
607
1294
|
connectionTimeoutMillis: config.connectionTimeoutMs ?? 5e3,
|
|
608
|
-
idleTimeoutMillis: 3e4
|
|
1295
|
+
idleTimeoutMillis: 3e4,
|
|
1296
|
+
allowExitOnIdle: true
|
|
609
1297
|
});
|
|
610
1298
|
this.pool.on("error", (err) => {
|
|
611
1299
|
process.stderr.write(`[pai-postgres] Pool error: ${err.message}\n`);
|
|
@@ -634,6 +1322,14 @@ var PostgresBackend = class PostgresBackend {
|
|
|
634
1322
|
client.release();
|
|
635
1323
|
}
|
|
636
1324
|
}
|
|
1325
|
+
async getProjectStats(projectId) {
|
|
1326
|
+
const filesResult = await this.pool.query("SELECT COUNT(*)::text AS n FROM pai_files WHERE project_id = $1", [projectId]);
|
|
1327
|
+
const chunksResult = await this.pool.query("SELECT COUNT(*)::text AS n FROM pai_chunks WHERE project_id = $1", [projectId]);
|
|
1328
|
+
return {
|
|
1329
|
+
files: parseInt(filesResult.rows[0]?.n ?? "0", 10),
|
|
1330
|
+
chunks: parseInt(chunksResult.rows[0]?.n ?? "0", 10)
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
637
1333
|
/**
|
|
638
1334
|
* Test the connection by running a trivial query.
|
|
639
1335
|
* Returns null on success, error message on failure.
|
|
@@ -740,13 +1436,17 @@ var PostgresBackend = class PostgresBackend {
|
|
|
740
1436
|
client.release();
|
|
741
1437
|
}
|
|
742
1438
|
}
|
|
743
|
-
async getUnembeddedChunkIds(projectId, limit) {
|
|
1439
|
+
async getUnembeddedChunkIds(projectId, limit, after) {
|
|
744
1440
|
const params = [];
|
|
745
1441
|
let sql = "SELECT id, text, project_id, path FROM pai_chunks WHERE embedding IS NULL";
|
|
746
1442
|
if (projectId !== void 0) {
|
|
747
1443
|
params.push(projectId);
|
|
748
1444
|
sql += ` AND project_id = $${params.length}`;
|
|
749
1445
|
}
|
|
1446
|
+
if (after) {
|
|
1447
|
+
params.push(after.projectId, after.projectId, after.id);
|
|
1448
|
+
sql += ` AND (project_id > $${params.length - 2} OR (project_id = $${params.length - 1} AND id > $${params.length}))`;
|
|
1449
|
+
}
|
|
750
1450
|
sql += " ORDER BY project_id, id";
|
|
751
1451
|
if (limit !== void 0) {
|
|
752
1452
|
params.push(limit);
|
|
@@ -910,8 +1610,167 @@ var PostgresBackend = class PostgresBackend {
|
|
|
910
1610
|
limit
|
|
911
1611
|
])).rows;
|
|
912
1612
|
}
|
|
1613
|
+
async upsertKgEntity(params) {
|
|
1614
|
+
const tenantId = params.tenantId ?? "default";
|
|
1615
|
+
const entityId = entityContentId(params.name, tenantId);
|
|
1616
|
+
const now = Date.now();
|
|
1617
|
+
await this.pool.query(`INSERT INTO kg_entities
|
|
1618
|
+
(entity_id, tenant_id, name, type, description, first_seen, last_seen, mention_count, feedback_weight)
|
|
1619
|
+
VALUES
|
|
1620
|
+
($1, $2, $3, $4, $5, $6, $7, 1, 0.5)
|
|
1621
|
+
ON CONFLICT (entity_id) DO UPDATE SET
|
|
1622
|
+
last_seen = EXCLUDED.last_seen,
|
|
1623
|
+
mention_count = kg_entities.mention_count + 1,
|
|
1624
|
+
description = COALESCE(EXCLUDED.description, kg_entities.description),
|
|
1625
|
+
type = CASE WHEN EXCLUDED.type != 'unknown' THEN EXCLUDED.type ELSE kg_entities.type END`, [
|
|
1626
|
+
entityId,
|
|
1627
|
+
tenantId,
|
|
1628
|
+
params.name,
|
|
1629
|
+
params.type ?? "unknown",
|
|
1630
|
+
params.description ?? null,
|
|
1631
|
+
now,
|
|
1632
|
+
now
|
|
1633
|
+
]);
|
|
1634
|
+
return entityId;
|
|
1635
|
+
}
|
|
1636
|
+
async findKgEntity(name, tenantId = "default") {
|
|
1637
|
+
const entityId = entityContentId(name, tenantId);
|
|
1638
|
+
const row = (await this.pool.query("SELECT * FROM kg_entities WHERE entity_id = $1 AND tenant_id = $2", [entityId, tenantId])).rows[0];
|
|
1639
|
+
return row ? rowToKgEntity(row) : null;
|
|
1640
|
+
}
|
|
1641
|
+
async listKgEntities(tenantId = "default", type, limit = 100) {
|
|
1642
|
+
return (type ? await this.pool.query("SELECT * FROM kg_entities WHERE tenant_id = $1 AND type = $2 ORDER BY mention_count DESC LIMIT $3", [
|
|
1643
|
+
tenantId,
|
|
1644
|
+
type,
|
|
1645
|
+
limit
|
|
1646
|
+
]) : await this.pool.query("SELECT * FROM kg_entities WHERE tenant_id = $1 ORDER BY mention_count DESC LIMIT $2", [tenantId, limit])).rows.map(rowToKgEntity);
|
|
1647
|
+
}
|
|
1648
|
+
async updateEntityFeedbackWeight(entityId, normalizedRating, alpha = .1) {
|
|
1649
|
+
const row = (await this.pool.query("SELECT feedback_weight FROM kg_entities WHERE entity_id = $1", [entityId])).rows[0];
|
|
1650
|
+
if (!row) return;
|
|
1651
|
+
const newWeight = Number(row.feedback_weight) + alpha * (normalizedRating - Number(row.feedback_weight));
|
|
1652
|
+
await this.pool.query("UPDATE kg_entities SET feedback_weight = $1 WHERE entity_id = $2", [newWeight, entityId]);
|
|
1653
|
+
}
|
|
1654
|
+
async getChunksForFeedback(chunkIds) {
|
|
1655
|
+
if (chunkIds.length === 0) return [];
|
|
1656
|
+
return (await this.pool.query("SELECT id, text, relevance_score FROM pai_chunks WHERE id = ANY($1)", [chunkIds])).rows.map((r) => ({
|
|
1657
|
+
id: r.id,
|
|
1658
|
+
text: r.text,
|
|
1659
|
+
relevanceScore: r.relevance_score
|
|
1660
|
+
}));
|
|
1661
|
+
}
|
|
1662
|
+
async updateChunkRelevanceScore(chunkId, score) {
|
|
1663
|
+
await this.pool.query("UPDATE pai_chunks SET relevance_score = $1 WHERE id = $2", [score, chunkId]);
|
|
1664
|
+
}
|
|
1665
|
+
async touchChunksLastAccessed(chunkIds) {
|
|
1666
|
+
if (chunkIds.length === 0) return;
|
|
1667
|
+
try {
|
|
1668
|
+
await this.pool.query("UPDATE pai_chunks SET last_accessed_at = $1 WHERE id = ANY($2)", [Date.now(), chunkIds]);
|
|
1669
|
+
} catch {}
|
|
1670
|
+
}
|
|
1671
|
+
async indexAll(registry) {
|
|
1672
|
+
return indexAllWithBackend(this, registry);
|
|
1673
|
+
}
|
|
1674
|
+
async findTunnels(registry, options) {
|
|
1675
|
+
return findTunnelsPostgres(this.pool, registry, options);
|
|
1676
|
+
}
|
|
1677
|
+
async getMemorySourcesReport() {
|
|
1678
|
+
return getMemorySourcesReportPostgres(this.pool);
|
|
1679
|
+
}
|
|
1680
|
+
async addKgTriple(params) {
|
|
1681
|
+
return addKgTriple(this.pool, params);
|
|
1682
|
+
}
|
|
1683
|
+
async queryKgTriples(params) {
|
|
1684
|
+
return queryKgTriples(this.pool, params);
|
|
1685
|
+
}
|
|
1686
|
+
async invalidateKgTriple(tripleId) {
|
|
1687
|
+
return invalidateKgTriple(this.pool, tripleId);
|
|
1688
|
+
}
|
|
1689
|
+
async getKgContradictions(subject) {
|
|
1690
|
+
return getKgContradictions(this.pool, subject);
|
|
1691
|
+
}
|
|
1692
|
+
async getKgStats() {
|
|
1693
|
+
return getKgStats(this.pool);
|
|
1694
|
+
}
|
|
1695
|
+
async storeObservation(obs) {
|
|
1696
|
+
return storeObservation(this.pool, obs);
|
|
1697
|
+
}
|
|
1698
|
+
async storeObservationWithProject(registry, obs) {
|
|
1699
|
+
return storeObservationWithProject(this.pool, registry, obs);
|
|
1700
|
+
}
|
|
1701
|
+
async queryObservations(opts) {
|
|
1702
|
+
return queryObservations(this.pool, opts);
|
|
1703
|
+
}
|
|
1704
|
+
async queryRecentObservations(projectId, limit) {
|
|
1705
|
+
return queryRecentObservations(this.pool, projectId, limit);
|
|
1706
|
+
}
|
|
1707
|
+
async querySessionObservations(sessionId) {
|
|
1708
|
+
return querySessionObservations(this.pool, sessionId);
|
|
1709
|
+
}
|
|
1710
|
+
async getObservationStats() {
|
|
1711
|
+
return getObservationStats(this.pool);
|
|
1712
|
+
}
|
|
1713
|
+
async getObservationTypesForPaths(filePaths, projectId) {
|
|
1714
|
+
return getObservationTypesForPaths(this.pool, filePaths, projectId);
|
|
1715
|
+
}
|
|
1716
|
+
async storeSessionSummary(summary) {
|
|
1717
|
+
return storeSessionSummary(this.pool, summary);
|
|
1718
|
+
}
|
|
1719
|
+
async queryRecentSummaries(projectId, limit) {
|
|
1720
|
+
return queryRecentSummaries(this.pool, projectId, limit);
|
|
1721
|
+
}
|
|
1722
|
+
async recordSkillInvocation(input) {
|
|
1723
|
+
return recordSkillInvocation(this.pool, input);
|
|
1724
|
+
}
|
|
1725
|
+
async querySkillTelemetry(opts) {
|
|
1726
|
+
return querySkillTelemetry(this.pool, opts);
|
|
1727
|
+
}
|
|
1728
|
+
async countFilesWithPaths(paths) {
|
|
1729
|
+
if (paths.length === 0) return 0;
|
|
1730
|
+
const result = await this.pool.query("SELECT COUNT(*)::text AS n FROM pai_files WHERE path = ANY($1)", [paths]);
|
|
1731
|
+
return parseInt(result.rows[0]?.n ?? "0", 10);
|
|
1732
|
+
}
|
|
1733
|
+
async renameFilePaths(moves) {
|
|
1734
|
+
if (moves.length === 0) return 0;
|
|
1735
|
+
const client = await this.pool.connect();
|
|
1736
|
+
let filesUpdated = 0;
|
|
1737
|
+
try {
|
|
1738
|
+
await client.query("BEGIN");
|
|
1739
|
+
for (const { oldPath, newPath } of moves) {
|
|
1740
|
+
const r = await client.query("UPDATE pai_files SET path = $1 WHERE path = $2", [newPath, oldPath]);
|
|
1741
|
+
filesUpdated += r.rowCount ?? 0;
|
|
1742
|
+
await client.query("UPDATE pai_chunks SET path = $1 WHERE path = $2", [newPath, oldPath]);
|
|
1743
|
+
}
|
|
1744
|
+
await client.query("COMMIT");
|
|
1745
|
+
} catch (e) {
|
|
1746
|
+
await client.query("ROLLBACK");
|
|
1747
|
+
throw e;
|
|
1748
|
+
} finally {
|
|
1749
|
+
client.release();
|
|
1750
|
+
}
|
|
1751
|
+
return filesUpdated;
|
|
1752
|
+
}
|
|
913
1753
|
};
|
|
1754
|
+
/**
|
|
1755
|
+
* Postgres returns BIGINT columns (first_seen/last_seen) as strings — JS
|
|
1756
|
+
* numbers can't safely hold the full 64-bit range. Entity timestamps are
|
|
1757
|
+
* Date.now() ms and well inside Number.isSafeInteger, so a plain Number()
|
|
1758
|
+
* conversion here keeps the shape identical to better-sqlite3's row objects.
|
|
1759
|
+
*/
|
|
1760
|
+
function rowToKgEntity(row) {
|
|
1761
|
+
return {
|
|
1762
|
+
entity_id: row.entity_id,
|
|
1763
|
+
tenant_id: row.tenant_id,
|
|
1764
|
+
name: row.name,
|
|
1765
|
+
type: row.type,
|
|
1766
|
+
description: row.description,
|
|
1767
|
+
first_seen: row.first_seen != null ? Number(row.first_seen) : void 0,
|
|
1768
|
+
last_seen: row.last_seen != null ? Number(row.last_seen) : void 0,
|
|
1769
|
+
mention_count: Number(row.mention_count),
|
|
1770
|
+
feedback_weight: Number(row.feedback_weight)
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
914
1773
|
|
|
915
1774
|
//#endregion
|
|
916
1775
|
export { PostgresBackend };
|
|
917
|
-
//# sourceMappingURL=postgres-
|
|
1776
|
+
//# sourceMappingURL=postgres-nvU5pShF.mjs.map
|