@remnic/core 10.0.1 → 10.0.3
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/access-admin-ops-surface.js +40 -39
- package/dist/access-authorization-probe.js +41 -40
- package/dist/access-boundary.js +40 -39
- package/dist/access-citation-namespace.js +2 -2
- package/dist/access-cli.js +89 -88
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.js +40 -39
- package/dist/access-http-briefing.js +41 -40
- package/dist/access-http-query.js +42 -41
- package/dist/access-http.js +58 -57
- package/dist/access-identity-continuity-surface.js +40 -39
- package/dist/access-lcm-surface.js +40 -39
- package/dist/access-mcp.js +49 -48
- package/dist/access-namespace-preflight.js +40 -39
- package/dist/access-observe-write-category.js +9 -9
- package/dist/access-observe-write-surface.js +40 -39
- package/dist/access-offline-manifest.js +6 -6
- package/dist/access-operations-batch.js +41 -40
- package/dist/access-operations.js +48 -47
- package/dist/access-recall-concurrency.js +40 -39
- package/dist/access-recall-response.js +40 -39
- package/dist/access-recall-surface.js +40 -39
- package/dist/access-schema.js +8 -8
- package/dist/access-service.js +40 -39
- package/dist/access-surface-catalog.js +42 -41
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/active-recall.js +12 -12
- package/dist/briefing.js +8 -8
- package/dist/causal-consolidation.js +9 -9
- package/dist/{chunk-A7V7GZ7F.js → chunk-2DHJTJIY.js} +2 -2
- package/dist/{chunk-MJ737UIA.js → chunk-2NV7MMMM.js} +2 -2
- package/dist/{chunk-34TSXVQS.js → chunk-2QZFZNXO.js} +2 -2
- package/dist/{chunk-TVPDF6CZ.js → chunk-44PBRUXB.js} +2 -2
- package/dist/{chunk-YX5F4V7H.js → chunk-4DDIWRT7.js} +2 -2
- package/dist/{chunk-EPFNPPWH.js → chunk-4NRZ7BRS.js} +2 -2
- package/dist/{chunk-VSPBXHGR.js → chunk-5QXMESS7.js} +2 -2
- package/dist/{chunk-S4DDLTPX.js → chunk-5Y43VGYB.js} +2 -2
- package/dist/{chunk-VXCTQ3B3.js → chunk-6AA7XFMO.js} +2 -2
- package/dist/{chunk-LI3OM3NP.js → chunk-6GU3TNJJ.js} +5 -5
- package/dist/{chunk-HPEONBM3.js → chunk-6JIHC5AS.js} +4 -4
- package/dist/chunk-6JIHC5AS.js.map +1 -0
- package/dist/{chunk-B2UCJX76.js → chunk-7KICGR5W.js} +2 -2
- package/dist/chunk-7PZHH4KC.js +63 -0
- package/dist/chunk-7PZHH4KC.js.map +1 -0
- package/dist/{chunk-UT7WE7RM.js → chunk-AUWMKDKL.js} +2 -2
- package/dist/{chunk-U6VC6RP3.js → chunk-AWLQNLTM.js} +2 -2
- package/dist/{chunk-PJSK6OJM.js → chunk-BANSBRNX.js} +2 -2
- package/dist/{chunk-2TCNEJ36.js → chunk-BNMI3JBV.js} +3 -3
- package/dist/{chunk-LTDWZ2HC.js → chunk-CFCEJNOE.js} +2 -2
- package/dist/{chunk-R6NHHKXE.js → chunk-CGOKNQDK.js} +2 -2
- package/dist/{chunk-HNU5CZ7E.js → chunk-CHI37EHP.js} +6 -6
- package/dist/{chunk-SFDKLEIY.js → chunk-CSLK7BN6.js} +2 -2
- package/dist/{chunk-6O2VUWV6.js → chunk-EIFYYHAF.js} +2 -2
- package/dist/{chunk-7AUC2YSU.js → chunk-GBA4N2SG.js} +16 -16
- package/dist/{chunk-V3NKVYUD.js → chunk-GUD6FYQG.js} +2 -2
- package/dist/{chunk-YY5FM5M7.js → chunk-HO7IQP7G.js} +3 -3
- package/dist/{chunk-EUYNHJTK.js → chunk-HP4CU2ZA.js} +2 -2
- package/dist/{chunk-GS6LTJFW.js → chunk-HRZZJ4XB.js} +2 -2
- package/dist/{chunk-KM2HQZR3.js → chunk-I2KEH2LS.js} +2 -2
- package/dist/{chunk-GYLDYYRA.js → chunk-I6WFBNA4.js} +4 -4
- package/dist/{chunk-S3BB34NI.js → chunk-IHCYCE6D.js} +3 -3
- package/dist/{chunk-PXDEIJL5.js → chunk-IXYIW3MZ.js} +2 -2
- package/dist/{chunk-BOT276JC.js → chunk-JSUA4YFE.js} +2 -2
- package/dist/{chunk-7LCGFUMB.js → chunk-KQAFEZQX.js} +4 -4
- package/dist/{chunk-WWUFQ3AG.js → chunk-KRTIHXWC.js} +2 -2
- package/dist/{chunk-VH76UQK3.js → chunk-KW77LGDX.js} +2 -2
- package/dist/{chunk-DX3ZODO3.js → chunk-L6DJJEFW.js} +2 -2
- package/dist/{chunk-FF4KLI5W.js → chunk-LDPPTPO5.js} +8 -1
- package/dist/chunk-LDPPTPO5.js.map +1 -0
- package/dist/{chunk-CRZ7BU6R.js → chunk-LMMC55WM.js} +2 -2
- package/dist/{chunk-3I375S4J.js → chunk-ME2LMFHF.js} +42 -42
- package/dist/{chunk-DHQUQ34W.js → chunk-MYYHACOW.js} +2 -2
- package/dist/{chunk-BKPV47BK.js → chunk-NGOZH7R5.js} +27 -27
- package/dist/{chunk-DPX3J2N4.js → chunk-NLDMTXWV.js} +2 -2
- package/dist/{chunk-U46P4UAR.js → chunk-NQHSXHJP.js} +4 -4
- package/dist/{chunk-FB6FQT4P.js → chunk-NXVD46HT.js} +2 -2
- package/dist/{chunk-QPG72IPU.js → chunk-O24EWFPQ.js} +3 -3
- package/dist/{chunk-DDPPV3KA.js → chunk-O6AYMAPJ.js} +2 -2
- package/dist/{chunk-JLUS56YZ.js → chunk-OV7EYM6M.js} +2 -2
- package/dist/{chunk-WIVBNNA3.js → chunk-OWHPA4JS.js} +2 -2
- package/dist/{chunk-3UP6NP4C.js → chunk-OZVHLBH6.js} +2 -2
- package/dist/{chunk-QHHWJVVW.js → chunk-PNFWIB4X.js} +2 -2
- package/dist/{chunk-AM4T6CHX.js → chunk-Q4E24IRR.js} +9 -8
- package/dist/chunk-Q4E24IRR.js.map +1 -0
- package/dist/{chunk-SBBZXDVJ.js → chunk-Q6GI7HEC.js} +2 -2
- package/dist/{chunk-TFA6HPOA.js → chunk-QCP75JGO.js} +5 -5
- package/dist/{chunk-OJP3N7UK.js → chunk-R7WYAXPO.js} +2 -2
- package/dist/{chunk-6DNMUYEE.js → chunk-RIEGCK2W.js} +2 -2
- package/dist/{chunk-BCTDTVSR.js → chunk-RKSGUMVL.js} +2 -2
- package/dist/{chunk-GZRTH3WZ.js → chunk-RXT6UB25.js} +127 -6
- package/dist/chunk-RXT6UB25.js.map +1 -0
- package/dist/{chunk-47I2EK6G.js → chunk-SIYZDT2V.js} +3 -3
- package/dist/{chunk-5R4YPRAR.js → chunk-TGF43K66.js} +18 -18
- package/dist/{chunk-UP3XXGEN.js → chunk-TNVZCLJU.js} +2 -2
- package/dist/{chunk-RGGXSN5A.js → chunk-U6HWKOGD.js} +2 -2
- package/dist/{chunk-RWZXVCGX.js → chunk-UK5EH7AI.js} +4 -4
- package/dist/{chunk-DYZHVVJM.js → chunk-UVVSOSHL.js} +36 -36
- package/dist/{chunk-NUSQ3YEL.js → chunk-UXIZ5XN5.js} +2 -2
- package/dist/{chunk-DB6XFMOF.js → chunk-VC4J4UTI.js} +5 -5
- package/dist/{chunk-FYX2LXIX.js → chunk-WI37MWRZ.js} +2 -2
- package/dist/{chunk-P5FPO2BR.js → chunk-WWDO7ZWM.js} +3 -3
- package/dist/{chunk-GZ2KV2RO.js → chunk-WWLAKRMX.js} +4 -4
- package/dist/{chunk-QJWZJ7HL.js → chunk-X6X4UASX.js} +7 -5
- package/dist/chunk-X6X4UASX.js.map +1 -0
- package/dist/{chunk-NVZRIHW5.js → chunk-Y2ADLCB3.js} +7 -7
- package/dist/{chunk-VKQI47DJ.js → chunk-Y7427SL7.js} +4 -4
- package/dist/{chunk-LQGX5TCI.js → chunk-YQSWNFZE.js} +2 -2
- package/dist/{chunk-7DB7KYSR.js → chunk-ZS7RK6H6.js} +4 -4
- package/dist/cli.js +75 -74
- package/dist/coding/export-okf-codegraph.js +7 -7
- package/dist/compounding/engine.js +8 -8
- package/dist/config.js +12 -12
- package/dist/connectors/codex-materialize-runner.js +8 -8
- package/dist/connectors/index.js +8 -8
- package/dist/convergence-refresh.js +10 -10
- package/dist/conversation-index/backend.js +2 -2
- package/dist/corpus-watermark.js +10 -10
- package/dist/deep-recall-http-glue.js +9 -9
- package/dist/entity-retrieval.js +8 -8
- package/dist/explicit-capture.js +3 -3
- package/dist/external-wiki-access.js +41 -40
- package/dist/extraction.js +8 -8
- package/dist/harmonic-retrieval.d.ts.map +1 -1
- package/dist/harmonic-retrieval.js +91 -1
- package/dist/index.js +94 -93
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.js +2 -2
- package/dist/location/index.js +10 -10
- package/dist/maintenance/memory-governance.js +8 -8
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +8 -8
- package/dist/maintenance/rebuild-memory-projection.d.ts.map +1 -1
- package/dist/maintenance/rebuild-memory-projection.js +9 -9
- package/dist/memory-browse-operations.js +41 -40
- package/dist/memory-projection-batch.d.ts +20 -0
- package/dist/memory-projection-batch.d.ts.map +1 -0
- package/dist/memory-projection-batch.js +11 -0
- package/dist/memory-projection-batch.js.map +1 -0
- package/dist/namespaces/migrate.js +9 -9
- package/dist/namespaces/search.js +4 -4
- package/dist/namespaces/storage.js +8 -8
- package/dist/offline-sync-changeset-generations.js +3 -3
- package/dist/offline-sync-embedding-generation.js +2 -2
- package/dist/offline-sync-snapshot-stream.js +6 -6
- package/dist/offline-sync.js +5 -5
- package/dist/operator-doctor-corpus.js +11 -11
- package/dist/operator-toolkit.js +21 -21
- package/dist/orchestration/maintenance.js +16 -16
- package/dist/orchestrator.js +91 -90
- package/dist/recall-navigation-operations.js +41 -40
- package/dist/recall-navigation.js +40 -39
- package/dist/resume-bundles.js +15 -15
- package/dist/search/factory.js +3 -3
- package/dist/search/index.js +3 -3
- package/dist/semantic-consolidation.js +9 -9
- package/dist/semantic-rule-promotion.js +8 -8
- package/dist/semantic-rule-verifier.js +8 -8
- package/dist/session-identity.js +2 -2
- package/dist/session-transcript-migration.js +3 -3
- package/dist/source-agent-qualifier.js +8 -8
- package/dist/standup/build.js +8 -8
- package/dist/storage-paths.d.ts +11 -0
- package/dist/storage-paths.d.ts.map +1 -1
- package/dist/storage-paths.js +3 -1
- package/dist/storage.d.ts +1 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +7 -7
- package/dist/summarizer.js +4 -4
- package/dist/summary-snapshot.js +2 -2
- package/dist/support-passport/index.js +43 -42
- package/dist/threading.js +2 -2
- package/dist/transcript.d.ts +3 -3
- package/dist/transcript.d.ts.map +1 -1
- package/dist/transcript.js +3 -3
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/capsule-import.js +2 -2
- package/dist/transfer/export-okf.js +8 -8
- package/dist/verified-recall.js +8 -8
- package/dist/who-knows-cli.js +9 -9
- package/dist/who-knows.js +8 -8
- package/dist/write-quarantine.js +2 -2
- package/package.json +2 -2
- package/src/harmonic-retrieval-index.test.ts +387 -0
- package/src/harmonic-retrieval.ts +181 -15
- package/src/maintenance/rebuild-memory-projection.ts +3 -0
- package/src/memory-projection-batch.ts +82 -0
- package/src/storage-paths.ts +18 -0
- package/src/storage.ts +1 -1
- package/src/transcript-recent-unscoped.test.ts +139 -0
- package/src/transcript.ts +6 -6
- package/dist/chunk-AM4T6CHX.js.map +0 -1
- package/dist/chunk-FF4KLI5W.js.map +0 -1
- package/dist/chunk-GZRTH3WZ.js.map +0 -1
- package/dist/chunk-HPEONBM3.js.map +0 -1
- package/dist/chunk-QJWZJ7HL.js.map +0 -1
- /package/dist/{chunk-A7V7GZ7F.js.map → chunk-2DHJTJIY.js.map} +0 -0
- /package/dist/{chunk-MJ737UIA.js.map → chunk-2NV7MMMM.js.map} +0 -0
- /package/dist/{chunk-34TSXVQS.js.map → chunk-2QZFZNXO.js.map} +0 -0
- /package/dist/{chunk-TVPDF6CZ.js.map → chunk-44PBRUXB.js.map} +0 -0
- /package/dist/{chunk-YX5F4V7H.js.map → chunk-4DDIWRT7.js.map} +0 -0
- /package/dist/{chunk-EPFNPPWH.js.map → chunk-4NRZ7BRS.js.map} +0 -0
- /package/dist/{chunk-VSPBXHGR.js.map → chunk-5QXMESS7.js.map} +0 -0
- /package/dist/{chunk-S4DDLTPX.js.map → chunk-5Y43VGYB.js.map} +0 -0
- /package/dist/{chunk-VXCTQ3B3.js.map → chunk-6AA7XFMO.js.map} +0 -0
- /package/dist/{chunk-LI3OM3NP.js.map → chunk-6GU3TNJJ.js.map} +0 -0
- /package/dist/{chunk-B2UCJX76.js.map → chunk-7KICGR5W.js.map} +0 -0
- /package/dist/{chunk-UT7WE7RM.js.map → chunk-AUWMKDKL.js.map} +0 -0
- /package/dist/{chunk-U6VC6RP3.js.map → chunk-AWLQNLTM.js.map} +0 -0
- /package/dist/{chunk-PJSK6OJM.js.map → chunk-BANSBRNX.js.map} +0 -0
- /package/dist/{chunk-2TCNEJ36.js.map → chunk-BNMI3JBV.js.map} +0 -0
- /package/dist/{chunk-LTDWZ2HC.js.map → chunk-CFCEJNOE.js.map} +0 -0
- /package/dist/{chunk-R6NHHKXE.js.map → chunk-CGOKNQDK.js.map} +0 -0
- /package/dist/{chunk-HNU5CZ7E.js.map → chunk-CHI37EHP.js.map} +0 -0
- /package/dist/{chunk-SFDKLEIY.js.map → chunk-CSLK7BN6.js.map} +0 -0
- /package/dist/{chunk-6O2VUWV6.js.map → chunk-EIFYYHAF.js.map} +0 -0
- /package/dist/{chunk-7AUC2YSU.js.map → chunk-GBA4N2SG.js.map} +0 -0
- /package/dist/{chunk-V3NKVYUD.js.map → chunk-GUD6FYQG.js.map} +0 -0
- /package/dist/{chunk-YY5FM5M7.js.map → chunk-HO7IQP7G.js.map} +0 -0
- /package/dist/{chunk-EUYNHJTK.js.map → chunk-HP4CU2ZA.js.map} +0 -0
- /package/dist/{chunk-GS6LTJFW.js.map → chunk-HRZZJ4XB.js.map} +0 -0
- /package/dist/{chunk-KM2HQZR3.js.map → chunk-I2KEH2LS.js.map} +0 -0
- /package/dist/{chunk-GYLDYYRA.js.map → chunk-I6WFBNA4.js.map} +0 -0
- /package/dist/{chunk-S3BB34NI.js.map → chunk-IHCYCE6D.js.map} +0 -0
- /package/dist/{chunk-PXDEIJL5.js.map → chunk-IXYIW3MZ.js.map} +0 -0
- /package/dist/{chunk-BOT276JC.js.map → chunk-JSUA4YFE.js.map} +0 -0
- /package/dist/{chunk-7LCGFUMB.js.map → chunk-KQAFEZQX.js.map} +0 -0
- /package/dist/{chunk-WWUFQ3AG.js.map → chunk-KRTIHXWC.js.map} +0 -0
- /package/dist/{chunk-VH76UQK3.js.map → chunk-KW77LGDX.js.map} +0 -0
- /package/dist/{chunk-DX3ZODO3.js.map → chunk-L6DJJEFW.js.map} +0 -0
- /package/dist/{chunk-CRZ7BU6R.js.map → chunk-LMMC55WM.js.map} +0 -0
- /package/dist/{chunk-3I375S4J.js.map → chunk-ME2LMFHF.js.map} +0 -0
- /package/dist/{chunk-DHQUQ34W.js.map → chunk-MYYHACOW.js.map} +0 -0
- /package/dist/{chunk-BKPV47BK.js.map → chunk-NGOZH7R5.js.map} +0 -0
- /package/dist/{chunk-DPX3J2N4.js.map → chunk-NLDMTXWV.js.map} +0 -0
- /package/dist/{chunk-U46P4UAR.js.map → chunk-NQHSXHJP.js.map} +0 -0
- /package/dist/{chunk-FB6FQT4P.js.map → chunk-NXVD46HT.js.map} +0 -0
- /package/dist/{chunk-QPG72IPU.js.map → chunk-O24EWFPQ.js.map} +0 -0
- /package/dist/{chunk-DDPPV3KA.js.map → chunk-O6AYMAPJ.js.map} +0 -0
- /package/dist/{chunk-JLUS56YZ.js.map → chunk-OV7EYM6M.js.map} +0 -0
- /package/dist/{chunk-WIVBNNA3.js.map → chunk-OWHPA4JS.js.map} +0 -0
- /package/dist/{chunk-3UP6NP4C.js.map → chunk-OZVHLBH6.js.map} +0 -0
- /package/dist/{chunk-QHHWJVVW.js.map → chunk-PNFWIB4X.js.map} +0 -0
- /package/dist/{chunk-SBBZXDVJ.js.map → chunk-Q6GI7HEC.js.map} +0 -0
- /package/dist/{chunk-TFA6HPOA.js.map → chunk-QCP75JGO.js.map} +0 -0
- /package/dist/{chunk-OJP3N7UK.js.map → chunk-R7WYAXPO.js.map} +0 -0
- /package/dist/{chunk-6DNMUYEE.js.map → chunk-RIEGCK2W.js.map} +0 -0
- /package/dist/{chunk-BCTDTVSR.js.map → chunk-RKSGUMVL.js.map} +0 -0
- /package/dist/{chunk-47I2EK6G.js.map → chunk-SIYZDT2V.js.map} +0 -0
- /package/dist/{chunk-5R4YPRAR.js.map → chunk-TGF43K66.js.map} +0 -0
- /package/dist/{chunk-UP3XXGEN.js.map → chunk-TNVZCLJU.js.map} +0 -0
- /package/dist/{chunk-RGGXSN5A.js.map → chunk-U6HWKOGD.js.map} +0 -0
- /package/dist/{chunk-RWZXVCGX.js.map → chunk-UK5EH7AI.js.map} +0 -0
- /package/dist/{chunk-DYZHVVJM.js.map → chunk-UVVSOSHL.js.map} +0 -0
- /package/dist/{chunk-NUSQ3YEL.js.map → chunk-UXIZ5XN5.js.map} +0 -0
- /package/dist/{chunk-DB6XFMOF.js.map → chunk-VC4J4UTI.js.map} +0 -0
- /package/dist/{chunk-FYX2LXIX.js.map → chunk-WI37MWRZ.js.map} +0 -0
- /package/dist/{chunk-P5FPO2BR.js.map → chunk-WWDO7ZWM.js.map} +0 -0
- /package/dist/{chunk-GZ2KV2RO.js.map → chunk-WWLAKRMX.js.map} +0 -0
- /package/dist/{chunk-NVZRIHW5.js.map → chunk-Y2ADLCB3.js.map} +0 -0
- /package/dist/{chunk-VKQI47DJ.js.map → chunk-Y7427SL7.js.map} +0 -0
- /package/dist/{chunk-LQGX5TCI.js.map → chunk-YQSWNFZE.js.map} +0 -0
- /package/dist/{chunk-7DB7KYSR.js.map → chunk-ZS7RK6H6.js.map} +0 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, rename, rm, symlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { getMemoryProjectionPath } from "./memory-projection-store.js";
|
|
7
|
+
import { openBetterSqlite3 } from "./runtime/better-sqlite.js";
|
|
8
|
+
import { recordAbstractionNode } from "./abstraction-nodes.js";
|
|
9
|
+
import { searchHarmonicRetrieval, type HarmonicRetrievalResult } from "./harmonic-retrieval.js";
|
|
10
|
+
import { rebuildMemoryProjection } from "./maintenance/rebuild-memory-projection.js";
|
|
11
|
+
import { StorageManager } from "./storage.js";
|
|
12
|
+
|
|
13
|
+
test("indexed harmonic projection matches the corpus scan and still sees later sources", async () => {
|
|
14
|
+
const memoryDir = await mkdtemp(path.join(tmpdir(), "remnic-harmonic-index-"));
|
|
15
|
+
try {
|
|
16
|
+
const storage = new StorageManager(memoryDir);
|
|
17
|
+
await storage.ensureDirectories();
|
|
18
|
+
const active = await storage.writeMemory("fact", "active harmonic source payload", { source: "test" });
|
|
19
|
+
const archived = await storage.writeMemory("fact", "archived harmonic source payload", { source: "test" });
|
|
20
|
+
await storage.updateMemoryFrontmatter(archived.id, { status: "archived" });
|
|
21
|
+
await recordAbstractionNode({
|
|
22
|
+
memoryDir,
|
|
23
|
+
node: {
|
|
24
|
+
schemaVersion: 1,
|
|
25
|
+
nodeId: "mixed-sources",
|
|
26
|
+
recordedAt: "2026-10-12T00:00:00.000Z",
|
|
27
|
+
sessionKey: "agent:index",
|
|
28
|
+
kind: "topic",
|
|
29
|
+
abstractionLevel: "meso",
|
|
30
|
+
title: "stored title",
|
|
31
|
+
summary: "stored summary",
|
|
32
|
+
sourceMemoryIds: [active.id, archived.id, "missing-source"],
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const beforeIndex = await searchHarmonicRetrieval({
|
|
37
|
+
memoryDir,
|
|
38
|
+
query: "active harmonic source payload",
|
|
39
|
+
maxResults: 5,
|
|
40
|
+
anchorsEnabled: false,
|
|
41
|
+
});
|
|
42
|
+
await rebuildMemoryProjection({ memoryDir, storage, dryRun: false });
|
|
43
|
+
const originalReadAll = StorageManager.prototype.readAllMemories;
|
|
44
|
+
let corpusScans = 0;
|
|
45
|
+
StorageManager.prototype.readAllMemories = function (this: StorageManager, ...args: unknown[]) {
|
|
46
|
+
corpusScans += 1;
|
|
47
|
+
return Reflect.apply(originalReadAll, this, args);
|
|
48
|
+
};
|
|
49
|
+
let afterIndex: HarmonicRetrievalResult[];
|
|
50
|
+
try {
|
|
51
|
+
afterIndex = await searchHarmonicRetrieval({
|
|
52
|
+
memoryDir,
|
|
53
|
+
query: "active harmonic source payload",
|
|
54
|
+
maxResults: 5,
|
|
55
|
+
anchorsEnabled: false,
|
|
56
|
+
});
|
|
57
|
+
} finally {
|
|
58
|
+
StorageManager.prototype.readAllMemories = originalReadAll;
|
|
59
|
+
}
|
|
60
|
+
assert.equal(corpusScans, 0, "an openable fresh index must not scan the hot corpus");
|
|
61
|
+
assert.deepEqual(afterIndex, beforeIndex);
|
|
62
|
+
assert.deepEqual(afterIndex.map((result) => result.node.sourceMemoryIds), [[active.id]]);
|
|
63
|
+
assert.equal(afterIndex[0]?.node.title.includes("active harmonic source payload"), true);
|
|
64
|
+
|
|
65
|
+
const late = await storage.writeMemory("fact", "late harmonic source payload", { source: "test" });
|
|
66
|
+
await recordAbstractionNode({
|
|
67
|
+
memoryDir,
|
|
68
|
+
node: {
|
|
69
|
+
schemaVersion: 1,
|
|
70
|
+
nodeId: "late-source",
|
|
71
|
+
recordedAt: "2026-10-12T01:00:00.000Z",
|
|
72
|
+
sessionKey: "agent:index",
|
|
73
|
+
kind: "topic",
|
|
74
|
+
abstractionLevel: "meso",
|
|
75
|
+
title: "late title",
|
|
76
|
+
summary: "late summary",
|
|
77
|
+
sourceMemoryIds: [late.id],
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
const lateResults = await searchHarmonicRetrieval({
|
|
81
|
+
memoryDir,
|
|
82
|
+
query: "late harmonic source payload",
|
|
83
|
+
maxResults: 5,
|
|
84
|
+
anchorsEnabled: false,
|
|
85
|
+
});
|
|
86
|
+
assert.equal(lateResults.some((result) => result.node.nodeId === "late-source" && result.node.sourceMemoryIds?.includes(late.id)), true);
|
|
87
|
+
|
|
88
|
+
const coldMemory = (await storage.readAllMemories()).find((memory) => memory.frontmatter.id === active.id);
|
|
89
|
+
if (!coldMemory) throw new Error("active source missing before cold move");
|
|
90
|
+
const coldPath = path.join(memoryDir, "cold", path.relative(memoryDir, coldMemory.path));
|
|
91
|
+
await mkdir(path.dirname(coldPath), { recursive: true });
|
|
92
|
+
await rename(coldMemory.path, coldPath);
|
|
93
|
+
storage.invalidateMemoryCachesForTiers(["hot", "cold"]);
|
|
94
|
+
const coldResults = await searchHarmonicRetrieval({
|
|
95
|
+
memoryDir,
|
|
96
|
+
query: "active harmonic source payload",
|
|
97
|
+
maxResults: 5,
|
|
98
|
+
anchorsEnabled: false,
|
|
99
|
+
});
|
|
100
|
+
assert.equal(coldResults.some((result) => result.node.sourceMemoryIds?.includes(active.id)), true);
|
|
101
|
+
} finally {
|
|
102
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
function setRebuiltAt(memoryDir: string, iso: string): void {
|
|
107
|
+
const db = openBetterSqlite3(getMemoryProjectionPath(memoryDir));
|
|
108
|
+
try {
|
|
109
|
+
db.prepare("UPDATE meta SET value = ? WHERE key = 'rebuiltAt'").run(iso);
|
|
110
|
+
} finally {
|
|
111
|
+
db.close();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function nodeFor(memoryDir: string, nodeId: string, sourceMemoryIds: string[], title: string) {
|
|
116
|
+
await recordAbstractionNode({
|
|
117
|
+
memoryDir,
|
|
118
|
+
node: {
|
|
119
|
+
schemaVersion: 1,
|
|
120
|
+
nodeId,
|
|
121
|
+
recordedAt: "2026-10-12T00:00:00.000Z",
|
|
122
|
+
sessionKey: "agent:index",
|
|
123
|
+
kind: "topic",
|
|
124
|
+
abstractionLevel: "meso",
|
|
125
|
+
title,
|
|
126
|
+
summary: title,
|
|
127
|
+
sourceMemoryIds,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const search = (memoryDir: string, query: string) =>
|
|
133
|
+
searchHarmonicRetrieval({ memoryDir, query, maxResults: 5, anchorsEnabled: false });
|
|
134
|
+
|
|
135
|
+
test("a source written after the rebuild stays found beyond two days, and unsafe or queue ids never resolve", async () => {
|
|
136
|
+
const memoryDir = await mkdtemp(path.join(tmpdir(), "remnic-harmonic-index-"));
|
|
137
|
+
try {
|
|
138
|
+
const storage = new StorageManager(memoryDir);
|
|
139
|
+
await storage.ensureDirectories();
|
|
140
|
+
const base = await storage.writeMemory("fact", "baseline harmonic anchor", { source: "test" });
|
|
141
|
+
await rebuildMemoryProjection({ memoryDir, storage, dryRun: false });
|
|
142
|
+
|
|
143
|
+
// Written after the rebuild, then filed under a day three days back; the
|
|
144
|
+
// index was rebuilt four days ago, so that day is inside the probe window.
|
|
145
|
+
const late = await storage.writeMemory("fact", "aged unindexed zebra source", { source: "test" });
|
|
146
|
+
const lateMemory = (await storage.readAllMemories()).find((m) => m.frontmatter.id === late.id);
|
|
147
|
+
if (!lateMemory) throw new Error("late source missing");
|
|
148
|
+
const day = new Date(Date.now() - 3 * 86_400_000).toISOString().slice(0, 10);
|
|
149
|
+
const agedPath = path.join(memoryDir, "facts", day, `${late.id}.md`);
|
|
150
|
+
await mkdir(path.dirname(agedPath), { recursive: true });
|
|
151
|
+
await rename(lateMemory.path, agedPath);
|
|
152
|
+
storage.invalidateMemoryCachesForTiers(["hot", "cold"]);
|
|
153
|
+
setRebuiltAt(memoryDir, new Date(Date.now() - 4 * 86_400_000).toISOString());
|
|
154
|
+
await nodeFor(memoryDir, "aged-node", [late.id], "aged unindexed zebra source");
|
|
155
|
+
const aged = await search(memoryDir, "aged unindexed zebra source");
|
|
156
|
+
assert.equal(aged.some((r) => r.node.nodeId === "aged-node" && r.node.sourceMemoryIds?.includes(late.id)), true);
|
|
157
|
+
|
|
158
|
+
// Path traversal ids and question-queue ids are dropped, exactly as the
|
|
159
|
+
// corpus scan drops them.
|
|
160
|
+
const questionId = await storage.writeQuestion("queued zebra question", "ctx", 1);
|
|
161
|
+
await nodeFor(memoryDir, "unsafe-node", ["../facts/" + base.id, "../../etc/passwd"], "unsafe traversal cue");
|
|
162
|
+
await nodeFor(memoryDir, "question-node", [questionId], "queue leak cue");
|
|
163
|
+
setRebuiltAt(memoryDir, new Date().toISOString());
|
|
164
|
+
for (const query of ["unsafe traversal cue", "queue leak cue"]) {
|
|
165
|
+
assert.deepEqual(await search(memoryDir, query), [], query);
|
|
166
|
+
}
|
|
167
|
+
} finally {
|
|
168
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("an index older than a week falls back to the corpus scan and still finds an aged source", async () => {
|
|
173
|
+
const memoryDir = await mkdtemp(path.join(tmpdir(), "remnic-harmonic-index-"));
|
|
174
|
+
try {
|
|
175
|
+
const storage = new StorageManager(memoryDir);
|
|
176
|
+
await storage.ensureDirectories();
|
|
177
|
+
const source = await storage.writeMemory("fact", "stale index quokka source", { source: "test" });
|
|
178
|
+
await rebuildMemoryProjection({ memoryDir, storage, dryRun: false });
|
|
179
|
+
setRebuiltAt(memoryDir, new Date(Date.now() - 30 * 86_400_000).toISOString());
|
|
180
|
+
await nodeFor(memoryDir, "stale-node", [source.id], "stale index quokka source");
|
|
181
|
+
const results = await search(memoryDir, "stale index quokka source");
|
|
182
|
+
assert.equal(results.some((r) => r.node.nodeId === "stale-node"), true);
|
|
183
|
+
} finally {
|
|
184
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Counts hot-corpus scans while `run` executes.
|
|
189
|
+
async function countCorpusScans<T>(run: () => Promise<T>): Promise<{ value: T; scans: number }> {
|
|
190
|
+
const original = StorageManager.prototype.readAllMemories;
|
|
191
|
+
let scans = 0;
|
|
192
|
+
StorageManager.prototype.readAllMemories = function (this: StorageManager, ...args: unknown[]) {
|
|
193
|
+
scans += 1;
|
|
194
|
+
return Reflect.apply(original, this, args);
|
|
195
|
+
};
|
|
196
|
+
try {
|
|
197
|
+
return { value: await run(), scans };
|
|
198
|
+
} finally {
|
|
199
|
+
StorageManager.prototype.readAllMemories = original;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
test("a scoped or pre-complete projection is not trusted and falls back to the corpus scan", async () => {
|
|
204
|
+
const memoryDir = await mkdtemp(path.join(tmpdir(), "remnic-harmonic-index-"));
|
|
205
|
+
try {
|
|
206
|
+
const storage = new StorageManager(memoryDir);
|
|
207
|
+
await storage.ensureDirectories();
|
|
208
|
+
const source = await storage.writeMemory("fact", "scoped rebuild okapi source", { source: "test" });
|
|
209
|
+
await nodeFor(memoryDir, "okapi-node", [source.id], "scoped rebuild okapi source");
|
|
210
|
+
|
|
211
|
+
// First projection built with a scope that excludes the source: rebuiltAt is
|
|
212
|
+
// fresh but memory_current does not hold it.
|
|
213
|
+
await rebuildMemoryProjection({ memoryDir, storage, dryRun: false, updatedBefore: "2000-01-01T00:00:00.000Z" });
|
|
214
|
+
const scoped = await countCorpusScans(() => search(memoryDir, "scoped rebuild okapi source"));
|
|
215
|
+
assert.equal(scoped.value.some((r) => r.node.nodeId === "okapi-node"), true, "out-of-scope source must still resolve");
|
|
216
|
+
assert.equal(scoped.scans > 0, true, "an incomplete index must fall back to the scan");
|
|
217
|
+
|
|
218
|
+
// An unscoped rebuild is complete and takes the index path.
|
|
219
|
+
await rebuildMemoryProjection({ memoryDir, storage, dryRun: false });
|
|
220
|
+
const full = await countCorpusScans(() => search(memoryDir, "scoped rebuild okapi source"));
|
|
221
|
+
assert.equal(full.value.some((r) => r.node.nodeId === "okapi-node"), true);
|
|
222
|
+
assert.equal(full.scans, 0);
|
|
223
|
+
|
|
224
|
+
// A projection written before the `complete` flag existed has no such key.
|
|
225
|
+
const db = openBetterSqlite3(getMemoryProjectionPath(memoryDir));
|
|
226
|
+
try {
|
|
227
|
+
db.prepare("DELETE FROM meta WHERE key = 'complete'").run();
|
|
228
|
+
} finally {
|
|
229
|
+
db.close();
|
|
230
|
+
}
|
|
231
|
+
const legacy = await countCorpusScans(() => search(memoryDir, "scoped rebuild okapi source"));
|
|
232
|
+
assert.equal(legacy.value.some((r) => r.node.nodeId === "okapi-node"), true);
|
|
233
|
+
assert.equal(legacy.scans > 0, true, "a projection without the flag must fall back");
|
|
234
|
+
} finally {
|
|
235
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("a legacy memory whose id contains a slash still resolves through the projection", async () => {
|
|
240
|
+
const memoryDir = await mkdtemp(path.join(tmpdir(), "remnic-harmonic-index-"));
|
|
241
|
+
try {
|
|
242
|
+
const storage = new StorageManager(memoryDir);
|
|
243
|
+
await storage.ensureDirectories();
|
|
244
|
+
const slashId = "legacy/slash-id";
|
|
245
|
+
const day = new Date().toISOString().slice(0, 10);
|
|
246
|
+
await mkdir(path.join(memoryDir, "facts", day), { recursive: true });
|
|
247
|
+
await writeFile(
|
|
248
|
+
path.join(memoryDir, "facts", day, "legacy-slash-file.md"),
|
|
249
|
+
`---\nid: ${slashId}\ncategory: fact\ncreated: ${new Date().toISOString()}\nupdated: ${new Date().toISOString()}\nsource: test\nconfidence: 0.8\nconfidenceTier: implied\ntags: ["alpha"]\n---\n\nslash identifier narwhal source\n`,
|
|
250
|
+
"utf-8",
|
|
251
|
+
);
|
|
252
|
+
await nodeFor(memoryDir, "slash-node", [slashId], "slash identifier narwhal source");
|
|
253
|
+
const before = await search(memoryDir, "slash identifier narwhal source");
|
|
254
|
+
assert.equal(before.some((r) => r.node.nodeId === "slash-node"), true, "the corpus scan keys by frontmatter id");
|
|
255
|
+
await rebuildMemoryProjection({ memoryDir, storage, dryRun: false });
|
|
256
|
+
const indexed = await countCorpusScans(() => search(memoryDir, "slash identifier narwhal source"));
|
|
257
|
+
assert.deepEqual(indexed.value, before);
|
|
258
|
+
assert.equal(indexed.scans, 0);
|
|
259
|
+
} finally {
|
|
260
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
function memoryFileText(id: string, body: string): string {
|
|
265
|
+
const now = new Date().toISOString();
|
|
266
|
+
return `---\nid: ${id}\ncategory: fact\ncreated: ${now}\nupdated: ${now}\nsource: test\nconfidence: 0.8\nconfidenceTier: implied\ntags: ["alpha"]\n---\n\n${body}\n`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
test("direct probes reject symlinked and artifact sources like the scan, and an old-dated post-rebuild source is still found", async () => {
|
|
270
|
+
const memoryDir = await mkdtemp(path.join(tmpdir(), "remnic-harmonic-index-"));
|
|
271
|
+
const outsideDir = await mkdtemp(path.join(tmpdir(), "remnic-harmonic-outside-"));
|
|
272
|
+
try {
|
|
273
|
+
const storage = new StorageManager(memoryDir);
|
|
274
|
+
await storage.ensureDirectories();
|
|
275
|
+
const day = new Date().toISOString().slice(0, 10);
|
|
276
|
+
await mkdir(path.join(memoryDir, "facts", day), { recursive: true });
|
|
277
|
+
await mkdir(path.join(memoryDir, "artifacts", day), { recursive: true });
|
|
278
|
+
|
|
279
|
+
// A source file outside the namespace root, reachable through a symlink.
|
|
280
|
+
await writeFile(path.join(outsideDir, "outside-sym.md"), memoryFileText("outside-sym", "symlink escape cue body"), "utf-8");
|
|
281
|
+
await symlink(path.join(outsideDir, "outside-sym.md"), path.join(memoryDir, "facts", day, "outside-sym.md"));
|
|
282
|
+
// An artifact: outside the recall surface the corpus scan reads.
|
|
283
|
+
await writeFile(path.join(memoryDir, "artifacts", day, "art-1.md"), memoryFileText("art-1", "artifact leak cue body"), "utf-8");
|
|
284
|
+
await nodeFor(memoryDir, "symlink-node", ["outside-sym"], "symlink escape cue");
|
|
285
|
+
await nodeFor(memoryDir, "artifact-node", ["art-1"], "artifact leak cue");
|
|
286
|
+
|
|
287
|
+
const scanSymlink = await search(memoryDir, "symlink escape cue");
|
|
288
|
+
const scanArtifact = await search(memoryDir, "artifact leak cue");
|
|
289
|
+
assert.deepEqual(scanSymlink, [], "the corpus scan must not follow the symlink");
|
|
290
|
+
assert.deepEqual(scanArtifact, [], "the corpus scan must not read artifacts");
|
|
291
|
+
|
|
292
|
+
await rebuildMemoryProjection({ memoryDir, storage, dryRun: false });
|
|
293
|
+
const indexed = await countCorpusScans(async () => ({
|
|
294
|
+
symlink: await search(memoryDir, "symlink escape cue"),
|
|
295
|
+
artifact: await search(memoryDir, "artifact leak cue"),
|
|
296
|
+
}));
|
|
297
|
+
assert.deepEqual(indexed.value.symlink, scanSymlink);
|
|
298
|
+
assert.deepEqual(indexed.value.artifact, scanArtifact);
|
|
299
|
+
assert.equal(indexed.scans, 0, "this was the index path, not a fallback");
|
|
300
|
+
|
|
301
|
+
// Written after the rebuild but filed under a date far outside the probe
|
|
302
|
+
// window (an import that keeps its original created date).
|
|
303
|
+
await mkdir(path.join(memoryDir, "facts", "2020-01-01"), { recursive: true });
|
|
304
|
+
await writeFile(
|
|
305
|
+
path.join(memoryDir, "facts", "2020-01-01", "imported-old.md"),
|
|
306
|
+
memoryFileText("imported-old", "imported older marmot cue body"),
|
|
307
|
+
"utf-8",
|
|
308
|
+
);
|
|
309
|
+
storage.invalidateMemoryCachesForTiers(["hot", "cold"]);
|
|
310
|
+
await nodeFor(memoryDir, "imported-node", ["imported-old"], "imported older marmot cue");
|
|
311
|
+
const old = await search(memoryDir, "imported older marmot cue");
|
|
312
|
+
assert.equal(old.some((r) => r.node.nodeId === "imported-node" && r.node.sourceMemoryIds?.includes("imported-old")), true);
|
|
313
|
+
} finally {
|
|
314
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
315
|
+
await rm(outsideDir, { recursive: true, force: true });
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("a dangling source id costs one path walk, then is remembered as absent", async () => {
|
|
320
|
+
const memoryDir = await mkdtemp(path.join(tmpdir(), "remnic-harmonic-index-"));
|
|
321
|
+
try {
|
|
322
|
+
const storage = new StorageManager(memoryDir);
|
|
323
|
+
await storage.ensureDirectories();
|
|
324
|
+
const real = await storage.writeMemory("fact", "real source wombat body", { source: "test" });
|
|
325
|
+
await nodeFor(memoryDir, "real-node", [real.id, "dangling-id"], "real source wombat");
|
|
326
|
+
await rebuildMemoryProjection({ memoryDir, storage, dryRun: false });
|
|
327
|
+
|
|
328
|
+
const original = StorageManager.prototype.collectActiveMemoryPaths;
|
|
329
|
+
let walks = 0;
|
|
330
|
+
StorageManager.prototype.collectActiveMemoryPaths = function (this: StorageManager, ...args: unknown[]) {
|
|
331
|
+
walks += 1;
|
|
332
|
+
return Reflect.apply(original, this, args);
|
|
333
|
+
};
|
|
334
|
+
try {
|
|
335
|
+
const first = await search(memoryDir, "real source wombat");
|
|
336
|
+
assert.equal(first.some((r) => r.node.nodeId === "real-node" && r.node.sourceMemoryIds?.includes(real.id)), true);
|
|
337
|
+
assert.equal(walks, 1, "the unresolved id is looked up by one walk");
|
|
338
|
+
await search(memoryDir, "real source wombat");
|
|
339
|
+
assert.equal(walks, 1, "a confirmed-absent id is not walked again within the TTL");
|
|
340
|
+
} finally {
|
|
341
|
+
StorageManager.prototype.collectActiveMemoryPaths = original;
|
|
342
|
+
}
|
|
343
|
+
} finally {
|
|
344
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
test("a separator-containing id written after the rebuild is still placed, by the scan", async () => {
|
|
350
|
+
const memoryDir = await mkdtemp(path.join(tmpdir(), "remnic-harmonic-index-"));
|
|
351
|
+
try {
|
|
352
|
+
const storage = new StorageManager(memoryDir);
|
|
353
|
+
await storage.ensureDirectories();
|
|
354
|
+
await rebuildMemoryProjection({ memoryDir, storage, dryRun: false });
|
|
355
|
+
const day = new Date().toISOString().slice(0, 10);
|
|
356
|
+
await mkdir(path.join(memoryDir, "facts", day), { recursive: true });
|
|
357
|
+
await writeFile(
|
|
358
|
+
path.join(memoryDir, "facts", day, "capsule-import-file.md"),
|
|
359
|
+
memoryFileText("capsule/import-id", "capsule import ocelot source body"),
|
|
360
|
+
"utf-8",
|
|
361
|
+
);
|
|
362
|
+
storage.invalidateMemoryCachesForTiers(["hot", "cold"]);
|
|
363
|
+
await nodeFor(memoryDir, "capsule-node", ["capsule/import-id"], "capsule import ocelot source");
|
|
364
|
+
const results = await search(memoryDir, "capsule import ocelot source");
|
|
365
|
+
assert.equal(results.some((r) => r.node.nodeId === "capsule-node" && r.node.sourceMemoryIds?.includes("capsule/import-id")), true);
|
|
366
|
+
} finally {
|
|
367
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test("the cold path walk honors an aborted signal", async () => {
|
|
372
|
+
const memoryDir = await mkdtemp(path.join(tmpdir(), "remnic-harmonic-index-"));
|
|
373
|
+
try {
|
|
374
|
+
const storage = new StorageManager(memoryDir);
|
|
375
|
+
await storage.ensureDirectories();
|
|
376
|
+
await mkdir(path.join(memoryDir, "cold", "facts", "2020-01-01"), { recursive: true });
|
|
377
|
+
await writeFile(
|
|
378
|
+
path.join(memoryDir, "cold", "facts", "2020-01-01", "cold-1.md"),
|
|
379
|
+
memoryFileText("cold-1", "cold walk abort body"),
|
|
380
|
+
"utf-8",
|
|
381
|
+
);
|
|
382
|
+
assert.equal((await storage.collectColdMemoryPaths()).length, 1);
|
|
383
|
+
await assert.rejects(() => storage.collectColdMemoryPaths({ abortSignal: AbortSignal.abort() }));
|
|
384
|
+
} finally {
|
|
385
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
386
|
+
}
|
|
387
|
+
});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { realpath } from "node:fs/promises";
|
|
2
4
|
import { listJsonFilesStrict, readJsonFile } from "./json-store.js";
|
|
3
5
|
import { throwIfAborted } from "./abort-error.js";
|
|
4
6
|
import { inferMemoryStatus, toMemoryPathRel } from "./memory-lifecycle-ledger-utils.js";
|
|
@@ -13,30 +15,168 @@ import { compareDeterministicStrings } from "./deterministic-order.js";
|
|
|
13
15
|
import { countRecallTokenOverlap, normalizeRecallTokens } from "./recall-tokenization.js";
|
|
14
16
|
import { stripAttributesSuffix } from "./structured-attributes.js";
|
|
15
17
|
import { isValidityExpiredNow } from "./temporal-validity.js";
|
|
16
|
-
import { isRecord } from "./store-contract.js";
|
|
18
|
+
import { assertSafePathSegment, isRecord } from "./store-contract.js";
|
|
17
19
|
import { isSupportPassportPrivateMemory } from "./support-passport/card-projection.js";
|
|
18
20
|
import type { MemoryFile } from "./types.js";
|
|
21
|
+
import { readProjectedMemoryStates } from "./memory-projection-batch.js";
|
|
22
|
+
import { SecureStoreLockedError } from "./secure-store/secure-fs.js";
|
|
23
|
+
import { StorageManager } from "./storage.js";
|
|
24
|
+
import { RECALL_FALLBACK_DIRS } from "./utils/category-dir.js";
|
|
25
|
+
import { isPathInsideStorageRoot } from "./storage-paths.js";
|
|
26
|
+
|
|
19
27
|
|
|
20
28
|
type SourceMemoryMap = Map<string, MemoryFile>;
|
|
21
29
|
|
|
22
|
-
|
|
30
|
+
// `rootReal` set: a direct probe, which skips the corpus walk's symlink
|
|
31
|
+
// hardening, so the resolved file must stay inside the memory root. Null: the
|
|
32
|
+
// path came from the contained corpus walk and is already trusted.
|
|
33
|
+
async function readMemoryFile(
|
|
34
|
+
storage: StorageManager,
|
|
35
|
+
rootReal: string | null,
|
|
36
|
+
filePath: string,
|
|
37
|
+
abortSignal?: AbortSignal,
|
|
38
|
+
): Promise<MemoryFile | null> {
|
|
39
|
+
throwIfAborted(abortSignal, "harmonic retrieval aborted");
|
|
40
|
+
if (!existsSync(filePath)) return null;
|
|
41
|
+
try {
|
|
42
|
+
if (rootReal !== null && !isPathInsideStorageRoot(rootReal, await realpath(filePath))) return null;
|
|
43
|
+
return await storage.readMemoryByPath(filePath);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
throwIfAborted(abortSignal, "harmonic retrieval aborted");
|
|
46
|
+
if (err instanceof SecureStoreLockedError) throw err;
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// An index older than this is treated as unusable: the date window needed to
|
|
52
|
+
// cover "created since the last rebuild" would stop being cheap.
|
|
53
|
+
const INDEX_MAX_AGE_MS = 7 * 86_400_000;
|
|
54
|
+
const DAY_MS = 86_400_000;
|
|
55
|
+
|
|
56
|
+
function utcDaysSince(rebuiltMs: number, nowMs: number): string[] {
|
|
57
|
+
const days: string[] = [];
|
|
58
|
+
for (let day = Math.floor((rebuiltMs - DAY_MS) / DAY_MS) * DAY_MS; day <= nowMs; day += DAY_MS) {
|
|
59
|
+
days.push(new Date(day).toISOString().slice(0, 10));
|
|
60
|
+
}
|
|
61
|
+
return days;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Source ids confirmed absent by a corpus path walk. A node only references a
|
|
65
|
+
// source after the source file exists, so a confirmed absence stays true unless
|
|
66
|
+
// the file is restored later; the short TTL bounds that. Without this, one
|
|
67
|
+
// dangling id (a hard-deleted fact) would force a walk on every recall.
|
|
68
|
+
const ABSENT_SOURCE_TTL_MS = 10 * 60_000;
|
|
69
|
+
const ABSENT_SOURCE_MAX_ENTRIES = 4096;
|
|
70
|
+
const absentSourceIds = new Map<string, number>(); // `${memoryDir}\0${id}` -> expiry ms
|
|
71
|
+
|
|
72
|
+
// Memories the projection has no row for were written after its last rebuild, so
|
|
73
|
+
// their dated directory is normally one of `days`. Hot and cold tiers are probed
|
|
74
|
+
// by exact path. Only recall-corpus categories are probed: `questions/` is a queue
|
|
75
|
+
// and `artifacts/` is outside the recall surface, so neither can be a source.
|
|
76
|
+
function unprojectedCandidatePaths(memoryDir: string, id: string, days: string[]): string[] {
|
|
77
|
+
const paths: string[] = [];
|
|
78
|
+
for (const root of [memoryDir, path.join(memoryDir, "cold")]) {
|
|
79
|
+
for (const dir of RECALL_FALLBACK_DIRS) {
|
|
80
|
+
paths.push(path.join(root, dir, `${id}.md`));
|
|
81
|
+
for (const day of days) paths.push(path.join(root, dir, day, `${id}.md`));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return paths;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolve only the source ids the abstraction nodes reference. Returns null when
|
|
89
|
+
* the projection is absent, unreadable, too old, or not known to cover every
|
|
90
|
+
* active memory (a scoped or pre-`complete` rebuild), so the caller keeps the
|
|
91
|
+
* full-corpus scan. Every file is read from disk, so status, tags and validity
|
|
92
|
+
* are current; the index only says where to look. An id the cheap probes cannot
|
|
93
|
+
* place (an import filed under an old date, a tier root outside the memory dir)
|
|
94
|
+
* is looked up by one contained path-only walk, never a frontmatter parse of the
|
|
95
|
+
* corpus.
|
|
96
|
+
*/
|
|
97
|
+
async function readIndexedSourceMemories(options: {
|
|
98
|
+
memoryDir: string;
|
|
99
|
+
abortSignal?: AbortSignal;
|
|
100
|
+
sourceMemoryIds: string[];
|
|
101
|
+
nowMs: number;
|
|
102
|
+
}): Promise<SourceMemoryMap | null> {
|
|
103
|
+
// The ids go to SQL as bound parameters, so any id the projection holds is safe to look up.
|
|
104
|
+
const batch = readProjectedMemoryStates(options.memoryDir, options.sourceMemoryIds);
|
|
105
|
+
const rebuiltMs = Date.parse(batch?.rebuiltAt ?? "");
|
|
106
|
+
if (!batch?.complete || !Number.isFinite(rebuiltMs) || options.nowMs - rebuiltMs > INDEX_MAX_AGE_MS) return null;
|
|
107
|
+
const rootReal = await realpath(options.memoryDir).catch(() => null);
|
|
108
|
+
if (rootReal === null) return null;
|
|
109
|
+
|
|
110
|
+
const storage = new StorageManager(options.memoryDir);
|
|
111
|
+
const days = utcDaysSince(rebuiltMs, options.nowMs);
|
|
112
|
+
const sourceMemories: SourceMemoryMap = new Map();
|
|
113
|
+
const pending: string[] = [];
|
|
114
|
+
for (const id of options.sourceMemoryIds) {
|
|
115
|
+
const state = batch.states.get(id);
|
|
116
|
+
// An id is only ever joined into a path when it is a single safe segment.
|
|
117
|
+
let safeId = true;
|
|
118
|
+
try {
|
|
119
|
+
assertSafePathSegment(id, "sourceMemoryId");
|
|
120
|
+
} catch {
|
|
121
|
+
safeId = false;
|
|
122
|
+
}
|
|
123
|
+
// An unprojected id that is not a single path segment cannot be probed or
|
|
124
|
+
// matched by filename, and the scan keys sources by frontmatter id, so only
|
|
125
|
+
// the scan can place it. Rare (legacy/imported ids); worst case is the old cost.
|
|
126
|
+
if (!state && !safeId) return null;
|
|
127
|
+
// state.path was containment-checked by the projection reader.
|
|
128
|
+
const candidates = state
|
|
129
|
+
? [state.path, path.join(options.memoryDir, "cold", state.pathRel)]
|
|
130
|
+
: unprojectedCandidatePaths(options.memoryDir, id, days);
|
|
131
|
+
let found = false;
|
|
132
|
+
for (const candidate of candidates) {
|
|
133
|
+
const memory = await readMemoryFile(storage, rootReal, candidate, options.abortSignal);
|
|
134
|
+
if (memory?.frontmatter.id !== id) continue;
|
|
135
|
+
sourceMemories.set(id, memory);
|
|
136
|
+
found = true;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
const absentKey = `${options.memoryDir}\0${id}`;
|
|
140
|
+
if (!found && safeId && (absentSourceIds.get(absentKey) ?? 0) <= options.nowMs) pending.push(id);
|
|
141
|
+
}
|
|
142
|
+
if (pending.length === 0) return sourceMemories;
|
|
143
|
+
|
|
144
|
+
const wanted = new Set(pending);
|
|
145
|
+
const walked = [
|
|
146
|
+
...(await storage.collectActiveMemoryPaths({ abortSignal: options.abortSignal })),
|
|
147
|
+
...(await storage.collectColdMemoryPaths({ abortSignal: options.abortSignal })),
|
|
148
|
+
];
|
|
149
|
+
for (const filePath of walked) {
|
|
150
|
+
throwIfAborted(options.abortSignal, "harmonic retrieval aborted");
|
|
151
|
+
if (!wanted.has(path.basename(filePath, ".md"))) continue;
|
|
152
|
+
const memory = await readMemoryFile(storage, null, filePath, options.abortSignal);
|
|
153
|
+
if (memory && wanted.has(memory.frontmatter.id)) sourceMemories.set(memory.frontmatter.id, memory);
|
|
154
|
+
}
|
|
155
|
+
if (absentSourceIds.size > ABSENT_SOURCE_MAX_ENTRIES) absentSourceIds.clear();
|
|
156
|
+
for (const id of pending) {
|
|
157
|
+
if (!sourceMemories.has(id)) absentSourceIds.set(`${options.memoryDir}\0${id}`, options.nowMs + ABSENT_SOURCE_TTL_MS);
|
|
158
|
+
}
|
|
159
|
+
return sourceMemories;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function readCorpusSourceMemories(options: {
|
|
23
163
|
memoryDir: string;
|
|
24
164
|
abortSignal?: AbortSignal;
|
|
25
165
|
}): Promise<SourceMemoryMap> {
|
|
26
166
|
const sourceMemories: SourceMemoryMap = new Map();
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
])
|
|
34
|
-
for (const memory of [...hotMemories, ...coldMemories]) {
|
|
167
|
+
try {
|
|
168
|
+
const storage = new StorageManager(options.memoryDir);
|
|
169
|
+
const [hotMemories, coldMemories] = await Promise.all([
|
|
170
|
+
storage.readAllMemories({ abortSignal: options.abortSignal }),
|
|
171
|
+
storage.readAllColdMemories(),
|
|
172
|
+
]);
|
|
173
|
+
for (const memory of [...hotMemories, ...coldMemories]) {
|
|
35
174
|
throwIfAborted(options.abortSignal, "harmonic retrieval aborted");
|
|
36
175
|
sourceMemories.set(memory.frontmatter.id, memory);
|
|
37
176
|
}
|
|
38
|
-
} catch {
|
|
177
|
+
} catch (err) {
|
|
39
178
|
throwIfAborted(options.abortSignal, "harmonic retrieval aborted");
|
|
179
|
+
if (err instanceof SecureStoreLockedError) return new Map();
|
|
40
180
|
// Source validation fails closed when the authorized memory directory is
|
|
41
181
|
// unreadable. Source-less nodes remain eligible for retrieval.
|
|
42
182
|
sourceMemories.clear();
|
|
@@ -44,6 +184,27 @@ async function readSourceMemories(options: {
|
|
|
44
184
|
return sourceMemories;
|
|
45
185
|
}
|
|
46
186
|
|
|
187
|
+
async function readSourceMemories(options: {
|
|
188
|
+
memoryDir: string;
|
|
189
|
+
abortSignal?: AbortSignal;
|
|
190
|
+
sourceMemoryIds: string[];
|
|
191
|
+
nowMs?: number;
|
|
192
|
+
}): Promise<SourceMemoryMap> {
|
|
193
|
+
try {
|
|
194
|
+
const indexed = await readIndexedSourceMemories({
|
|
195
|
+
memoryDir: options.memoryDir,
|
|
196
|
+
abortSignal: options.abortSignal,
|
|
197
|
+
sourceMemoryIds: options.sourceMemoryIds,
|
|
198
|
+
nowMs: options.nowMs ?? Date.now(),
|
|
199
|
+
});
|
|
200
|
+
if (indexed) return indexed;
|
|
201
|
+
} catch (err) {
|
|
202
|
+
throwIfAborted(options.abortSignal, "harmonic retrieval aborted");
|
|
203
|
+
if (err instanceof SecureStoreLockedError) return new Map();
|
|
204
|
+
}
|
|
205
|
+
return readCorpusSourceMemories(options);
|
|
206
|
+
}
|
|
207
|
+
|
|
47
208
|
function parseSourceMemoryInsertedAt(node: AbstractionNode): Map<string, string> {
|
|
48
209
|
const insertedAtById = new Map<string, string>();
|
|
49
210
|
const insertedAtRaw = node.metadata?.[HARMONIC_SOURCE_MEMORY_INSERTED_AT_KEY];
|
|
@@ -304,10 +465,15 @@ export async function readProjectedHarmonicGraph(options: {
|
|
|
304
465
|
}): Promise<ProjectedHarmonicGraph> {
|
|
305
466
|
throwIfAborted(options.abortSignal, "harmonic retrieval aborted");
|
|
306
467
|
const nodes = await readAbstractionNodes(options);
|
|
307
|
-
const
|
|
308
|
-
const sourceMemories: SourceMemoryMap =
|
|
309
|
-
? await readSourceMemories(
|
|
310
|
-
|
|
468
|
+
const sourceMemoryIds = [...new Set(nodes.flatMap((node) => node.sourceMemoryIds ?? []))];
|
|
469
|
+
const sourceMemories: SourceMemoryMap = sourceMemoryIds.length > 0
|
|
470
|
+
? await readSourceMemories({
|
|
471
|
+
memoryDir: options.memoryDir,
|
|
472
|
+
abortSignal: options.abortSignal,
|
|
473
|
+
sourceMemoryIds,
|
|
474
|
+
nowMs: options.nowMs,
|
|
475
|
+
})
|
|
476
|
+
: new Map();
|
|
311
477
|
const nowMs = options.nowMs ?? Date.now();
|
|
312
478
|
// A node whose full stored source set survived projection keeps legacy
|
|
313
479
|
// anchor attribution (an anchor written before per-node source ids existed).
|
|
@@ -755,6 +755,7 @@ function writeProjectionDb(
|
|
|
755
755
|
governance: Awaited<ReturnType<typeof loadLatestGovernanceProjection>>,
|
|
756
756
|
usedLifecycleLedger: boolean,
|
|
757
757
|
sourceLifecycleLedgerEventCount: number,
|
|
758
|
+
complete: boolean,
|
|
758
759
|
): void {
|
|
759
760
|
const db = openBetterSqlite3(dbPath);
|
|
760
761
|
const memoryDir = path.dirname(path.dirname(dbPath));
|
|
@@ -765,6 +766,7 @@ function writeProjectionDb(
|
|
|
765
766
|
insertMeta.run("schemaVersion", String(MEMORY_PROJECTION_SCHEMA_VERSION));
|
|
766
767
|
insertMeta.run("rebuiltAt", nowIso);
|
|
767
768
|
insertMeta.run("usedLifecycleLedger", usedLifecycleLedger ? "true" : "false");
|
|
769
|
+
insertMeta.run("complete", complete ? "true" : "false");
|
|
768
770
|
writeProjectionMetaHighWater(insertMeta, sourceLifecycleLedgerEventCount, timelineRows);
|
|
769
771
|
insertMeta.run("latestGovernanceRunId", governance?.runId ?? "");
|
|
770
772
|
|
|
@@ -1126,6 +1128,7 @@ export async function rebuildMemoryProjection(
|
|
|
1126
1128
|
nextGovernance,
|
|
1127
1129
|
snapshot.usedLifecycleLedger,
|
|
1128
1130
|
snapshot.sourceLifecycleLedgerEventCount,
|
|
1131
|
+
!hasScopedProjectionFilter(snapshot.scope),
|
|
1129
1132
|
);
|
|
1130
1133
|
backupPath = await backupExistingProjection(options.memoryDir, outputPath, now);
|
|
1131
1134
|
backupPath = await commitPreparedFileAtomically(tempPath, outputPath, backupPath);
|