@remnic/core 10.0.95 → 10.0.97
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/abstraction-nodes.d.ts.map +1 -1
- package/dist/abstraction-nodes.js +7 -1
- package/dist/access-admin-ops-surface.js +91 -90
- package/dist/access-audit.js +2 -2
- package/dist/access-cli.js +91 -90
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.js +91 -90
- package/dist/access-http-briefing.js +91 -90
- package/dist/access-http-query.js +91 -90
- package/dist/access-http-types.d.ts +2 -0
- package/dist/access-http-types.d.ts.map +1 -1
- package/dist/access-http.d.ts +1 -0
- package/dist/access-http.d.ts.map +1 -1
- package/dist/access-http.js +91 -90
- package/dist/access-identity-continuity-surface.js +91 -90
- package/dist/access-lcm-surface.js +91 -90
- package/dist/access-mcp.js +91 -90
- package/dist/access-observe-write-surface.js +91 -90
- package/dist/access-operations-batch.js +91 -90
- package/dist/access-operations.js +91 -90
- package/dist/access-recall-concurrency.js +91 -90
- package/dist/access-recall-response.js +91 -90
- package/dist/access-recall-surface.js +91 -90
- package/dist/access-schema.d.ts +5 -5
- package/dist/access-service.js +91 -90
- package/dist/active-recall.js +16 -16
- package/dist/briefing.js +15 -15
- package/dist/buffer.js +7 -7
- package/dist/calibration.js +2 -2
- package/dist/{capsule-merge-6IDAYTKG.js → capsule-merge-ILR35DIB.js} +10 -10
- package/dist/causal-consolidation.js +16 -16
- package/dist/{chunk-UN4RVUG2.js → chunk-2R3ACFYN.js} +3 -3
- package/dist/{chunk-RQFXZYTF.js → chunk-33C4BK7I.js} +4 -4
- package/dist/{chunk-BN2EBCLB.js → chunk-3DQZ2GAY.js} +2 -2
- package/dist/{chunk-DDATG2SF.js → chunk-3ETADXEP.js} +136 -127
- package/dist/{chunk-DDATG2SF.js.map → chunk-3ETADXEP.js.map} +1 -1
- package/dist/{chunk-UBEKET2V.js → chunk-3R63SXC5.js} +4 -4
- package/dist/{chunk-GBJUTX44.js → chunk-3WDFQKB4.js} +2 -2
- package/dist/{chunk-LLPMMMWG.js → chunk-4IJFJQPD.js} +2 -2
- package/dist/{chunk-BPK32FRM.js → chunk-4PXDXCNE.js} +61 -6
- package/dist/chunk-4PXDXCNE.js.map +1 -0
- package/dist/{chunk-CIWMTNHW.js → chunk-5NFF2TSD.js} +2 -2
- package/dist/{chunk-WGGRZYSQ.js → chunk-6RW5TONS.js} +8 -8
- package/dist/{chunk-LNGELD2M.js → chunk-6VAIFSDG.js} +2 -2
- package/dist/{chunk-2WCLTWV2.js → chunk-7NVK6AGC.js} +2 -2
- package/dist/{chunk-X5HGU2EM.js → chunk-A2D5WZRO.js} +2 -2
- package/dist/{chunk-LS7PT3YD.js → chunk-AMRGZOIP.js} +5 -5
- package/dist/{chunk-PC5DRBGD.js → chunk-AP4BFWSW.js} +2 -2
- package/dist/{chunk-OUVYYKYU.js → chunk-B2BMTXIF.js} +2 -2
- package/dist/{chunk-A4F7OIC5.js → chunk-BUOP3DOK.js} +7 -3
- package/dist/{chunk-A4F7OIC5.js.map → chunk-BUOP3DOK.js.map} +1 -1
- package/dist/{chunk-DNY63QWJ.js → chunk-CCQEANCY.js} +2 -2
- package/dist/{chunk-YIXJSW5M.js → chunk-CH2N4XCJ.js} +3 -3
- package/dist/{chunk-LQ5SS4OP.js → chunk-D6IGZYTN.js} +3 -3
- package/dist/{chunk-FWRXW7CZ.js → chunk-D6WJNWZG.js} +29 -29
- package/dist/{chunk-4EZWSDFL.js → chunk-E3YHIAO3.js} +3 -3
- package/dist/{chunk-IL3RHBWM.js → chunk-ECC6AAEE.js} +3 -3
- package/dist/{chunk-XJ2S5UBF.js → chunk-EHJ2RYR7.js} +3 -3
- package/dist/{chunk-F4QFU5IZ.js → chunk-EZ7XJJ5L.js} +2 -2
- package/dist/{chunk-FQAQ63QA.js → chunk-FHQ5SPMD.js} +2 -2
- package/dist/{chunk-LXATZLSC.js → chunk-FL3O34XF.js} +12 -5
- package/dist/chunk-FL3O34XF.js.map +1 -0
- package/dist/{chunk-DPPLNZ3A.js → chunk-FM4P7HSW.js} +4 -4
- package/dist/{chunk-QDESY76F.js → chunk-FQACH7MC.js} +2 -2
- package/dist/{chunk-XPXHQI6N.js → chunk-FVMEZ57K.js} +2 -2
- package/dist/{chunk-ILY5SSOM.js → chunk-FVPWQWIK.js} +6 -6
- package/dist/{chunk-F2XNSIPP.js → chunk-G2CGIFRP.js} +10 -3
- package/dist/chunk-G2CGIFRP.js.map +1 -0
- package/dist/{chunk-S3S72HRM.js → chunk-HCCRWSP7.js} +2 -2
- package/dist/{chunk-NMGHGTVR.js → chunk-HRMOKGQM.js} +6 -6
- package/dist/chunk-ITAUMNQU.js +40 -0
- package/dist/chunk-ITAUMNQU.js.map +1 -0
- package/dist/{chunk-R23JWJD3.js → chunk-J426U7XK.js} +9 -9
- package/dist/{chunk-CL4JU3DF.js → chunk-KI7E3N6Q.js} +3 -3
- package/dist/{chunk-SQ3GEIEL.js → chunk-KUQLUYIR.js} +5 -5
- package/dist/{chunk-J7BIFOYP.js → chunk-MXXTU4R2.js} +2 -2
- package/dist/{chunk-IAYUTGF2.js → chunk-O3GZPLVY.js} +4 -4
- package/dist/{chunk-IGOW4TJE.js → chunk-O3JG4PD3.js} +2 -2
- package/dist/{chunk-UHMHJX5G.js → chunk-OIG4ZFMB.js} +4 -4
- package/dist/{chunk-D5XG2KPS.js → chunk-OTSRGZRU.js} +2 -2
- package/dist/{chunk-ZDV5YWID.js → chunk-PWATRW3H.js} +6 -6
- package/dist/{chunk-ESLOLCD4.js → chunk-RUWKIPWJ.js} +2 -2
- package/dist/{chunk-IQ55QGY5.js → chunk-RXL67CV6.js} +2 -2
- package/dist/{chunk-W54Z2MOP.js → chunk-RXSGBO7Y.js} +2 -2
- package/dist/{chunk-QMFAMOT2.js → chunk-SFZEJJ6V.js} +2 -2
- package/dist/{chunk-QHARXWTC.js → chunk-SW3WUUYA.js} +2 -2
- package/dist/{chunk-FZZMLGWQ.js → chunk-TFI2Y5AR.js} +2 -2
- package/dist/{chunk-BOK6BN5M.js → chunk-THV6PZ7V.js} +2 -2
- package/dist/{chunk-Z3FTSHT5.js → chunk-UG54AEUG.js} +2 -2
- package/dist/{chunk-X2BVB74H.js → chunk-UZQXJDOE.js} +2 -2
- package/dist/{chunk-SUTEBYGI.js → chunk-VRBTZDQ5.js} +2 -2
- package/dist/{chunk-RKABLU5M.js → chunk-XP5CKXFV.js} +9 -9
- package/dist/{chunk-RNG7S3FY.js → chunk-YCQMNW3F.js} +6 -6
- package/dist/{chunk-OJXK5C6Z.js → chunk-ZG4GNPRE.js} +7 -7
- package/dist/{chunk-TYRWSE4B.js → chunk-ZUMHQEW4.js} +2 -2
- package/dist/{chunk-DAOMWGO2.js → chunk-ZX5N5D6M.js} +4 -4
- package/dist/cli.js +91 -90
- package/dist/coding/export-okf-codegraph.js +14 -14
- package/dist/compounding/engine.js +16 -16
- package/dist/config.js +16 -16
- package/dist/connectors/codex-materialize-runner.js +15 -15
- package/dist/connectors/index.js +15 -15
- package/dist/consolidation-provenance-check.js +8 -8
- package/dist/consolidation-undo.js +6 -6
- package/dist/contradiction/index.js +6 -6
- package/dist/convergence-refresh.js +17 -17
- package/dist/corpus-watermark.js +17 -17
- package/dist/cue-anchors.d.ts.map +1 -1
- package/dist/cue-anchors.js +8 -2
- package/dist/{entity-canonical-id-migration-JJSTZF73.js → entity-canonical-id-migration-IG77BOGD.js} +5 -5
- package/dist/entity-mention-index-cache.js +2 -2
- package/dist/entity-retrieval.js +16 -16
- package/dist/explicit-capture.js +91 -90
- package/dist/extraction.js +21 -21
- package/dist/harmonic-construction.js +9 -3
- package/dist/harmonic-graph-version.d.ts +14 -0
- package/dist/harmonic-graph-version.d.ts.map +1 -0
- package/dist/harmonic-graph-version.js +17 -0
- package/dist/harmonic-retrieval.d.ts +19 -14
- package/dist/harmonic-retrieval.d.ts.map +1 -1
- package/dist/harmonic-retrieval.js +22 -17
- package/dist/index.js +105 -104
- package/dist/lcm/engine.js +3 -3
- package/dist/lcm/index.js +8 -8
- package/dist/local-llm.js +2 -2
- package/dist/location/index.js +16 -16
- package/dist/maintenance/memory-governance.js +15 -15
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +15 -15
- package/dist/maintenance/rebuild-memory-projection-runner.js +17 -17
- package/dist/maintenance/rebuild-memory-projection.js +16 -16
- package/dist/memory-browse.js +7 -7
- package/dist/namespaces/migrate.js +16 -16
- package/dist/namespaces/search.d.ts.map +1 -1
- package/dist/namespaces/search.js +6 -6
- package/dist/namespaces/storage.js +15 -15
- package/dist/operator-doctor-corpus.js +18 -18
- package/dist/operator-toolkit.js +24 -24
- package/dist/orchestration/backlog-scheduler.js +92 -91
- package/dist/orchestration/backlog-scheduler.js.map +1 -1
- package/dist/orchestration/buffer-drain-runner.js +92 -91
- package/dist/orchestration/maintenance.js +25 -25
- package/dist/orchestrator.js +93 -92
- package/dist/page-versioning.js +6 -6
- package/dist/qmd.js +4 -4
- package/dist/recall-navigation.js +91 -90
- package/dist/recall-state-view-admission.js +7 -7
- package/dist/recall-why-service.js +7 -7
- package/dist/{resolution-OVVIF7ZY.js → resolution-G4SLHSEU.js} +7 -7
- package/dist/resolution-G4SLHSEU.js.map +1 -0
- package/dist/resume-bundles.js +17 -17
- package/dist/schemas.d.ts +9 -9
- package/dist/search/factory.js +5 -5
- package/dist/search/index.js +5 -5
- package/dist/semantic-consolidation.js +16 -16
- package/dist/semantic-rule-promotion.js +15 -15
- package/dist/semantic-rule-verifier.js +15 -15
- package/dist/source-agent-qualifier.js +18 -18
- package/dist/standup/build.js +15 -15
- package/dist/storage.js +14 -14
- package/dist/summarizer.js +3 -3
- package/dist/support-passport/index.js +3 -3
- package/dist/transfer/capsule-import.js +8 -8
- package/dist/transfer/export-okf.js +15 -15
- package/dist/verified-recall.js +15 -15
- package/dist/who-knows-cli.js +16 -16
- package/dist/who-knows.js +15 -15
- package/package.json +2 -2
- package/src/abstraction-nodes.ts +7 -2
- package/src/access-http-cancellation.test.ts +45 -0
- package/src/access-http-types.ts +2 -0
- package/src/access-http.ts +13 -1
- package/src/cue-anchors.ts +8 -3
- package/src/harmonic-graph-cache.test.ts +244 -0
- package/src/harmonic-graph-version.ts +46 -0
- package/src/harmonic-retrieval.ts +92 -2
- package/src/namespaces/search.test.ts +35 -0
- package/src/namespaces/search.ts +7 -1
- package/src/orchestration/recall-internal.ts +1 -1
- package/dist/chunk-BPK32FRM.js.map +0 -1
- package/dist/chunk-F2XNSIPP.js.map +0 -1
- package/dist/chunk-LXATZLSC.js.map +0 -1
- /package/dist/{capsule-merge-6IDAYTKG.js.map → capsule-merge-ILR35DIB.js.map} +0 -0
- /package/dist/{chunk-UN4RVUG2.js.map → chunk-2R3ACFYN.js.map} +0 -0
- /package/dist/{chunk-RQFXZYTF.js.map → chunk-33C4BK7I.js.map} +0 -0
- /package/dist/{chunk-BN2EBCLB.js.map → chunk-3DQZ2GAY.js.map} +0 -0
- /package/dist/{chunk-UBEKET2V.js.map → chunk-3R63SXC5.js.map} +0 -0
- /package/dist/{chunk-GBJUTX44.js.map → chunk-3WDFQKB4.js.map} +0 -0
- /package/dist/{chunk-LLPMMMWG.js.map → chunk-4IJFJQPD.js.map} +0 -0
- /package/dist/{chunk-CIWMTNHW.js.map → chunk-5NFF2TSD.js.map} +0 -0
- /package/dist/{chunk-WGGRZYSQ.js.map → chunk-6RW5TONS.js.map} +0 -0
- /package/dist/{chunk-LNGELD2M.js.map → chunk-6VAIFSDG.js.map} +0 -0
- /package/dist/{chunk-2WCLTWV2.js.map → chunk-7NVK6AGC.js.map} +0 -0
- /package/dist/{chunk-X5HGU2EM.js.map → chunk-A2D5WZRO.js.map} +0 -0
- /package/dist/{chunk-LS7PT3YD.js.map → chunk-AMRGZOIP.js.map} +0 -0
- /package/dist/{chunk-PC5DRBGD.js.map → chunk-AP4BFWSW.js.map} +0 -0
- /package/dist/{chunk-OUVYYKYU.js.map → chunk-B2BMTXIF.js.map} +0 -0
- /package/dist/{chunk-DNY63QWJ.js.map → chunk-CCQEANCY.js.map} +0 -0
- /package/dist/{chunk-YIXJSW5M.js.map → chunk-CH2N4XCJ.js.map} +0 -0
- /package/dist/{chunk-LQ5SS4OP.js.map → chunk-D6IGZYTN.js.map} +0 -0
- /package/dist/{chunk-FWRXW7CZ.js.map → chunk-D6WJNWZG.js.map} +0 -0
- /package/dist/{chunk-4EZWSDFL.js.map → chunk-E3YHIAO3.js.map} +0 -0
- /package/dist/{chunk-IL3RHBWM.js.map → chunk-ECC6AAEE.js.map} +0 -0
- /package/dist/{chunk-XJ2S5UBF.js.map → chunk-EHJ2RYR7.js.map} +0 -0
- /package/dist/{chunk-F4QFU5IZ.js.map → chunk-EZ7XJJ5L.js.map} +0 -0
- /package/dist/{chunk-FQAQ63QA.js.map → chunk-FHQ5SPMD.js.map} +0 -0
- /package/dist/{chunk-DPPLNZ3A.js.map → chunk-FM4P7HSW.js.map} +0 -0
- /package/dist/{chunk-QDESY76F.js.map → chunk-FQACH7MC.js.map} +0 -0
- /package/dist/{chunk-XPXHQI6N.js.map → chunk-FVMEZ57K.js.map} +0 -0
- /package/dist/{chunk-ILY5SSOM.js.map → chunk-FVPWQWIK.js.map} +0 -0
- /package/dist/{chunk-S3S72HRM.js.map → chunk-HCCRWSP7.js.map} +0 -0
- /package/dist/{chunk-NMGHGTVR.js.map → chunk-HRMOKGQM.js.map} +0 -0
- /package/dist/{chunk-R23JWJD3.js.map → chunk-J426U7XK.js.map} +0 -0
- /package/dist/{chunk-CL4JU3DF.js.map → chunk-KI7E3N6Q.js.map} +0 -0
- /package/dist/{chunk-SQ3GEIEL.js.map → chunk-KUQLUYIR.js.map} +0 -0
- /package/dist/{chunk-J7BIFOYP.js.map → chunk-MXXTU4R2.js.map} +0 -0
- /package/dist/{chunk-IAYUTGF2.js.map → chunk-O3GZPLVY.js.map} +0 -0
- /package/dist/{chunk-IGOW4TJE.js.map → chunk-O3JG4PD3.js.map} +0 -0
- /package/dist/{chunk-UHMHJX5G.js.map → chunk-OIG4ZFMB.js.map} +0 -0
- /package/dist/{chunk-D5XG2KPS.js.map → chunk-OTSRGZRU.js.map} +0 -0
- /package/dist/{chunk-ZDV5YWID.js.map → chunk-PWATRW3H.js.map} +0 -0
- /package/dist/{chunk-ESLOLCD4.js.map → chunk-RUWKIPWJ.js.map} +0 -0
- /package/dist/{chunk-IQ55QGY5.js.map → chunk-RXL67CV6.js.map} +0 -0
- /package/dist/{chunk-W54Z2MOP.js.map → chunk-RXSGBO7Y.js.map} +0 -0
- /package/dist/{chunk-QMFAMOT2.js.map → chunk-SFZEJJ6V.js.map} +0 -0
- /package/dist/{chunk-QHARXWTC.js.map → chunk-SW3WUUYA.js.map} +0 -0
- /package/dist/{chunk-FZZMLGWQ.js.map → chunk-TFI2Y5AR.js.map} +0 -0
- /package/dist/{chunk-BOK6BN5M.js.map → chunk-THV6PZ7V.js.map} +0 -0
- /package/dist/{chunk-Z3FTSHT5.js.map → chunk-UG54AEUG.js.map} +0 -0
- /package/dist/{chunk-X2BVB74H.js.map → chunk-UZQXJDOE.js.map} +0 -0
- /package/dist/{chunk-SUTEBYGI.js.map → chunk-VRBTZDQ5.js.map} +0 -0
- /package/dist/{chunk-RKABLU5M.js.map → chunk-XP5CKXFV.js.map} +0 -0
- /package/dist/{chunk-RNG7S3FY.js.map → chunk-YCQMNW3F.js.map} +0 -0
- /package/dist/{chunk-OJXK5C6Z.js.map → chunk-ZG4GNPRE.js.map} +0 -0
- /package/dist/{chunk-TYRWSE4B.js.map → chunk-ZUMHQEW4.js.map} +0 -0
- /package/dist/{chunk-DAOMWGO2.js.map → chunk-ZX5N5D6M.js.map} +0 -0
- /package/dist/{entity-canonical-id-migration-JJSTZF73.js.map → entity-canonical-id-migration-IG77BOGD.js.map} +0 -0
- /package/dist/{resolution-OVVIF7ZY.js.map → harmonic-graph-version.js.map} +0 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, readFile, rm, 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 { recordAbstractionNode, upsertAbstractionNodes, type AbstractionNode } from "./abstraction-nodes.js";
|
|
7
|
+
import { pruneOrphanCueAnchors, recordCueAnchor, upsertCueAnchors, type CueAnchor } from "./cue-anchors.js";
|
|
8
|
+
import {
|
|
9
|
+
clearHarmonicGraphCache,
|
|
10
|
+
harmonicGraphCacheStats,
|
|
11
|
+
readProjectedHarmonicGraph,
|
|
12
|
+
searchHarmonicRetrieval,
|
|
13
|
+
} from "./harmonic-retrieval.js";
|
|
14
|
+
import { bumpMemoryCorpusVersionForDir } from "./memory-corpus-version.js";
|
|
15
|
+
import { StorageManager } from "./storage.js";
|
|
16
|
+
|
|
17
|
+
function node(nodeId: string, title: string, sourceMemoryIds: string[]): AbstractionNode {
|
|
18
|
+
return {
|
|
19
|
+
schemaVersion: 1,
|
|
20
|
+
nodeId,
|
|
21
|
+
recordedAt: new Date().toISOString(),
|
|
22
|
+
sessionKey: "agent:test",
|
|
23
|
+
kind: "topic",
|
|
24
|
+
abstractionLevel: "meso",
|
|
25
|
+
title,
|
|
26
|
+
summary: title,
|
|
27
|
+
sourceMemoryIds,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function withDir(run: (memoryDir: string, storage: StorageManager) => Promise<void>) {
|
|
32
|
+
const memoryDir = await mkdtemp(path.join(tmpdir(), "remnic-harmonic-cache-"));
|
|
33
|
+
clearHarmonicGraphCache();
|
|
34
|
+
try {
|
|
35
|
+
const storage = new StorageManager(memoryDir);
|
|
36
|
+
await storage.ensureDirectories();
|
|
37
|
+
await run(memoryDir, storage);
|
|
38
|
+
} finally {
|
|
39
|
+
clearHarmonicGraphCache();
|
|
40
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const read = (memoryDir: string, extra: { nowMs?: number } = {}) =>
|
|
45
|
+
readProjectedHarmonicGraph({ memoryDir, anchorsEnabled: true, useCache: true, ...extra });
|
|
46
|
+
|
|
47
|
+
test("a second cached read does no file reads", async () => {
|
|
48
|
+
await withDir(async (memoryDir, storage) => {
|
|
49
|
+
const first = await storage.writeMemory("fact", "cache source one", { source: "test" });
|
|
50
|
+
const nodeFile = await recordAbstractionNode({ memoryDir, node: node("n1", "cache source one", [first.id]) });
|
|
51
|
+
const before = { ...harmonicGraphCacheStats };
|
|
52
|
+
const a = await read(memoryDir);
|
|
53
|
+
assert.equal(a.nodes.length, 1);
|
|
54
|
+
assert.equal(harmonicGraphCacheStats.misses, before.misses + 1);
|
|
55
|
+
// Remove every file the projection reads. A real read would now return nothing.
|
|
56
|
+
await rm(nodeFile, { force: true });
|
|
57
|
+
await rm((await storage.getMemoryById(first.id))!.path, { force: true });
|
|
58
|
+
const b = await read(memoryDir);
|
|
59
|
+
assert.equal(harmonicGraphCacheStats.hits, before.hits + 1);
|
|
60
|
+
assert.equal(b, a);
|
|
61
|
+
assert.equal(b.nodes.length, 1);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("a recall after a new memory is saved sees it", async () => {
|
|
66
|
+
await withDir(async (memoryDir, storage) => {
|
|
67
|
+
const first = await storage.writeMemory("fact", "alpha harmonic", { source: "test" });
|
|
68
|
+
await recordAbstractionNode({ memoryDir, node: node("n1", "alpha harmonic", [first.id]) });
|
|
69
|
+
const hits = () => harmonicGraphCacheStats.hits;
|
|
70
|
+
const query = { memoryDir, query: "bravo cache", maxResults: 3, anchorsEnabled: true, useGraphCache: true };
|
|
71
|
+
assert.equal((await searchHarmonicRetrieval(query)).length, 0);
|
|
72
|
+
const h = hits();
|
|
73
|
+
assert.equal((await searchHarmonicRetrieval(query)).length, 0);
|
|
74
|
+
assert.equal(hits(), h + 1);
|
|
75
|
+
|
|
76
|
+
const second = await storage.writeMemory("fact", "bravo cache", { source: "test" });
|
|
77
|
+
await recordAbstractionNode({ memoryDir, node: node("n2", "bravo cache", [second.id]) });
|
|
78
|
+
const results = await searchHarmonicRetrieval(query);
|
|
79
|
+
assert.deepEqual(results.map((result) => result.node.nodeId), ["n2"]);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("an out-of-band corpus bump invalidates, so a source that leaves the active set drops out", async () => {
|
|
84
|
+
await withDir(async (memoryDir, storage) => {
|
|
85
|
+
const memory = await storage.writeMemory("fact", "gamma status fact", { source: "test" });
|
|
86
|
+
await recordAbstractionNode({ memoryDir, node: node("n1", "gamma status fact", [memory.id]) });
|
|
87
|
+
assert.equal((await read(memoryDir)).nodes.length, 1);
|
|
88
|
+
const memoryPath = (await storage.getMemoryById(memory.id))!.path;
|
|
89
|
+
await writeFile(
|
|
90
|
+
memoryPath,
|
|
91
|
+
(await readFile(memoryPath, "utf8")).replace("category: fact", "category: fact\nstatus: quarantined"),
|
|
92
|
+
);
|
|
93
|
+
// The documented out-of-band bump: the same call review and governance writers make.
|
|
94
|
+
bumpMemoryCorpusVersionForDir(memoryDir);
|
|
95
|
+
assert.equal((await read(memoryDir)).nodes.length, 0);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("an explicit nowMs bypasses the cache", async () => {
|
|
100
|
+
await withDir(async (memoryDir, storage) => {
|
|
101
|
+
const memory = await storage.writeMemory("fact", "delta bypass fact", { source: "test" });
|
|
102
|
+
const nodeFile = await recordAbstractionNode({ memoryDir, node: node("n1", "delta bypass fact", [memory.id]) });
|
|
103
|
+
assert.equal((await read(memoryDir)).nodes.length, 1);
|
|
104
|
+
await rm(nodeFile, { force: true });
|
|
105
|
+
assert.equal((await read(memoryDir, { nowMs: Date.now() })).nodes.length, 0);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("cached arrays are frozen", async () => {
|
|
110
|
+
await withDir(async (memoryDir, storage) => {
|
|
111
|
+
const memory = await storage.writeMemory("fact", "epsilon frozen fact", { source: "test" });
|
|
112
|
+
await recordAbstractionNode({ memoryDir, node: node("n1", "epsilon frozen fact", [memory.id]) });
|
|
113
|
+
const graph = await read(memoryDir);
|
|
114
|
+
assert.throws(() => graph.nodes.push(node("x", "x", [])), TypeError);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("cached node records are deep frozen, so a caller cannot change later reads", async () => {
|
|
119
|
+
await withDir(async (memoryDir, storage) => {
|
|
120
|
+
const memory = await storage.writeMemory("fact", "zeta deep frozen fact", { source: "test" });
|
|
121
|
+
await recordAbstractionNode({ memoryDir, node: node("n1", "zeta deep frozen fact", [memory.id]) });
|
|
122
|
+
const graph = await read(memoryDir);
|
|
123
|
+
const cached = graph.nodes[0]!;
|
|
124
|
+
assert.throws(() => { cached.title = "changed"; }, TypeError);
|
|
125
|
+
assert.throws(() => cached.sourceMemoryIds?.push("x"), TypeError);
|
|
126
|
+
assert.equal((await read(memoryDir)).nodes[0]!.title, "zeta deep frozen fact");
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("an aborted signal rejects even when the graph is cached", async () => {
|
|
131
|
+
await withDir(async (memoryDir, storage) => {
|
|
132
|
+
const memory = await storage.writeMemory("fact", "eta abort fact", { source: "test" });
|
|
133
|
+
await recordAbstractionNode({ memoryDir, node: node("n1", "eta abort fact", [memory.id]) });
|
|
134
|
+
await read(memoryDir);
|
|
135
|
+
const controller = new AbortController();
|
|
136
|
+
controller.abort();
|
|
137
|
+
await assert.rejects(
|
|
138
|
+
readProjectedHarmonicGraph({ memoryDir, anchorsEnabled: true, useCache: true, abortSignal: controller.signal }),
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
function anchor(anchorId: string, nodeRef: string, memoryId: string): CueAnchor {
|
|
144
|
+
return {
|
|
145
|
+
schemaVersion: 1,
|
|
146
|
+
anchorId,
|
|
147
|
+
anchorType: "entity",
|
|
148
|
+
anchorValue: anchorId,
|
|
149
|
+
normalizedCue: anchorId,
|
|
150
|
+
recordedAt: new Date().toISOString(),
|
|
151
|
+
sessionKey: "agent:test",
|
|
152
|
+
nodeRefs: [nodeRef],
|
|
153
|
+
sourceMemoryIdsByNodeRef: { [nodeRef]: [memoryId] },
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Each case seeds node n1 (source m1) and, for the anchor cases, anchor a1, fills the cache, then runs ONE
|
|
158
|
+
// store write with no memory write after it. The next read must miss and show the new count; with the
|
|
159
|
+
// graph version bump removed these fail because the corpus sentinels never move.
|
|
160
|
+
async function seedForWrite(memoryDir: string, storage: StorageManager) {
|
|
161
|
+
const m1 = await storage.writeMemory("fact", "seed one", { source: "test" });
|
|
162
|
+
const m2 = await storage.writeMemory("fact", "seed two", { source: "test" });
|
|
163
|
+
await recordAbstractionNode({ memoryDir, node: node("n1", "seed one", [m1.id]) });
|
|
164
|
+
await recordAbstractionNode({ memoryDir, node: node("n2", "seed two", [m2.id]) });
|
|
165
|
+
await recordCueAnchor({ memoryDir, anchor: anchor("a1", "n1", m1.id) });
|
|
166
|
+
return { m1, m2 };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function counts(memoryDir: string) {
|
|
170
|
+
const graph = await read(memoryDir);
|
|
171
|
+
return { nodes: graph.nodes.length, anchors: graph.anchors.length };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const storeWrites: Array<{
|
|
175
|
+
name: string;
|
|
176
|
+
write: (memoryDir: string, ids: { m1: string; m2: string }) => Promise<void>;
|
|
177
|
+
expected: { nodes: number; anchors: number };
|
|
178
|
+
}> = [
|
|
179
|
+
{
|
|
180
|
+
name: "recordAbstractionNode",
|
|
181
|
+
write: async (memoryDir, ids) => { await recordAbstractionNode({ memoryDir, node: node("n3", "seed three", [ids.m1]) }); },
|
|
182
|
+
expected: { nodes: 3, anchors: 1 },
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: "upsertAbstractionNodes",
|
|
186
|
+
write: async (memoryDir, ids) => { await upsertAbstractionNodes({ memoryDir, nodes: [node("n3", "seed three", [ids.m1])] }); },
|
|
187
|
+
expected: { nodes: 3, anchors: 1 },
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: "recordCueAnchor",
|
|
191
|
+
write: async (memoryDir, ids) => { await recordCueAnchor({ memoryDir, anchor: anchor("a2", "n2", ids.m2) }); },
|
|
192
|
+
expected: { nodes: 2, anchors: 2 },
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: "upsertCueAnchors",
|
|
196
|
+
write: async (memoryDir, ids) => { await upsertCueAnchors({ memoryDir, anchors: [anchor("a2", "n2", ids.m2)] }); },
|
|
197
|
+
expected: { nodes: 2, anchors: 2 },
|
|
198
|
+
},
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
for (const { name, write, expected } of storeWrites) {
|
|
202
|
+
test(`${name} alone invalidates the cached graph`, async () => {
|
|
203
|
+
await withDir(async (memoryDir, storage) => {
|
|
204
|
+
const { m1, m2 } = await seedForWrite(memoryDir, storage);
|
|
205
|
+
assert.deepEqual(await counts(memoryDir), { nodes: 2, anchors: 1 });
|
|
206
|
+
const hitsBefore = harmonicGraphCacheStats.hits;
|
|
207
|
+
assert.deepEqual(await counts(memoryDir), { nodes: 2, anchors: 1 });
|
|
208
|
+
assert.equal(harmonicGraphCacheStats.hits, hitsBefore + 1);
|
|
209
|
+
|
|
210
|
+
await write(memoryDir, { m1: m1.id, m2: m2.id });
|
|
211
|
+
const missesBefore = harmonicGraphCacheStats.misses;
|
|
212
|
+
assert.deepEqual(await counts(memoryDir), expected);
|
|
213
|
+
assert.equal(harmonicGraphCacheStats.misses, missesBefore + 1);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
test("pruneOrphanCueAnchors alone invalidates the cached graph", async () => {
|
|
219
|
+
await withDir(async (memoryDir, storage) => {
|
|
220
|
+
const { m2 } = await seedForWrite(memoryDir, storage);
|
|
221
|
+
await recordCueAnchor({ memoryDir, anchor: anchor("a2", "n2", m2.id) });
|
|
222
|
+
assert.deepEqual(await counts(memoryDir), { nodes: 2, anchors: 2 });
|
|
223
|
+
// n2's file goes away behind the cache's back, which makes a2 an orphan. Only the prune below bumps.
|
|
224
|
+
await rm(path.join(memoryDir, "state", "abstraction-nodes", "nodes", new Date().toISOString().slice(0, 10), "n2.json"), { force: true });
|
|
225
|
+
assert.deepEqual(await counts(memoryDir), { nodes: 2, anchors: 2 });
|
|
226
|
+
assert.equal(await pruneOrphanCueAnchors({ memoryDir }), 1);
|
|
227
|
+
assert.deepEqual(await counts(memoryDir), { nodes: 1, anchors: 1 });
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("an entry older than the TTL is rebuilt", async (t) => {
|
|
232
|
+
await withDir(async (memoryDir, storage) => {
|
|
233
|
+
const memory = await storage.writeMemory("fact", "zeta ttl fact", { source: "test" });
|
|
234
|
+
const nodeFile = await recordAbstractionNode({ memoryDir, node: node("n1", "zeta ttl fact", [memory.id]) });
|
|
235
|
+
const start = Date.now();
|
|
236
|
+
const clock = t.mock.method(Date, "now", () => start);
|
|
237
|
+
assert.equal((await read(memoryDir)).nodes.length, 1);
|
|
238
|
+
await rm(nodeFile, { force: true });
|
|
239
|
+
clock.mock.mockImplementation(() => start + 29_000);
|
|
240
|
+
assert.equal((await read(memoryDir)).nodes.length, 1, "still cached inside the TTL");
|
|
241
|
+
clock.mock.mockImplementation(() => start + 31_000);
|
|
242
|
+
assert.equal((await read(memoryDir)).nodes.length, 0, "rebuilt past the TTL");
|
|
243
|
+
});
|
|
244
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync, statSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { MEMORY_CORPUS_VERSION_SENTINEL } from "./memory-corpus-version.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Append-only sentinel in the abstraction node store root. The byte size is the
|
|
7
|
+
* version of the node and cue anchor stores, the same scheme the memory corpus
|
|
8
|
+
* sentinel uses, so a bump in one process is seen by every other process.
|
|
9
|
+
*/
|
|
10
|
+
export const HARMONIC_GRAPH_VERSION_SENTINEL = ".harmonic-graph-version.log";
|
|
11
|
+
|
|
12
|
+
// Names must match StorageManager.versionFilePath("memory-status" | "cold-write").
|
|
13
|
+
const MEMORY_STATUS_VERSION_SENTINEL = ".memory-status-version.log";
|
|
14
|
+
const COLD_WRITE_VERSION_SENTINEL = ".cold-write-version.log";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Record that a node or cue anchor was written. Fail-open: a failed bump must not
|
|
18
|
+
* fail the write; the harmonic graph cache TTL bounds the stale window.
|
|
19
|
+
*/
|
|
20
|
+
export function bumpHarmonicGraphVersion(abstractionNodeStoreDir: string): void {
|
|
21
|
+
try {
|
|
22
|
+
mkdirSync(abstractionNodeStoreDir, { recursive: true });
|
|
23
|
+
appendFileSync(path.join(abstractionNodeStoreDir, HARMONIC_GRAPH_VERSION_SENTINEL), "x");
|
|
24
|
+
} catch {
|
|
25
|
+
/* fail-open */
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sentinelSize(filePath: string): number {
|
|
30
|
+
try {
|
|
31
|
+
return statSync(filePath).size;
|
|
32
|
+
} catch {
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Four stats; changes whenever a memory, a node or a cue anchor is written. */
|
|
38
|
+
export function readHarmonicGraphStamp(memoryDir: string, abstractionNodeStoreDir: string): string {
|
|
39
|
+
const stateDir = path.join(memoryDir, "state");
|
|
40
|
+
return [
|
|
41
|
+
sentinelSize(path.join(stateDir, MEMORY_CORPUS_VERSION_SENTINEL)),
|
|
42
|
+
sentinelSize(path.join(stateDir, MEMORY_STATUS_VERSION_SENTINEL)),
|
|
43
|
+
sentinelSize(path.join(stateDir, COLD_WRITE_VERSION_SENTINEL)),
|
|
44
|
+
sentinelSize(path.join(abstractionNodeStoreDir, HARMONIC_GRAPH_VERSION_SENTINEL)),
|
|
45
|
+
].join(":");
|
|
46
|
+
}
|
|
@@ -2,6 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import { realpath } from "node:fs/promises";
|
|
3
3
|
import { listJsonFilesStrict, readJsonFile } from "./json-store.js";
|
|
4
4
|
import { throwIfAborted } from "./abort-error.js";
|
|
5
|
+
import { readHarmonicGraphStamp } from "./harmonic-graph-version.js";
|
|
5
6
|
import { inferMemoryStatus, toMemoryPathRel } from "./memory-lifecycle-ledger-utils.js";
|
|
6
7
|
import {
|
|
7
8
|
HARMONIC_SOURCE_MEMORY_INSERTED_AT_KEY,
|
|
@@ -403,7 +404,7 @@ export interface ProjectedHarmonicGraph {
|
|
|
403
404
|
* one of that node's surviving active sources, and is dropped once no
|
|
404
405
|
* reference survives.
|
|
405
406
|
*/
|
|
406
|
-
|
|
407
|
+
async function buildProjectedHarmonicGraph(options: {
|
|
407
408
|
memoryDir: string;
|
|
408
409
|
abstractionNodeStoreDir?: string;
|
|
409
410
|
anchorsEnabled: boolean;
|
|
@@ -460,6 +461,90 @@ export async function readProjectedHarmonicGraph(options: {
|
|
|
460
461
|
return { nodes: projectedNodes, anchors };
|
|
461
462
|
}
|
|
462
463
|
|
|
464
|
+
const GRAPH_CACHE_TTL_MS = 30_000;
|
|
465
|
+
const GRAPH_CACHE_MAX_ENTRIES = 8;
|
|
466
|
+
|
|
467
|
+
interface GraphCacheEntry {
|
|
468
|
+
stamp: string;
|
|
469
|
+
startedAtMs: number;
|
|
470
|
+
graph: ProjectedHarmonicGraph;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const graphCache = new Map<string, GraphCacheEntry>();
|
|
474
|
+
/** Counters for tests and diagnostics. */
|
|
475
|
+
export const harmonicGraphCacheStats = { hits: 0, misses: 0 };
|
|
476
|
+
|
|
477
|
+
/** Freeze records and their nested arrays/objects so a caller cannot corrupt the shared cache. */
|
|
478
|
+
function deepFreeze<T extends object>(value: T): T {
|
|
479
|
+
// isFrozen is true for primitives and null, and a frozen value is not revisited, so this ends on cycles too.
|
|
480
|
+
if (Object.isFrozen(value)) return value;
|
|
481
|
+
Object.freeze(value);
|
|
482
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
483
|
+
return value;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export function clearHarmonicGraphCache(): void {
|
|
487
|
+
graphCache.clear();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* `readProjectedHarmonicGraph` with an opt-in in-memory cache. Every recall used
|
|
492
|
+
* to re-read all nodes, anchors and source memories from disk (about 4.7 s on a
|
|
493
|
+
* 2107 node, 9199 anchor, 4105 source graph). With `useCache` a completed graph is
|
|
494
|
+
* reused while the stamp below is unchanged: the memory corpus, memory status and
|
|
495
|
+
* cold-write version sentinels (bumped by every memory write, in any process) and
|
|
496
|
+
* the graph version sentinel (bumped by node and anchor writes). A stamp read is
|
|
497
|
+
* four stats, not a scan of the memories. The TTL bounds the one input a sentinel
|
|
498
|
+
* cannot see, which is wall-clock validity expiry. An explicit `nowMs` bypasses the
|
|
499
|
+
* cache so tests stay deterministic. The cached arrays are frozen: callers must not
|
|
500
|
+
* mutate them.
|
|
501
|
+
*/
|
|
502
|
+
export async function readProjectedHarmonicGraph(options: {
|
|
503
|
+
memoryDir: string;
|
|
504
|
+
abstractionNodeStoreDir?: string;
|
|
505
|
+
anchorsEnabled: boolean;
|
|
506
|
+
abortSignal?: AbortSignal;
|
|
507
|
+
temporalExpiredInInjection?: boolean;
|
|
508
|
+
nowMs?: number;
|
|
509
|
+
useCache?: boolean;
|
|
510
|
+
}): Promise<ProjectedHarmonicGraph> {
|
|
511
|
+
if (options.useCache !== true || options.nowMs !== undefined) return buildProjectedHarmonicGraph(options);
|
|
512
|
+
// Same cancellation contract as the builder: a cache hit must not mask an aborted signal.
|
|
513
|
+
throwIfAborted(options.abortSignal, "harmonic retrieval aborted");
|
|
514
|
+
const nodeStoreDir = resolveAbstractionNodeStoreDir(options.memoryDir, options.abstractionNodeStoreDir);
|
|
515
|
+
const key = JSON.stringify([
|
|
516
|
+
options.memoryDir,
|
|
517
|
+
nodeStoreDir,
|
|
518
|
+
options.anchorsEnabled === true,
|
|
519
|
+
options.temporalExpiredInInjection === true,
|
|
520
|
+
]);
|
|
521
|
+
// The stamp is read BEFORE the build, so a write that lands mid-build leaves the
|
|
522
|
+
// entry stale and the next call rebuilds.
|
|
523
|
+
const stamp = readHarmonicGraphStamp(options.memoryDir, nodeStoreDir);
|
|
524
|
+
const startedAtMs = Date.now();
|
|
525
|
+
const hit = graphCache.get(key);
|
|
526
|
+
if (hit && hit.stamp === stamp && startedAtMs - hit.startedAtMs < GRAPH_CACHE_TTL_MS) {
|
|
527
|
+
harmonicGraphCacheStats.hits++;
|
|
528
|
+
graphCache.delete(key);
|
|
529
|
+
graphCache.set(key, hit);
|
|
530
|
+
return hit.graph;
|
|
531
|
+
}
|
|
532
|
+
harmonicGraphCacheStats.misses++;
|
|
533
|
+
const built = await buildProjectedHarmonicGraph(options);
|
|
534
|
+
const graph = {
|
|
535
|
+
nodes: deepFreeze(built.nodes),
|
|
536
|
+
anchors: deepFreeze(built.anchors),
|
|
537
|
+
};
|
|
538
|
+
graphCache.delete(key);
|
|
539
|
+
graphCache.set(key, { stamp, startedAtMs, graph });
|
|
540
|
+
while (graphCache.size > GRAPH_CACHE_MAX_ENTRIES) {
|
|
541
|
+
const oldest = graphCache.keys().next().value;
|
|
542
|
+
if (oldest === undefined) break;
|
|
543
|
+
graphCache.delete(oldest);
|
|
544
|
+
}
|
|
545
|
+
return graph;
|
|
546
|
+
}
|
|
547
|
+
|
|
463
548
|
export async function searchHarmonicRetrieval(options: {
|
|
464
549
|
memoryDir: string;
|
|
465
550
|
abstractionNodeStoreDir?: string;
|
|
@@ -469,6 +554,8 @@ export async function searchHarmonicRetrieval(options: {
|
|
|
469
554
|
anchorsEnabled: boolean;
|
|
470
555
|
abortSignal?: AbortSignal;
|
|
471
556
|
temporalExpiredInInjection?: boolean;
|
|
557
|
+
/** Share one projected graph between recalls until a write bumps a version sentinel (see readProjectedHarmonicGraph). */
|
|
558
|
+
useGraphCache?: boolean;
|
|
472
559
|
}): Promise<HarmonicRetrievalResult[]> {
|
|
473
560
|
throwIfAborted(options.abortSignal, "harmonic retrieval aborted");
|
|
474
561
|
const queryTokens = new Set(normalizeRecallTokens(options.query, ["what", "which"]));
|
|
@@ -476,7 +563,10 @@ export async function searchHarmonicRetrieval(options: {
|
|
|
476
563
|
|
|
477
564
|
// One projection for every graph reader: nodes and anchors both arrive
|
|
478
565
|
// already narrowed to the namespace's current active memories.
|
|
479
|
-
const { nodes: eligibleNodes, anchors } = await readProjectedHarmonicGraph(
|
|
566
|
+
const { nodes: eligibleNodes, anchors } = await readProjectedHarmonicGraph({
|
|
567
|
+
...options,
|
|
568
|
+
useCache: options.useGraphCache === true,
|
|
569
|
+
});
|
|
480
570
|
const candidates = new Map<string, HarmonicCandidate>();
|
|
481
571
|
|
|
482
572
|
for (const node of eligibleNodes) {
|
|
@@ -507,6 +507,41 @@ test("searchAcrossNamespaces still rejects when the caller aborts", async () =>
|
|
|
507
507
|
assert.equal(degradations.length, 0, "a caller abort is not a backend degradation");
|
|
508
508
|
});
|
|
509
509
|
|
|
510
|
+
test("searchAcrossNamespaces with an already aborted caller leaves no unhandled backend rejection", async () => {
|
|
511
|
+
// The backend leg gets the aborted signal and rejects after the router has
|
|
512
|
+
// already returned its own rejection. Nothing may be left holding that
|
|
513
|
+
// second rejection: on the daemon it is an unhandled AbortError and the
|
|
514
|
+
// process exits (jarvis-ml, 12 Nov 2026).
|
|
515
|
+
class AbortingBackend extends FakeBackend {
|
|
516
|
+
override async search(): Promise<QmdSearchResult[]> {
|
|
517
|
+
await setImmediate();
|
|
518
|
+
throw Object.assign(new Error("QMD inflight search aborted"), { name: "AbortError" });
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
const router = new NamespaceSearchRouter(
|
|
522
|
+
config(),
|
|
523
|
+
{ storageFor: async (namespace: string) => ({ dir: `/tmp/remnic/${namespace}` }) },
|
|
524
|
+
() => new AbortingBackend(false),
|
|
525
|
+
);
|
|
526
|
+
let unhandled = 0;
|
|
527
|
+
const onUnhandled = () => {
|
|
528
|
+
unhandled += 1;
|
|
529
|
+
};
|
|
530
|
+
process.on("unhandledRejection", onUnhandled);
|
|
531
|
+
try {
|
|
532
|
+
const caller = new AbortController();
|
|
533
|
+
caller.abort();
|
|
534
|
+
await assert.rejects(
|
|
535
|
+
router.searchAcrossNamespaces({ query: "a", namespaces: ["main"], maxResults: 10, execution: { signal: caller.signal } }),
|
|
536
|
+
{ message: "operation aborted" },
|
|
537
|
+
);
|
|
538
|
+
for (let i = 0; i < 5; i += 1) await setImmediate();
|
|
539
|
+
assert.equal(unhandled, 0);
|
|
540
|
+
} finally {
|
|
541
|
+
process.off("unhandledRejection", onUnhandled);
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
|
|
510
545
|
test("searchAcrossNamespaces propagates backend errors instead of masking them", async () => {
|
|
511
546
|
const degradations: SearchDegradation[] = [];
|
|
512
547
|
class ThrowingBackend extends FakeBackend {
|
package/src/namespaces/search.ts
CHANGED
|
@@ -622,7 +622,13 @@ class NamespaceSearchAbortError extends Error {
|
|
|
622
622
|
|
|
623
623
|
function awaitWithAbort<T>(operation: Promise<T>, signal?: AbortSignal): Promise<T> {
|
|
624
624
|
if (!signal) return operation;
|
|
625
|
-
if (signal.aborted)
|
|
625
|
+
if (signal.aborted) {
|
|
626
|
+
// The operation was started with this same aborted signal and will reject
|
|
627
|
+
// on its own. Without a handler that second rejection is unhandled and
|
|
628
|
+
// ends the process.
|
|
629
|
+
operation.catch(() => {});
|
|
630
|
+
return Promise.reject(new NamespaceSearchAbortError());
|
|
631
|
+
}
|
|
626
632
|
|
|
627
633
|
return new Promise<T>((resolve, reject) => {
|
|
628
634
|
const onAbort = () => {
|
|
@@ -1537,7 +1537,7 @@ export class RecallInternalCoordinator {
|
|
|
1537
1537
|
sessionKey,
|
|
1538
1538
|
anchorsEnabled: resolveConsolidationCapabilities(this.deps.config).abstractionAnchors,
|
|
1539
1539
|
temporalExpiredInInjection: this.deps.config.temporalExpiredInInjection,
|
|
1540
|
-
abortSignal: harmonicRetrievalAbort.signal,
|
|
1540
|
+
abortSignal: harmonicRetrievalAbort.signal, useGraphCache: true,
|
|
1541
1541
|
}),
|
|
1542
1542
|
),
|
|
1543
1543
|
);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/harmonic-retrieval.ts"],"sourcesContent":["import path from \"node:path\";\nimport { realpath } from \"node:fs/promises\";\nimport { listJsonFilesStrict, readJsonFile } from \"./json-store.js\";\nimport { throwIfAborted } from \"./abort-error.js\";\nimport { inferMemoryStatus, toMemoryPathRel } from \"./memory-lifecycle-ledger-utils.js\";\n import {\n HARMONIC_SOURCE_MEMORY_INSERTED_AT_KEY,\n resolveAbstractionNodeStoreDir,\n validateAbstractionNode,\n type AbstractionNode,\n } from \"./abstraction-nodes.js\";\nimport { resolveCueAnchorStoreDir, validateCueAnchor, type CueAnchor, type CueAnchorType } from \"./cue-anchors.js\";\nimport { compareDeterministicStrings } from \"./deterministic-order.js\";\nimport { countRecallTokenOverlap, normalizeRecallTokens } from \"./recall-tokenization.js\";\nimport { stripAttributesSuffix } from \"./structured-attributes.js\";\nimport { isValidityExpiredNow } from \"./temporal-validity.js\";\nimport { assertSafePathSegment, isRecord } from \"./store-contract.js\";\nimport { isSupportPassportPrivateMemory } from \"./support-passport/card-projection.js\";\nimport type { MemoryFile } from \"./types.js\";\nimport { readProjectedMemoryStates } from \"./memory-projection-batch.js\";\nimport { SecureStoreLockedError } from \"./secure-store/secure-fs.js\";\nimport { StorageManager } from \"./storage.js\";\nimport { RECALL_FALLBACK_DIRS } from \"./utils/category-dir.js\";\nimport { readHarmonicSourceMemory, readUnresolvedHarmonicSources } from \"./harmonic-source-lookup.js\";\n\n\ntype SourceMemoryMap = Map<string, MemoryFile>;\n\n\n// An index older than this is treated as unusable: the date window needed to\n// cover \"created since the last rebuild\" would stop being cheap.\nconst INDEX_MAX_AGE_MS = 7 * 86_400_000;\nconst DAY_MS = 86_400_000;\n\nfunction utcDaysSince(rebuiltMs: number, nowMs: number): string[] {\n const days: string[] = [];\n for (let day = Math.floor((rebuiltMs - DAY_MS) / DAY_MS) * DAY_MS; day <= nowMs; day += DAY_MS) {\n days.push(new Date(day).toISOString().slice(0, 10));\n }\n return days;\n}\n\n\n// Memories the projection has no row for were written after its last rebuild, so\n// their dated directory is normally one of `days`. Hot and cold tiers are probed\n// by exact path. Only recall-corpus categories are probed: `questions/` is a queue\n// and `artifacts/` is outside the recall surface, so neither can be a source.\nfunction unprojectedCandidatePaths(memoryDir: string, id: string, days: string[]): string[] {\n const paths: string[] = [];\n for (const root of [memoryDir, path.join(memoryDir, \"cold\")]) {\n for (const dir of RECALL_FALLBACK_DIRS) {\n paths.push(path.join(root, dir, `${id}.md`));\n for (const day of days) paths.push(path.join(root, dir, day, `${id}.md`));\n }\n }\n return paths;\n}\n\n/** Resolve indexed sources first, then use the durable unresolved-id path cache. */\nasync function readIndexedSourceMemories(options: {\n memoryDir: string;\n abortSignal?: AbortSignal;\n sourceMemoryIds: string[];\n nowMs: number;\n}): Promise<SourceMemoryMap | null> {\n // The ids go to SQL as bound parameters, so any id the projection holds is safe to look up.\n const batch = readProjectedMemoryStates(options.memoryDir, options.sourceMemoryIds);\n const rebuiltMs = Date.parse(batch?.rebuiltAt ?? \"\");\n if (!batch?.complete || !batch.rebuiltAt || !Number.isFinite(rebuiltMs) || options.nowMs - rebuiltMs > INDEX_MAX_AGE_MS) return null;\n const rootReal = await realpath(options.memoryDir).catch(() => null);\n if (rootReal === null) return null;\n\n const storage = new StorageManager(options.memoryDir);\n const days = utcDaysSince(rebuiltMs, options.nowMs);\n const sourceMemories: SourceMemoryMap = new Map();\n const pending: string[] = [];\n for (const id of options.sourceMemoryIds) {\n const state = batch.states.get(id);\n // An id is only ever joined into a path when it is a single safe segment.\n let safeId = true;\n try {\n assertSafePathSegment(id, \"sourceMemoryId\");\n } catch {\n safeId = false;\n }\n // state.path was containment-checked by the projection reader.\n const candidates = state\n ? [state.path, path.join(options.memoryDir, \"cold\", state.pathRel)]\n : safeId ? unprojectedCandidatePaths(options.memoryDir, id, days) : [];\n let found = false;\n for (const candidate of candidates) {\n const memory = await readHarmonicSourceMemory(storage, rootReal, candidate, options.abortSignal);\n if (memory?.frontmatter.id !== id) continue;\n sourceMemories.set(id, memory);\n found = true;\n break;\n }\n if (!found) pending.push(id);\n }\n if (pending.length === 0) return sourceMemories;\n\n const unresolved = await readUnresolvedHarmonicSources({\n storage, rootReal, rebuiltAt: batch.rebuiltAt, sourceMemoryIds: pending,\n nowMs: options.nowMs, abortSignal: options.abortSignal,\n });\n for (const [id, memory] of unresolved) sourceMemories.set(id, memory);\n return sourceMemories;\n}\n\nasync function readCorpusSourceMemories(options: {\n memoryDir: string;\n abortSignal?: AbortSignal;\n}): Promise<SourceMemoryMap> {\n const sourceMemories: SourceMemoryMap = new Map();\n try {\n const storage = new StorageManager(options.memoryDir);\n const [hotMemories, coldMemories] = await Promise.all([\n storage.readAllMemories({ abortSignal: options.abortSignal }),\n storage.readAllColdMemories({ abortSignal: options.abortSignal }),\n ]);\n for (const memory of [...hotMemories, ...coldMemories]) {\n throwIfAborted(options.abortSignal, \"harmonic retrieval aborted\");\n sourceMemories.set(memory.frontmatter.id, memory);\n }\n } catch (err) {\n throwIfAborted(options.abortSignal, \"harmonic retrieval aborted\");\n if (err instanceof SecureStoreLockedError) return new Map();\n // Source validation fails closed when the authorized memory directory is\n // unreadable. Source-less nodes remain eligible for retrieval.\n sourceMemories.clear();\n }\n return sourceMemories;\n}\n\nasync function readSourceMemories(options: {\n memoryDir: string;\n abortSignal?: AbortSignal;\n sourceMemoryIds: string[];\n nowMs?: number;\n}): Promise<SourceMemoryMap> {\n try {\n const indexed = await readIndexedSourceMemories({\n memoryDir: options.memoryDir,\n abortSignal: options.abortSignal,\n sourceMemoryIds: options.sourceMemoryIds,\n nowMs: options.nowMs ?? Date.now(),\n });\n if (indexed) return indexed;\n } catch (err) {\n throwIfAborted(options.abortSignal, \"harmonic retrieval aborted\");\n if (err instanceof SecureStoreLockedError) return new Map();\n }\n return readCorpusSourceMemories(options);\n}\n\nfunction parseSourceMemoryInsertedAt(node: AbstractionNode): Map<string, string> {\n const insertedAtById = new Map<string, string>();\n const insertedAtRaw = node.metadata?.[HARMONIC_SOURCE_MEMORY_INSERTED_AT_KEY];\n if (!insertedAtRaw) return insertedAtById;\n try {\n const parsed = JSON.parse(insertedAtRaw) as unknown;\n if (!isRecord(parsed)) return insertedAtById;\n for (const [memoryId, value] of Object.entries(parsed)) {\n if (typeof value === \"string\") insertedAtById.set(memoryId, value);\n }\n } catch {\n // Malformed insertion metadata leaves ordering on the recordedAt fallback.\n }\n return insertedAtById;\n}\n\nfunction projectSourceBackedNode(\n node: AbstractionNode,\n sourceMemories: SourceMemoryMap,\n memoryDir: string,\n temporalExpiredInInjection: boolean,\n nowMs: number\n): AbstractionNode | null {\n const sourceMemoryIds = node.sourceMemoryIds ?? [];\n if (sourceMemoryIds.length === 0) return node;\n\n const activeSourceMemoryIds = sourceMemoryIds.filter((memoryId) => {\n const memory = sourceMemories.get(memoryId);\n return (\n memory !== undefined &&\n // #2332: a private support-passport record leaves the eligible set the\n // same way a rejected one does — a node title or anchor attribution\n // rebuilt from it would leak governed metadata into the deep-recall\n // frontier (and every other graph reader) before any content check runs.\n !isSupportPassportPrivateMemory(memory) &&\n inferMemoryStatus(memory.frontmatter, toMemoryPathRel(memoryDir, memory.path)) === \"active\" &&\n (temporalExpiredInInjection || !isValidityExpiredNow(memory.frontmatter, nowMs))\n );\n });\n if (activeSourceMemoryIds.length === 0) return null;\n\n // Newest-first source order keeps the projected title and summary on the most\n // recently inserted facts. Equal or missing timestamps fall back to ascending\n // id order, which matches the stored sourceMemoryIds order.\n const insertedAtById = parseSourceMemoryInsertedAt(node);\n const insertedAtMs = (memoryId: string): number => {\n const parsedMs = Date.parse(insertedAtById.get(memoryId) ?? \"\");\n return Number.isFinite(parsedMs) ? parsedMs : Date.parse(node.recordedAt);\n };\n activeSourceMemoryIds.sort(\n (left, right) =>\n insertedAtMs(right) - insertedAtMs(left) || compareDeterministicStrings(left, right)\n );\n\n const activeMemories = activeSourceMemoryIds.flatMap((memoryId) => {\n const memory = sourceMemories.get(memoryId);\n return memory ? [memory] : [];\n });\n const activeContents = activeMemories.map((memory) =>\n memory.frontmatter.structuredAttributes ? stripAttributesSuffix(memory.content) : memory.content\n );\n let metadata: Record<string, string> | undefined;\n const activeInsertedAt = activeSourceMemoryIds.flatMap((memoryId) => {\n const value = insertedAtById.get(memoryId);\n return typeof value === \"string\" ? [[memoryId, value] as const] : [];\n });\n if (activeInsertedAt.length > 0) {\n metadata = {\n [HARMONIC_SOURCE_MEMORY_INSERTED_AT_KEY]: JSON.stringify(Object.fromEntries(activeInsertedAt)),\n };\n }\n\n return {\n ...node,\n sourceMemoryIds: activeSourceMemoryIds,\n title: activeContents[0]?.slice(0, 80) ?? node.title,\n summary: activeContents.slice(0, 3).join(\"; \").slice(0, 400),\n tags: [...new Set(activeMemories.flatMap((memory) => memory.frontmatter.tags))].sort(compareDeterministicStrings),\n entityRefs: [\n ...new Set(\n activeMemories.flatMap((memory) =>\n typeof memory.frontmatter.entityRef === \"string\" ? [memory.frontmatter.entityRef] : []\n )\n ),\n ].sort(compareDeterministicStrings),\n metadata,\n };\n}\n\nfunction anchorMatchesProjectedNode(\n anchor: CueAnchor,\n nodeRef: string,\n node: AbstractionNode,\n allowLegacyAttribution: boolean\n): boolean {\n const activeSourceMemoryIds = node.sourceMemoryIds ?? [];\n if (activeSourceMemoryIds.length === 0) return true;\n if (!anchor.sourceMemoryIdsByNodeRef) return allowLegacyAttribution;\n const anchorSourceMemoryIds = anchor.sourceMemoryIdsByNodeRef[nodeRef] ?? [];\n return anchorSourceMemoryIds.some((sourceMemoryId) => activeSourceMemoryIds.includes(sourceMemoryId));\n}\n\nexport interface HarmonicMatchedAnchor {\n anchorId: string;\n anchorType: CueAnchorType;\n anchorValue: string;\n}\n\nexport interface HarmonicRetrievalResult {\n node: AbstractionNode;\n score: number;\n nodeScore: number;\n anchorScore: number;\n matchedFields: string[];\n matchedAnchors: HarmonicMatchedAnchor[];\n}\n\ninterface HarmonicCandidate {\n node: AbstractionNode;\n nodeScore: number;\n anchorScore: number;\n matchedFields: Set<string>;\n matchedAnchors: Map<string, HarmonicMatchedAnchor>;\n}\n\nfunction scoreNode(node: AbstractionNode, queryTokens: Set<string>): { score: number; matchedFields: string[] } {\n const matchedFields: string[] = [];\n let score = 0;\n\n const titleMatches = countRecallTokenOverlap(queryTokens, node.title);\n if (titleMatches > 0) {\n score += titleMatches * 3;\n matchedFields.push(\"title\");\n }\n\n const summaryMatches = countRecallTokenOverlap(queryTokens, node.summary);\n if (summaryMatches > 0) {\n score += summaryMatches * 3;\n matchedFields.push(\"summary\");\n }\n\n const tagMatches = countRecallTokenOverlap(queryTokens, node.tags?.join(\" \"));\n if (tagMatches > 0) {\n score += tagMatches * 2;\n matchedFields.push(\"tags\");\n }\n\n const entityMatches = countRecallTokenOverlap(queryTokens, node.entityRefs?.join(\" \"));\n if (entityMatches > 0) {\n score += entityMatches * 2;\n matchedFields.push(\"entityRefs\");\n }\n\n const kindMatches = countRecallTokenOverlap(queryTokens, `${node.kind} ${node.abstractionLevel}`);\n if (kindMatches > 0) {\n score += kindMatches;\n matchedFields.push(\"kind\");\n }\n\n return { score, matchedFields };\n}\n\nfunction scoreAnchor(\n anchor: CueAnchor,\n queryTokens: Set<string>,\n eligibleNodeTags: readonly string[]\n): { score: number; matchedFields: string[] } {\n const matchedFields: string[] = [];\n let score = 0;\n\n const valueMatches = countRecallTokenOverlap(queryTokens, anchor.anchorValue);\n const normalizedMatches = countRecallTokenOverlap(queryTokens, anchor.normalizedCue);\n const cueMatches = Math.max(valueMatches, normalizedMatches);\n if (cueMatches > 0) {\n score += cueMatches * 4;\n if (valueMatches > 0) matchedFields.push(\"anchorValue\");\n if (normalizedMatches > 0) matchedFields.push(\"anchor\");\n }\n\n const typeMatches = countRecallTokenOverlap(queryTokens, anchor.anchorType);\n if (typeMatches > 0) {\n score += typeMatches;\n matchedFields.push(\"anchorType\");\n }\n\n const tagMatches = countRecallTokenOverlap(queryTokens, eligibleNodeTags.join(\" \"));\n if (tagMatches > 0) {\n score += tagMatches * 2;\n matchedFields.push(\"anchorTags\");\n }\n\n return { score, matchedFields };\n}\n\nexport async function readAbstractionNodes(options: {\n memoryDir: string;\n abstractionNodeStoreDir?: string;\n}): Promise<AbstractionNode[]> {\n const rootDir = resolveAbstractionNodeStoreDir(options.memoryDir, options.abstractionNodeStoreDir);\n const files = await listJsonFilesStrict(path.join(rootDir, \"nodes\"), { allowMissingDirectory: true });\n const nodes: AbstractionNode[] = [];\n for (const filePath of files) {\n try {\n nodes.push(validateAbstractionNode(await readJsonFile(filePath)));\n } catch {\n // fail-open: invalid artifacts stay visible via status tooling instead of recall\n }\n }\n return nodes;\n}\n\nexport async function readCueAnchors(options: {\n memoryDir: string;\n abstractionNodeStoreDir?: string;\n}): Promise<CueAnchor[]> {\n const abstractionRoot = resolveAbstractionNodeStoreDir(options.memoryDir, options.abstractionNodeStoreDir);\n const rootDir = resolveCueAnchorStoreDir(abstractionRoot);\n const files = await listJsonFilesStrict(rootDir, { allowMissingDirectory: true });\n const anchors: CueAnchor[] = [];\n for (const filePath of files) {\n try {\n anchors.push(validateCueAnchor(await readJsonFile(filePath)));\n } catch {\n // fail-open: invalid artifacts stay visible via status tooling instead of recall\n }\n }\n return anchors;\n}\n\nexport interface ProjectedHarmonicGraph {\n nodes: AbstractionNode[];\n anchors: CueAnchor[];\n}\n\n/**\n * Read nodes + anchors and project BOTH against the namespace's current active\n * memories. This is the single projection every graph reader shares: a raw\n * `readAbstractionNodes` / `readCueAnchors` pair still carries the stored title,\n * summary, tags, and anchor attribution of a memory that was later rejected,\n * quarantined, or otherwise moved out of the active set, so a second reader\n * that skipped the projection could leak governed metadata even when the\n * memory itself is excluded from the final results.\n *\n * - A node whose every source memory left the active set is dropped.\n * - A surviving node's title/summary/tags/entityRefs are rebuilt from its\n * active sources only.\n * - An anchor keeps a node reference only while its attribution runs through\n * one of that node's surviving active sources, and is dropped once no\n * reference survives.\n */\nexport async function readProjectedHarmonicGraph(options: {\n memoryDir: string;\n abstractionNodeStoreDir?: string;\n anchorsEnabled: boolean;\n abortSignal?: AbortSignal;\n temporalExpiredInInjection?: boolean;\n nowMs?: number;\n}): Promise<ProjectedHarmonicGraph> {\n throwIfAborted(options.abortSignal, \"harmonic retrieval aborted\");\n const nodes = await readAbstractionNodes(options);\n const sourceMemoryIds = [...new Set(nodes.flatMap((node) => node.sourceMemoryIds ?? []))];\n const sourceMemories: SourceMemoryMap = sourceMemoryIds.length > 0\n ? await readSourceMemories({\n memoryDir: options.memoryDir,\n abortSignal: options.abortSignal,\n sourceMemoryIds,\n nowMs: options.nowMs,\n })\n : new Map();\n const nowMs = options.nowMs ?? Date.now();\n // A node whose full stored source set survived projection keeps legacy\n // anchor attribution (an anchor written before per-node source ids existed).\n const legacyCompatibleNodeRefs = new Set<string>();\n const projectedNodes = nodes.flatMap((node) => {\n const projected = projectSourceBackedNode(\n node,\n sourceMemories,\n options.memoryDir,\n options.temporalExpiredInInjection === true,\n nowMs\n );\n if (\n projected &&\n (node.sourceMemoryIds?.length ?? 0) > 0 &&\n projected.sourceMemoryIds?.length === node.sourceMemoryIds?.length\n ) {\n legacyCompatibleNodeRefs.add(node.nodeId);\n }\n return projected ? [projected] : [];\n });\n if (!options.anchorsEnabled) return { nodes: projectedNodes, anchors: [] };\n throwIfAborted(options.abortSignal, \"harmonic retrieval aborted\");\n const nodeIndex = new Map(projectedNodes.map((node) => [node.nodeId, node]));\n const anchors = (await readCueAnchors(options)).flatMap((anchor) => {\n throwIfAborted(options.abortSignal, \"harmonic retrieval aborted\");\n const nodeRefs = anchor.nodeRefs.filter((nodeRef) => {\n const node = nodeIndex.get(nodeRef);\n return (\n node !== undefined &&\n anchorMatchesProjectedNode(anchor, nodeRef, node, legacyCompatibleNodeRefs.has(nodeRef))\n );\n });\n return nodeRefs.length > 0 ? [{ ...anchor, nodeRefs }] : [];\n });\n return { nodes: projectedNodes, anchors };\n}\n\nexport async function searchHarmonicRetrieval(options: {\n memoryDir: string;\n abstractionNodeStoreDir?: string;\n query: string;\n maxResults: number;\n sessionKey?: string;\n anchorsEnabled: boolean;\n abortSignal?: AbortSignal;\n temporalExpiredInInjection?: boolean;\n}): Promise<HarmonicRetrievalResult[]> {\n throwIfAborted(options.abortSignal, \"harmonic retrieval aborted\");\n const queryTokens = new Set(normalizeRecallTokens(options.query, [\"what\", \"which\"]));\n if (queryTokens.size === 0 || options.maxResults <= 0) return [];\n\n // One projection for every graph reader: nodes and anchors both arrive\n // already narrowed to the namespace's current active memories.\n const { nodes: eligibleNodes, anchors } = await readProjectedHarmonicGraph(options);\n const candidates = new Map<string, HarmonicCandidate>();\n\n for (const node of eligibleNodes) {\n throwIfAborted(options.abortSignal, \"harmonic retrieval aborted\");\n const { score, matchedFields } = scoreNode(node, queryTokens);\n if (score <= 0) continue;\n candidates.set(node.nodeId, {\n node,\n nodeScore: score,\n anchorScore: 0,\n matchedFields: new Set(matchedFields),\n matchedAnchors: new Map(),\n });\n }\n\n if (options.anchorsEnabled) {\n throwIfAborted(options.abortSignal, \"harmonic retrieval aborted\");\n const nodeIndex = new Map(eligibleNodes.map((node) => [node.nodeId, node]));\n for (const anchor of anchors) {\n throwIfAborted(options.abortSignal, \"harmonic retrieval aborted\");\n // `nodeRefs` are the projection's surviving references already.\n const eligibleNodeRefs = anchor.nodeRefs;\n const eligibleNodeTags = new Set(eligibleNodeRefs.flatMap((nodeRef) => nodeIndex.get(nodeRef)?.tags ?? []));\n const eligibleAnchorTags = anchor.tags?.filter((tag) => eligibleNodeTags.has(tag)) ?? [];\n const { score, matchedFields } = scoreAnchor(anchor, queryTokens, eligibleAnchorTags);\n if (score <= 0) continue;\n for (const nodeRef of eligibleNodeRefs) {\n const node = nodeIndex.get(nodeRef);\n if (!node) continue;\n const existing = candidates.get(nodeRef) ?? {\n node,\n nodeScore: 0,\n anchorScore: 0,\n matchedFields: new Set<string>(),\n matchedAnchors: new Map<string, HarmonicMatchedAnchor>(),\n };\n existing.anchorScore += score;\n existing.matchedFields.add(\"anchor\");\n for (const field of matchedFields) existing.matchedFields.add(field);\n existing.matchedAnchors.set(anchor.anchorId, {\n anchorId: anchor.anchorId,\n anchorType: anchor.anchorType,\n anchorValue: anchor.anchorValue,\n });\n candidates.set(nodeRef, existing);\n }\n }\n }\n\n return [...candidates.values()]\n .map((candidate) => {\n let score = candidate.nodeScore + candidate.anchorScore;\n if (options.sessionKey && candidate.node.sessionKey === options.sessionKey) score += 0.5;\n return {\n node: candidate.node,\n score,\n nodeScore: candidate.nodeScore,\n anchorScore: candidate.anchorScore,\n matchedFields: [...candidate.matchedFields].sort(),\n matchedAnchors: [...candidate.matchedAnchors.values()].sort(\n (left, right) =>\n left.anchorType.localeCompare(right.anchorType) || left.anchorValue.localeCompare(right.anchorValue)\n ),\n };\n })\n .filter((result) => result.nodeScore > 0 || result.anchorScore > 0)\n .sort(\n (left, right) =>\n right.score - left.score ||\n right.anchorScore - left.anchorScore ||\n right.node.recordedAt.localeCompare(left.node.recordedAt)\n )\n .slice(0, options.maxResults);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/abstraction-nodes.ts"],"sourcesContent":["import { mkdir, unlink, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { listJsonFilesStrict, readJsonFile, withJsonStoreMutationLock, writeJsonFileAtomic } from \"./json-store.js\";\nimport { compareDeterministicStrings, mergeSortedUniqueStrings } from \"./deterministic-order.js\";\nimport { isNotFoundError } from \"./utils/errno.js\";\nimport {\n assertIsoRecordedAt,\n assertSafePathSegment,\n assertString,\n isRecord,\n optionalStringArray,\n recordStoreDay,\n validateStringRecord,\n} from \"./store-contract.js\";\n\nexport type AbstractionNodeKind = \"episode\" | \"topic\" | \"project\" | \"workflow\" | \"constraint\";\nexport type AbstractionLevel = \"micro\" | \"meso\" | \"macro\";\n\nexport interface AbstractionNode {\n schemaVersion: 1;\n nodeId: string;\n recordedAt: string;\n sessionKey: string;\n kind: AbstractionNodeKind;\n abstractionLevel: AbstractionLevel;\n title: string;\n summary: string;\n sourceMemoryIds?: string[];\n entityRefs?: string[];\n tags?: string[];\n metadata?: Record<string, string>;\n}\n\nexport interface AbstractionNodeStoreStatus {\n enabled: boolean;\n anchorsEnabled: boolean;\n rootDir: string;\n nodesDir: string;\n nodes: {\n total: number;\n valid: number;\n invalid: number;\n byKind: Partial<Record<AbstractionNodeKind, number>>;\n byLevel: Partial<Record<AbstractionLevel, number>>;\n latestNodeId?: string;\n latestRecordedAt?: string;\n latestSessionKey?: string;\n };\n latestNode?: AbstractionNode;\n invalidNodes: Array<{\n path: string;\n error: string;\n }>;\n}\n\nfunction validateKind(raw: unknown): AbstractionNodeKind {\n const value = assertString(raw, \"kind\");\n if (![\"episode\", \"topic\", \"project\", \"workflow\", \"constraint\"].includes(value)) {\n throw new Error(\"kind must be one of episode|topic|project|workflow|constraint\");\n }\n return value as AbstractionNodeKind;\n}\n\nfunction validateLevel(raw: unknown): AbstractionLevel {\n const value = assertString(raw, \"abstractionLevel\");\n if (![\"micro\", \"meso\", \"macro\"].includes(value)) {\n throw new Error(\"abstractionLevel must be one of micro|meso|macro\");\n }\n return value as AbstractionLevel;\n}\n\nexport function resolveAbstractionNodeStoreDir(memoryDir: string, overrideDir?: string): string {\n if (typeof overrideDir === \"string\" && overrideDir.trim().length > 0) {\n return overrideDir.trim();\n }\n return path.join(memoryDir, \"state\", \"abstraction-nodes\");\n}\n\nexport function validateAbstractionNode(raw: unknown): AbstractionNode {\n if (!isRecord(raw)) throw new Error(\"abstraction node must be an object\");\n if (raw.schemaVersion !== 1) throw new Error(\"schemaVersion must be 1\");\n\n return {\n schemaVersion: 1,\n nodeId: assertSafePathSegment(assertString(raw.nodeId, \"nodeId\"), \"nodeId\"),\n recordedAt: assertIsoRecordedAt(assertString(raw.recordedAt, \"recordedAt\")),\n sessionKey: assertString(raw.sessionKey, \"sessionKey\"),\n kind: validateKind(raw.kind),\n abstractionLevel: validateLevel(raw.abstractionLevel),\n title: assertString(raw.title, \"title\"),\n summary: assertString(raw.summary, \"summary\"),\n sourceMemoryIds: optionalStringArray(raw.sourceMemoryIds, \"sourceMemoryIds\"),\n entityRefs: optionalStringArray(raw.entityRefs, \"entityRefs\"),\n tags: optionalStringArray(raw.tags, \"tags\"),\n metadata: validateStringRecord(raw.metadata, \"metadata\"),\n };\n}\n\nexport async function recordAbstractionNode(options: {\n memoryDir: string;\n abstractionNodeStoreDir?: string;\n node: AbstractionNode;\n}): Promise<string> {\n const rootDir = resolveAbstractionNodeStoreDir(options.memoryDir, options.abstractionNodeStoreDir);\n const validated = validateAbstractionNode(options.node);\n const day = recordStoreDay(validated.recordedAt);\n const nodesDir = path.join(rootDir, \"nodes\", day);\n const filePath = path.join(nodesDir, `${validated.nodeId}.json`);\n await mkdir(nodesDir, { recursive: true });\n await writeFile(filePath, JSON.stringify(validated, null, 2), \"utf8\");\n return filePath;\n}\n\nconst MAX_ABSTRACTION_NODE_VALUES = 50;\n\nfunction mergeSortedValues(existing: string[] | undefined, incoming: string[] | undefined): string[] {\n return mergeSortedUniqueStrings(existing, incoming).slice(0, MAX_ABSTRACTION_NODE_VALUES);\n}\n\nexport const HARMONIC_SOURCE_MEMORY_INSERTED_AT_KEY = \"remnic.harmonic.sourceMemoryInsertedAt.v1\";\n\nfunction sourceMemoryInsertedAt(node: AbstractionNode | undefined): Map<string, string> {\n const insertedAtById = new Map<string, string>();\n if (!node) return insertedAtById;\n let stored: Record<string, unknown> = {};\n try {\n const parsed = JSON.parse(node.metadata?.[HARMONIC_SOURCE_MEMORY_INSERTED_AT_KEY] ?? \"{}\") as unknown;\n stored = isRecord(parsed) ? parsed : {};\n } catch {\n stored = {};\n }\n for (const memoryId of node.sourceMemoryIds ?? []) {\n const insertedAt = stored[memoryId];\n insertedAtById.set(\n memoryId,\n typeof insertedAt === \"string\" && Number.isFinite(Date.parse(insertedAt)) ? insertedAt : node.recordedAt\n );\n }\n return insertedAtById;\n}\n\nfunction mergeRecentSourceMemoryIds(\n existing: AbstractionNode | undefined,\n incoming: AbstractionNode\n): { ids: string[]; insertedAtJson: string } {\n const insertedAtById = sourceMemoryInsertedAt(existing);\n const incomingSources = [...sourceMemoryInsertedAt(incoming).entries()].sort(\n ([leftId, leftAt], [rightId, rightAt]) =>\n Date.parse(leftAt) - Date.parse(rightAt) || compareDeterministicStrings(leftId, rightId)\n );\n let nextInsertedAtMs = Math.max(\n Date.now(),\n [...insertedAtById.values()].reduce((latest, insertedAt) => Math.max(latest, Date.parse(insertedAt) + 1), 0)\n );\n for (const [memoryId] of incomingSources) {\n if (insertedAtById.has(memoryId)) continue;\n insertedAtById.set(memoryId, new Date(nextInsertedAtMs).toISOString());\n nextInsertedAtMs++;\n }\n const retained = [...insertedAtById.entries()]\n .sort(\n ([leftId, leftAt], [rightId, rightAt]) =>\n Date.parse(rightAt) - Date.parse(leftAt) || compareDeterministicStrings(leftId, rightId)\n )\n .slice(0, MAX_ABSTRACTION_NODE_VALUES);\n const ids = retained.map(([memoryId]) => memoryId).sort(compareDeterministicStrings);\n const retainedInsertedAt = Object.fromEntries(ids.map((memoryId) => [memoryId, insertedAtById.get(memoryId)]));\n return { ids, insertedAtJson: JSON.stringify(retainedInsertedAt) };\n}\n\ntype AbstractionNodeFile = {\n filePath: string;\n node: AbstractionNode;\n};\n\nfunction compareNodeFiles(left: AbstractionNodeFile, right: AbstractionNodeFile): number {\n return (\n Date.parse(right.node.recordedAt) - Date.parse(left.node.recordedAt) ||\n compareDeterministicStrings(left.filePath, right.filePath)\n );\n}\n\nfunction compareIncomingNodes(left: AbstractionNode, right: AbstractionNode): number {\n return (\n Date.parse(left.recordedAt) - Date.parse(right.recordedAt) ||\n compareDeterministicStrings(canonicalNodeKey(left), canonicalNodeKey(right))\n );\n}\n\nfunction canonicalMetadata(metadata: Record<string, string> | undefined): Record<string, string> {\n return Object.fromEntries(\n Object.entries(metadata ?? {}).sort(([left], [right]) => compareDeterministicStrings(left, right))\n );\n}\n\nfunction canonicalNodeKey(node: AbstractionNode): string {\n return JSON.stringify({\n ...node,\n metadata: canonicalMetadata(node.metadata),\n });\n}\n\nfunction mergeAbstractionNodes(existing: AbstractionNode | undefined, incoming: AbstractionNode): AbstractionNode {\n const incomingIsNewest = !existing || Date.parse(existing.recordedAt) <= Date.parse(incoming.recordedAt);\n const newest = incomingIsNewest ? incoming : existing;\n const older = incomingIsNewest ? existing : incoming;\n if (!newest) throw new Error(\"abstraction node merge requires a node\");\n const retainedSources = mergeRecentSourceMemoryIds(existing, incoming);\n const mergedMetadata = canonicalMetadata({\n ...(older?.metadata ?? {}),\n ...(newest.metadata ?? {}),\n [HARMONIC_SOURCE_MEMORY_INSERTED_AT_KEY]: retainedSources.insertedAtJson,\n });\n return validateAbstractionNode({\n ...newest,\n sourceMemoryIds: retainedSources.ids,\n entityRefs: mergeSortedValues(existing?.entityRefs, incoming.entityRefs),\n tags: mergeSortedValues(existing?.tags, incoming.tags),\n metadata: mergedMetadata,\n });\n}\n\nexport async function upsertAbstractionNodes(options: {\n memoryDir: string;\n abstractionNodeStoreDir?: string;\n nodes: AbstractionNode[];\n}): Promise<string[]> {\n const rootDir = resolveAbstractionNodeStoreDir(options.memoryDir, options.abstractionNodeStoreDir);\n const incomingNodes = options.nodes.map(validateAbstractionNode);\n if (incomingNodes.length === 0) return [];\n return withJsonStoreMutationLock(rootDir, async () => {\n const nodesDir = path.join(rootDir, \"nodes\");\n const nodeFiles = await listJsonFilesStrict(nodesDir, { allowMissingDirectory: true });\n const existingNodes = await Promise.all(\n nodeFiles.map(async (filePath) => ({\n filePath,\n node: validateAbstractionNode(await readJsonFile(filePath)),\n }))\n );\n const existingById = new Map<string, AbstractionNodeFile[]>();\n for (const existing of existingNodes) {\n const records = existingById.get(existing.node.nodeId) ?? [];\n records.push(existing);\n existingById.set(existing.node.nodeId, records);\n }\n const incomingById = new Map<string, AbstractionNode[]>();\n for (const incoming of incomingNodes) {\n const records = incomingById.get(incoming.nodeId) ?? [];\n records.push(incoming);\n incomingById.set(incoming.nodeId, records);\n }\n\n const filePaths: string[] = [];\n for (const nodeId of [...incomingById.keys()].sort(compareDeterministicStrings)) {\n const existing = (existingById.get(nodeId) ?? []).sort(compareNodeFiles);\n const incoming = (incomingById.get(nodeId) ?? []).sort(compareIncomingNodes);\n let merged: AbstractionNode | undefined;\n for (const next of [...existing].reverse()) merged = mergeAbstractionNodes(merged, next.node);\n for (const next of incoming) merged = mergeAbstractionNodes(merged, next);\n if (!merged) throw new Error(`abstraction node batch has no node: ${nodeId}`);\n const filePath =\n existing[0]?.filePath ?? path.join(nodesDir, recordStoreDay(merged.recordedAt), `${merged.nodeId}.json`);\n await writeJsonFileAtomic(filePath, merged);\n for (const duplicate of existing.slice(1)) {\n await unlink(duplicate.filePath).catch((error) => {\n if (!isNotFoundError(error)) throw error;\n });\n }\n filePaths.push(filePath);\n }\n return filePaths;\n });\n}\n\nexport async function upsertAbstractionNode(options: {\n memoryDir: string;\n abstractionNodeStoreDir?: string;\n node: AbstractionNode;\n}): Promise<string> {\n const [filePath] = await upsertAbstractionNodes({ ...options, nodes: [options.node] });\n if (!filePath) throw new Error(\"abstraction node upsert did not write a file\");\n return filePath;\n}\n\nexport async function getAbstractionNodeStoreStatus(options: {\n memoryDir: string;\n abstractionNodeStoreDir?: string;\n enabled: boolean;\n anchorsEnabled: boolean;\n}): Promise<AbstractionNodeStoreStatus> {\n const rootDir = resolveAbstractionNodeStoreDir(options.memoryDir, options.abstractionNodeStoreDir);\n const nodesDir = path.join(rootDir, \"nodes\");\n const files = await listJsonFilesStrict(nodesDir, { allowMissingDirectory: true });\n const nodes: AbstractionNode[] = [];\n const invalidNodes: Array<{ path: string; error: string }> = [];\n\n for (const filePath of files) {\n try {\n nodes.push(validateAbstractionNode(await readJsonFile(filePath)));\n } catch (error) {\n invalidNodes.push({\n path: filePath,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n nodes.sort(\n (a, b) => Date.parse(b.recordedAt) - Date.parse(a.recordedAt) || compareDeterministicStrings(a.nodeId, b.nodeId)\n );\n\n const byKind: Partial<Record<AbstractionNodeKind, number>> = {};\n const byLevel: Partial<Record<AbstractionLevel, number>> = {};\n for (const node of nodes) {\n byKind[node.kind] = (byKind[node.kind] ?? 0) + 1;\n byLevel[node.abstractionLevel] = (byLevel[node.abstractionLevel] ?? 0) + 1;\n }\n\n return {\n enabled: options.enabled,\n anchorsEnabled: options.anchorsEnabled,\n rootDir,\n nodesDir,\n nodes: {\n total: files.length,\n valid: nodes.length,\n invalid: invalidNodes.length,\n byKind,\n byLevel,\n latestNodeId: nodes[0]?.nodeId,\n latestRecordedAt: nodes[0]?.recordedAt,\n latestSessionKey: nodes[0]?.sessionKey,\n },\n latestNode: nodes[0],\n invalidNodes,\n };\n}\n"],"mappings":"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","names":[]}
|