@remnic/core 10.0.99 → 10.0.101
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 +2 -2
- package/dist/access-cli.js +2 -2
- package/dist/access-extraction-force-flush.js +2 -2
- package/dist/access-http-briefing.js +2 -2
- package/dist/access-http-query.js +2 -2
- package/dist/access-http.js +2 -2
- package/dist/access-identity-continuity-surface.js +2 -2
- package/dist/access-lcm-surface.js +2 -2
- package/dist/access-mcp.js +2 -2
- package/dist/access-observe-write-surface.js +2 -2
- package/dist/access-operations-batch.js +2 -2
- package/dist/access-operations.js +2 -2
- package/dist/access-recall-concurrency.js +2 -2
- package/dist/access-recall-response.js +2 -2
- package/dist/access-recall-surface.js +2 -2
- package/dist/access-service.js +2 -2
- package/dist/{chunk-VZMRKYNR.js → chunk-MMOXCCHF.js} +2 -2
- package/dist/{chunk-4PXDXCNE.js → chunk-PL36IQPL.js} +47 -15
- package/dist/chunk-PL36IQPL.js.map +1 -0
- package/dist/{chunk-UBJ5RPDM.js → chunk-QIPDTAWZ.js} +2 -2
- package/dist/cli.js +2 -2
- package/dist/explicit-capture.js +2 -2
- package/dist/harmonic-retrieval.d.ts +5 -4
- package/dist/harmonic-retrieval.d.ts.map +1 -1
- package/dist/harmonic-retrieval.js +1 -1
- package/dist/index.js +2 -2
- package/dist/orchestration/backlog-scheduler.js +3 -3
- package/dist/orchestration/buffer-drain-runner.js +3 -3
- package/dist/orchestrator.js +2 -2
- package/dist/recall-navigation.js +2 -2
- package/package.json +2 -2
- package/src/entity-lazy-compatibility.test.ts +34 -24
- package/src/harmonic-graph-cache.test.ts +187 -2
- package/src/harmonic-retrieval.ts +77 -19
- package/dist/chunk-4PXDXCNE.js.map +0 -1
- /package/dist/{chunk-VZMRKYNR.js.map → chunk-MMOXCCHF.js.map} +0 -0
- /package/dist/{chunk-UBJ5RPDM.js.map → chunk-QIPDTAWZ.js.map} +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BufferDrainRunner,
|
|
3
3
|
ClaimBufferStorage
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import "../chunk-
|
|
4
|
+
} from "../chunk-QIPDTAWZ.js";
|
|
5
|
+
import "../chunk-MMOXCCHF.js";
|
|
6
6
|
import "../chunk-6P7U3HWG.js";
|
|
7
7
|
import "../chunk-PQM3B54K.js";
|
|
8
8
|
import "../chunk-B3DR2JBZ.js";
|
|
@@ -175,7 +175,7 @@ import "../chunk-MDPT6QFK.js";
|
|
|
175
175
|
import "../chunk-UOBPK2OS.js";
|
|
176
176
|
import "../chunk-2YA5N2BY.js";
|
|
177
177
|
import "../chunk-3HSRWJTC.js";
|
|
178
|
-
import "../chunk-
|
|
178
|
+
import "../chunk-PL36IQPL.js";
|
|
179
179
|
import "../chunk-HUDPXTXW.js";
|
|
180
180
|
import "../chunk-AZEZSQ5R.js";
|
|
181
181
|
import "../chunk-YAZNBMNF.js";
|
package/dist/orchestrator.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
mergeGraphExpandedResults,
|
|
7
7
|
qmdStartupCollectionChecksWithTimeout,
|
|
8
8
|
resolveEffectiveIdentityInjectionMode
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-MMOXCCHF.js";
|
|
10
10
|
import "./chunk-6P7U3HWG.js";
|
|
11
11
|
import "./chunk-PQM3B54K.js";
|
|
12
12
|
import "./chunk-B3DR2JBZ.js";
|
|
@@ -183,7 +183,7 @@ import "./chunk-MDPT6QFK.js";
|
|
|
183
183
|
import "./chunk-UOBPK2OS.js";
|
|
184
184
|
import "./chunk-2YA5N2BY.js";
|
|
185
185
|
import "./chunk-3HSRWJTC.js";
|
|
186
|
-
import "./chunk-
|
|
186
|
+
import "./chunk-PL36IQPL.js";
|
|
187
187
|
import "./chunk-HUDPXTXW.js";
|
|
188
188
|
import "./chunk-AZEZSQ5R.js";
|
|
189
189
|
import "./chunk-YAZNBMNF.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
runRecallNavigation
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-MMOXCCHF.js";
|
|
4
4
|
import "./chunk-6P7U3HWG.js";
|
|
5
5
|
import "./chunk-PQM3B54K.js";
|
|
6
6
|
import "./chunk-B3DR2JBZ.js";
|
|
@@ -173,7 +173,7 @@ import "./chunk-MDPT6QFK.js";
|
|
|
173
173
|
import "./chunk-UOBPK2OS.js";
|
|
174
174
|
import "./chunk-2YA5N2BY.js";
|
|
175
175
|
import "./chunk-3HSRWJTC.js";
|
|
176
|
-
import "./chunk-
|
|
176
|
+
import "./chunk-PL36IQPL.js";
|
|
177
177
|
import "./chunk-HUDPXTXW.js";
|
|
178
178
|
import "./chunk-AZEZSQ5R.js";
|
|
179
179
|
import "./chunk-YAZNBMNF.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remnic/core",
|
|
3
|
-
"version": "10.0.
|
|
3
|
+
"version": "10.0.101",
|
|
4
4
|
"description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -3386,7 +3386,7 @@
|
|
|
3386
3386
|
"core"
|
|
3387
3387
|
],
|
|
3388
3388
|
"peerDependencies": {
|
|
3389
|
-
"@remnic/coding-graph": "^10.0.
|
|
3389
|
+
"@remnic/coding-graph": "^10.0.101"
|
|
3390
3390
|
},
|
|
3391
3391
|
"peerDependenciesMeta": {
|
|
3392
3392
|
"@remnic/coding-graph": {
|
|
@@ -6,6 +6,26 @@ import test from "node:test";
|
|
|
6
6
|
import { StorageManager, parseEntityFile } from "./storage.js";
|
|
7
7
|
import { sleep } from "./utils/sleep.js";
|
|
8
8
|
|
|
9
|
+
const deferredStores: StorageManager[] = [];
|
|
10
|
+
|
|
11
|
+
// Opens a store the way the daemon does: the canonical-id migration starts in
|
|
12
|
+
// the background. Registered so teardown can wait for it.
|
|
13
|
+
async function deferStore(dir: string): Promise<StorageManager> {
|
|
14
|
+
const store = new StorageManager(dir);
|
|
15
|
+
deferredStores.push(store);
|
|
16
|
+
await store.ensureDirectories({ waitForEntityMigration: false });
|
|
17
|
+
return store;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// The deferred migration keeps writing under dir until it settles. Release any
|
|
21
|
+
// lock the test holds, wait for every registered migration, then remove dir.
|
|
22
|
+
async function disposeStore(dir: string): Promise<void> {
|
|
23
|
+
await rm(path.join(dir, "state", "entity-canonical-id-migration.lock"), { force: true });
|
|
24
|
+
await Promise.all(deferredStores.splice(0).map((store) => store.drainEntityCanonicalIdMigration()));
|
|
25
|
+
StorageManager.clearAllStaticCaches();
|
|
26
|
+
await rm(dir, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
9
29
|
async function legacyStore() {
|
|
10
30
|
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-lazy-compat-"));
|
|
11
31
|
const seed = new StorageManager(dir);
|
|
@@ -15,8 +35,7 @@ async function legacyStore() {
|
|
|
15
35
|
await rm(journal, { force: true });
|
|
16
36
|
const lock = path.join(dir, "state", "entity-canonical-id-migration.lock");
|
|
17
37
|
await writeFile(lock, `${process.pid} test-holder ${new Date().toISOString()}\n`);
|
|
18
|
-
const storage =
|
|
19
|
-
await storage.ensureDirectories({ waitForEntityMigration: false });
|
|
38
|
+
const storage = await deferStore(dir);
|
|
20
39
|
return { dir, id, journal, lock, storage };
|
|
21
40
|
}
|
|
22
41
|
|
|
@@ -27,8 +46,7 @@ test("canonical reads find a legacy file without a journal or migration", async
|
|
|
27
46
|
await assert.rejects(() => readFile(fixture.journal), { code: "ENOENT" });
|
|
28
47
|
assert.ok(await readFile(fixture.lock));
|
|
29
48
|
} finally {
|
|
30
|
-
|
|
31
|
-
await rm(fixture.dir, { recursive: true, force: true });
|
|
49
|
+
await disposeStore(fixture.dir);
|
|
32
50
|
}
|
|
33
51
|
});
|
|
34
52
|
|
|
@@ -39,8 +57,7 @@ test("ambiguous legacy canonical matches are refused rather than picking a claim
|
|
|
39
57
|
await writeFile(path.join(fixture.dir, "entities", "other-legacy-person.md"), raw);
|
|
40
58
|
await assert.rejects(() => fixture.storage.readEntity(fixture.id), /ambiguous/i);
|
|
41
59
|
} finally {
|
|
42
|
-
|
|
43
|
-
await rm(fixture.dir, { recursive: true, force: true });
|
|
60
|
+
await disposeStore(fixture.dir);
|
|
44
61
|
}
|
|
45
62
|
});
|
|
46
63
|
|
|
@@ -50,8 +67,9 @@ test("completed migration skips corpus reads on repeated misses in a fresh read-
|
|
|
50
67
|
const seed = new StorageManager(dir);
|
|
51
68
|
await seed.writeEntity("Existing Person", "person", ["Synthetic fact."]);
|
|
52
69
|
await seed.ensureDirectories();
|
|
53
|
-
const reader =
|
|
54
|
-
|
|
70
|
+
const reader = await deferStore(dir);
|
|
71
|
+
// Let the startup migration finish so only the readEntity calls below are counted.
|
|
72
|
+
await reader.drainEntityCanonicalIdMigration();
|
|
55
73
|
// Bracket access is TypeScript's typed escape hatch for the private reader.
|
|
56
74
|
const originalRead = reader["readStorageSecureFile"].bind(reader);
|
|
57
75
|
const reads: string[] = [];
|
|
@@ -63,8 +81,7 @@ test("completed migration skips corpus reads on repeated misses in a fresh read-
|
|
|
63
81
|
assert.equal(await reader.readEntity("person-missing"), "");
|
|
64
82
|
assert.deepEqual(reads, ["person-missing.md", "person-missing.md"], "completed-store misses must not scan entity contents");
|
|
65
83
|
} finally {
|
|
66
|
-
|
|
67
|
-
await rm(dir, { recursive: true, force: true });
|
|
84
|
+
await disposeStore(dir);
|
|
68
85
|
}
|
|
69
86
|
});
|
|
70
87
|
|
|
@@ -74,8 +91,7 @@ test("an external entities change invalidates the amortized completion verdict o
|
|
|
74
91
|
const seed = new StorageManager(dir);
|
|
75
92
|
const id = await seed.writeEntity("Café", "project", ["Synthetic legacy fact."]);
|
|
76
93
|
await seed.ensureDirectories();
|
|
77
|
-
const reader =
|
|
78
|
-
await reader.ensureDirectories({ waitForEntityMigration: false });
|
|
94
|
+
const reader = await deferStore(dir);
|
|
79
95
|
const canonical = reader.normalizeEntityName("Café", "project");
|
|
80
96
|
// First miss computes and caches the completion verdict.
|
|
81
97
|
assert.equal(await reader.readEntity("project-missing"), "");
|
|
@@ -84,8 +100,7 @@ test("an external entities change invalidates the amortized completion verdict o
|
|
|
84
100
|
await rename(path.join(dir, "entities", `${id}.md`), path.join(dir, "entities", "legacy-cafe.md"));
|
|
85
101
|
assert.match(await reader.readEntity(canonical), /Synthetic legacy fact/);
|
|
86
102
|
} finally {
|
|
87
|
-
|
|
88
|
-
await rm(dir, { recursive: true, force: true });
|
|
103
|
+
await disposeStore(dir);
|
|
89
104
|
}
|
|
90
105
|
});
|
|
91
106
|
|
|
@@ -99,8 +114,7 @@ test("an external aliases change invalidates the amortized completion verdict on
|
|
|
99
114
|
const journal = await readFile(journalPath, "utf8");
|
|
100
115
|
assert.equal(JSON.parse(journal).complete, true);
|
|
101
116
|
|
|
102
|
-
const reader =
|
|
103
|
-
await reader.ensureDirectories({ waitForEntityMigration: false });
|
|
117
|
+
const reader = await deferStore(dir);
|
|
104
118
|
// Warm amortized completion (and any same-reader completion memo) on a miss.
|
|
105
119
|
assert.equal(await reader.readEntity("project-missing"), "");
|
|
106
120
|
|
|
@@ -121,8 +135,7 @@ test("an external aliases change invalidates the amortized completion verdict on
|
|
|
121
135
|
assert.equal(await readFile(journalPath, "utf8"), journal, "read compatibility must not replay migration");
|
|
122
136
|
assert.ok(await readFile(lock));
|
|
123
137
|
} finally {
|
|
124
|
-
|
|
125
|
-
await rm(dir, { recursive: true, force: true });
|
|
138
|
+
await disposeStore(dir);
|
|
126
139
|
}
|
|
127
140
|
});
|
|
128
141
|
|
|
@@ -146,16 +159,14 @@ for (const change of ["aliases", "raw legacy write"]) {
|
|
|
146
159
|
}
|
|
147
160
|
const lock = path.join(dir, "state", "entity-canonical-id-migration.lock");
|
|
148
161
|
await writeFile(lock, `${process.pid} test-holder ${new Date().toISOString()}\n`);
|
|
149
|
-
const reader =
|
|
150
|
-
await reader.ensureDirectories({ waitForEntityMigration: false });
|
|
162
|
+
const reader = await deferStore(dir);
|
|
151
163
|
const canonical = reader.normalizeEntityName("Café", "project");
|
|
152
164
|
if (change === "aliases") assert.notEqual(canonical, previousId);
|
|
153
165
|
assert.match(await reader.readEntity(canonical), /Synthetic legacy fact/);
|
|
154
166
|
assert.equal(await readFile(journalPath, "utf8"), journal, "read compatibility must not replay migration");
|
|
155
167
|
assert.ok(await readFile(lock));
|
|
156
168
|
} finally {
|
|
157
|
-
|
|
158
|
-
await rm(dir, { recursive: true, force: true });
|
|
169
|
+
await disposeStore(dir);
|
|
159
170
|
}
|
|
160
171
|
});
|
|
161
172
|
}
|
|
@@ -221,8 +232,7 @@ for (const mutation of mutations) {
|
|
|
221
232
|
const journal = JSON.parse(await readFile(fixture.journal, "utf8"));
|
|
222
233
|
assert.equal(journal.complete, true);
|
|
223
234
|
} finally {
|
|
224
|
-
|
|
225
|
-
await rm(fixture.dir, { recursive: true, force: true });
|
|
235
|
+
await disposeStore(fixture.dir);
|
|
226
236
|
}
|
|
227
237
|
});
|
|
228
238
|
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import test from "node:test";
|
|
5
|
+
import test, { type TestContext } from "node:test";
|
|
6
6
|
import { recordAbstractionNode, upsertAbstractionNodes, type AbstractionNode } from "./abstraction-nodes.js";
|
|
7
7
|
import { pruneOrphanCueAnchors, recordCueAnchor, upsertCueAnchors, type CueAnchor } from "./cue-anchors.js";
|
|
8
8
|
import {
|
|
9
9
|
clearHarmonicGraphCache,
|
|
10
10
|
harmonicGraphCacheStats,
|
|
11
|
+
type ProjectedHarmonicGraph,
|
|
11
12
|
readProjectedHarmonicGraph,
|
|
12
13
|
searchHarmonicRetrieval,
|
|
13
14
|
} from "./harmonic-retrieval.js";
|
|
@@ -228,6 +229,61 @@ test("pruneOrphanCueAnchors alone invalidates the cached graph", async () => {
|
|
|
228
229
|
});
|
|
229
230
|
});
|
|
230
231
|
|
|
232
|
+
// Recall waits for the graph build, so a cache miss costs every recall that lands on it. Production: a
|
|
233
|
+
// 2106 node, 9174 anchor graph rebuilt in 5 s standalone, 7 s inside the daemon, and 17 to 32 s when two
|
|
234
|
+
// recalls overlapped and each ran its own build.
|
|
235
|
+
test("concurrent reads on a cold cache share one build", async () => {
|
|
236
|
+
await withDir(async (memoryDir, storage) => {
|
|
237
|
+
const memory = await storage.writeMemory("fact", "theta shared fact", { source: "test" });
|
|
238
|
+
await recordAbstractionNode({ memoryDir, node: node("n1", "theta shared fact", [memory.id]) });
|
|
239
|
+
const before = { ...harmonicGraphCacheStats };
|
|
240
|
+
const [a, b, c] = await Promise.all([read(memoryDir), read(memoryDir), read(memoryDir)]);
|
|
241
|
+
assert.equal(harmonicGraphCacheStats.misses, before.misses + 1, "one build, not one per caller");
|
|
242
|
+
assert.equal(a, b);
|
|
243
|
+
assert.equal(b, c);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("a caller that aborts leaves the shared build for the others", async () => {
|
|
248
|
+
await withDir(async (memoryDir, storage) => {
|
|
249
|
+
const memory = await storage.writeMemory("fact", "iota abort fact", { source: "test" });
|
|
250
|
+
await recordAbstractionNode({ memoryDir, node: node("n1", "iota abort fact", [memory.id]) });
|
|
251
|
+
const controller = new AbortController();
|
|
252
|
+
const aborted = readProjectedHarmonicGraph({ memoryDir, anchorsEnabled: true, useCache: true, abortSignal: controller.signal });
|
|
253
|
+
const other = read(memoryDir);
|
|
254
|
+
controller.abort();
|
|
255
|
+
await assert.rejects(aborted);
|
|
256
|
+
assert.equal((await other).nodes.length, 1);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Unreadable node store: a file where the nodes directory belongs is not ENOENT, so the strict listing throws
|
|
261
|
+
// (a corrupt node file would not: readAbstractionNodes skips those on purpose).
|
|
262
|
+
async function breakNodeStore(memoryDir: string) {
|
|
263
|
+
const nodesDir = path.join(memoryDir, "state", "abstraction-nodes", "nodes");
|
|
264
|
+
const parked = `${nodesDir}.parked`;
|
|
265
|
+
await rename(nodesDir, parked);
|
|
266
|
+
await writeFile(nodesDir, "not a directory");
|
|
267
|
+
return async () => {
|
|
268
|
+
await rm(nodesDir, { force: true });
|
|
269
|
+
await rename(parked, nodesDir);
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
test("callers that joined a failing build all reject, and the next read retries and succeeds", async () => {
|
|
274
|
+
await withDir(async (memoryDir, storage) => {
|
|
275
|
+
const memory = await storage.writeMemory("fact", "mu retry fact", { source: "test" });
|
|
276
|
+
await recordAbstractionNode({ memoryDir, node: node("n1", "mu retry fact", [memory.id]) });
|
|
277
|
+
const restore = await breakNodeStore(memoryDir);
|
|
278
|
+
const missesBefore = harmonicGraphCacheStats.misses;
|
|
279
|
+
const outcomes = await Promise.allSettled([read(memoryDir), read(memoryDir), read(memoryDir)]);
|
|
280
|
+
assert.deepEqual(outcomes.map((outcome) => outcome.status), ["rejected", "rejected", "rejected"]);
|
|
281
|
+
assert.equal(harmonicGraphCacheStats.misses, missesBefore + 1, "the three callers shared one failing build");
|
|
282
|
+
await restore();
|
|
283
|
+
assert.equal((await read(memoryDir)).nodes.length, 1);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
231
287
|
test("an entry older than the TTL is rebuilt", async (t) => {
|
|
232
288
|
await withDir(async (memoryDir, storage) => {
|
|
233
289
|
const memory = await storage.writeMemory("fact", "zeta ttl fact", { source: "test" });
|
|
@@ -242,3 +298,132 @@ test("an entry older than the TTL is rebuilt", async (t) => {
|
|
|
242
298
|
assert.equal((await read(memoryDir)).nodes.length, 0, "rebuilt past the TTL");
|
|
243
299
|
});
|
|
244
300
|
});
|
|
301
|
+
|
|
302
|
+
interface BuildHold {
|
|
303
|
+
release: () => void;
|
|
304
|
+
reached: Promise<void>;
|
|
305
|
+
calls: number;
|
|
306
|
+
signal?: AbortSignal;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Hold the first build inside its source-memory read so a second read can land while it runs. The held call
|
|
311
|
+
* records the build's own abort signal; builds started after it are not held.
|
|
312
|
+
*/
|
|
313
|
+
function holdFirstBuild(t: TestContext): BuildHold {
|
|
314
|
+
const original = StorageManager.prototype.readAllMemories;
|
|
315
|
+
let release = () => {};
|
|
316
|
+
const opened = new Promise<void>((resolve) => {
|
|
317
|
+
release = resolve;
|
|
318
|
+
});
|
|
319
|
+
let reachedHold = () => {};
|
|
320
|
+
const reached = new Promise<void>((resolve) => {
|
|
321
|
+
reachedHold = resolve;
|
|
322
|
+
});
|
|
323
|
+
const hold: BuildHold = { release, reached, calls: 0 };
|
|
324
|
+
t.mock.method(StorageManager.prototype, "readAllMemories", async function (this: StorageManager, ...args: Parameters<typeof original>) {
|
|
325
|
+
hold.calls++;
|
|
326
|
+
if (hold.calls === 1) {
|
|
327
|
+
hold.signal = args[0]?.abortSignal;
|
|
328
|
+
reachedHold();
|
|
329
|
+
await opened;
|
|
330
|
+
}
|
|
331
|
+
return original.apply(this, args);
|
|
332
|
+
});
|
|
333
|
+
return hold;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Fail the test instead of hanging when a promise that must settle does not. */
|
|
337
|
+
function within<T>(promise: Promise<T>, ms: number, what: string): Promise<T> {
|
|
338
|
+
return Promise.race([
|
|
339
|
+
promise,
|
|
340
|
+
new Promise<never>((_resolve, reject) => setTimeout(() => reject(new Error(`${what} did not settle in ${ms} ms`)), ms).unref()),
|
|
341
|
+
]);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
test("the shared build aborts once every joined caller has aborted, and the next read starts a fresh one", async (t) => {
|
|
345
|
+
await withDir(async (memoryDir, storage) => {
|
|
346
|
+
const memory = await storage.writeMemory("fact", "nu abort fact", { source: "test" });
|
|
347
|
+
await recordAbstractionNode({ memoryDir, node: node("n1", "nu abort fact", [memory.id]) });
|
|
348
|
+
const hold = holdFirstBuild(t);
|
|
349
|
+
const first = new AbortController();
|
|
350
|
+
const second = new AbortController();
|
|
351
|
+
const options = { memoryDir, anchorsEnabled: true, useCache: true };
|
|
352
|
+
const a = readProjectedHarmonicGraph({ ...options, abortSignal: first.signal });
|
|
353
|
+
const b = readProjectedHarmonicGraph({ ...options, abortSignal: second.signal });
|
|
354
|
+
await within(hold.reached, 2000, "build start");
|
|
355
|
+
assert.equal(hold.calls, 1, "one build for both callers");
|
|
356
|
+
first.abort();
|
|
357
|
+
await assert.rejects(within(a, 1000, "first caller"), { name: "AbortError" });
|
|
358
|
+
assert.equal(hold.signal?.aborted, false, "one caller is still waiting, so the build keeps running");
|
|
359
|
+
second.abort();
|
|
360
|
+
await assert.rejects(within(b, 1000, "second caller"), { name: "AbortError" });
|
|
361
|
+
assert.equal(hold.signal?.aborted, true, "no caller is left, so the build is cancelled");
|
|
362
|
+
const missesBefore = harmonicGraphCacheStats.misses;
|
|
363
|
+
const next = read(memoryDir);
|
|
364
|
+
assert.equal(harmonicGraphCacheStats.misses, missesBefore + 1, "the next read does not join the cancelled build");
|
|
365
|
+
assert.equal((await within(next, 5000, "next read")).nodes.length, 1);
|
|
366
|
+
hold.release();
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("a read after a stamp change does not join the build that started on the old stamp", async (t) => {
|
|
371
|
+
await withDir(async (memoryDir, storage) => {
|
|
372
|
+
const first = await storage.writeMemory("fact", "xi stamp fact one", { source: "test" });
|
|
373
|
+
await recordAbstractionNode({ memoryDir, node: node("n1", "xi stamp fact one", [first.id]) });
|
|
374
|
+
const hold = holdFirstBuild(t);
|
|
375
|
+
const missesBefore = harmonicGraphCacheStats.misses;
|
|
376
|
+
const stale = read(memoryDir);
|
|
377
|
+
await within(hold.reached, 2000, "old build start");
|
|
378
|
+
const second = await storage.writeMemory("fact", "xi stamp fact two", { source: "test" });
|
|
379
|
+
await recordAbstractionNode({ memoryDir, node: node("n2", "xi stamp fact two", [second.id]) });
|
|
380
|
+
try {
|
|
381
|
+
const fresh = read(memoryDir);
|
|
382
|
+
assert.equal(harmonicGraphCacheStats.misses, missesBefore + 2, "the second read started its own build");
|
|
383
|
+
assert.equal((await within(fresh, 5000, "fresh read")).nodes.length, 2, "and it sees the write");
|
|
384
|
+
} finally {
|
|
385
|
+
hold.release();
|
|
386
|
+
}
|
|
387
|
+
await stale;
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test("a joined caller that aborts rejects at once, while the build it joined is still running", async (t) => {
|
|
392
|
+
await withDir(async (memoryDir, storage) => {
|
|
393
|
+
const memory = await storage.writeMemory("fact", "omicron join fact", { source: "test" });
|
|
394
|
+
await recordAbstractionNode({ memoryDir, node: node("n1", "omicron join fact", [memory.id]) });
|
|
395
|
+
const hold = holdFirstBuild(t);
|
|
396
|
+
const owner = read(memoryDir);
|
|
397
|
+
const controller = new AbortController();
|
|
398
|
+
const joiner = readProjectedHarmonicGraph({ memoryDir, anchorsEnabled: true, useCache: true, abortSignal: controller.signal });
|
|
399
|
+
controller.abort();
|
|
400
|
+
try {
|
|
401
|
+
await assert.rejects(within(joiner, 1000, "aborted joiner"), { name: "AbortError" });
|
|
402
|
+
} finally {
|
|
403
|
+
hold.release();
|
|
404
|
+
}
|
|
405
|
+
assert.equal((await owner).nodes.length, 1, "the owner is not affected");
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test("clearing the cache drops a running build: the next read starts its own and the old build stores nothing", async (t) => {
|
|
410
|
+
await withDir(async (memoryDir, storage) => {
|
|
411
|
+
const memory = await storage.writeMemory("fact", "pi clear fact", { source: "test" });
|
|
412
|
+
await recordAbstractionNode({ memoryDir, node: node("n1", "pi clear fact", [memory.id]) });
|
|
413
|
+
const hold = holdFirstBuild(t);
|
|
414
|
+
const missesBefore = harmonicGraphCacheStats.misses;
|
|
415
|
+
const old = read(memoryDir);
|
|
416
|
+
await within(hold.reached, 2000, "old build start");
|
|
417
|
+
clearHarmonicGraphCache();
|
|
418
|
+
let kept: ProjectedHarmonicGraph;
|
|
419
|
+
try {
|
|
420
|
+
const fresh = read(memoryDir);
|
|
421
|
+
assert.equal(harmonicGraphCacheStats.misses, missesBefore + 2, "the read after the clear did not join the old build");
|
|
422
|
+
kept = await within(fresh, 5000, "fresh read");
|
|
423
|
+
} finally {
|
|
424
|
+
hold.release();
|
|
425
|
+
}
|
|
426
|
+
await old;
|
|
427
|
+
assert.equal(await read(memoryDir), kept, "the finished old build did not overwrite the newer cache entry");
|
|
428
|
+
});
|
|
429
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { realpath } from "node:fs/promises";
|
|
3
3
|
import { listJsonFilesStrict, readJsonFile } from "./json-store.js";
|
|
4
|
-
import { throwIfAborted } from "./abort-error.js";
|
|
4
|
+
import { raceAbort, throwIfAborted } from "./abort-error.js";
|
|
5
5
|
import { readHarmonicGraphStamp } from "./harmonic-graph-version.js";
|
|
6
6
|
import { inferMemoryStatus, toMemoryPathRel } from "./memory-lifecycle-ledger-utils.js";
|
|
7
7
|
import {
|
|
@@ -471,8 +471,20 @@ interface GraphCacheEntry {
|
|
|
471
471
|
}
|
|
472
472
|
|
|
473
473
|
const graphCache = new Map<string, GraphCacheEntry>();
|
|
474
|
-
|
|
475
|
-
|
|
474
|
+
|
|
475
|
+
interface GraphBuild {
|
|
476
|
+
stamp: string;
|
|
477
|
+
promise: Promise<ProjectedHarmonicGraph>;
|
|
478
|
+
/** Cancels the build's source scan; fired when no caller is waiting for it. */
|
|
479
|
+
controller: AbortController;
|
|
480
|
+
/** Callers currently waiting for this build. */
|
|
481
|
+
waiting: number;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** One build per cache key at a time: every recall that lands on a miss waits for the same build. */
|
|
485
|
+
const graphBuilds = new Map<string, GraphBuild>();
|
|
486
|
+
/** Counters for tests and diagnostics. `shared` counts reads that joined a build already running. */
|
|
487
|
+
export const harmonicGraphCacheStats = { hits: 0, misses: 0, shared: 0 };
|
|
476
488
|
|
|
477
489
|
/** Freeze records and their nested arrays/objects so a caller cannot corrupt the shared cache. */
|
|
478
490
|
function deepFreeze<T extends object>(value: T): T {
|
|
@@ -485,6 +497,60 @@ function deepFreeze<T extends object>(value: T): T {
|
|
|
485
497
|
|
|
486
498
|
export function clearHarmonicGraphCache(): void {
|
|
487
499
|
graphCache.clear();
|
|
500
|
+
graphBuilds.clear();
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
type GraphReadOptions = Parameters<typeof buildProjectedHarmonicGraph>[0];
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Start the one build for `key`. The build owns an abort controller instead of using one caller's signal,
|
|
507
|
+
* so a caller that leaves does not cancel it for the others; `joinGraphBuild` cancels it when the last caller leaves.
|
|
508
|
+
*/
|
|
509
|
+
function startGraphBuild(key: string, stamp: string, options: GraphReadOptions): GraphBuild {
|
|
510
|
+
harmonicGraphCacheStats.misses++;
|
|
511
|
+
const startedAtMs = Date.now();
|
|
512
|
+
const controller = new AbortController();
|
|
513
|
+
const promise: Promise<ProjectedHarmonicGraph> = buildProjectedHarmonicGraph({
|
|
514
|
+
memoryDir: options.memoryDir,
|
|
515
|
+
abstractionNodeStoreDir: options.abstractionNodeStoreDir,
|
|
516
|
+
anchorsEnabled: options.anchorsEnabled,
|
|
517
|
+
temporalExpiredInInjection: options.temporalExpiredInInjection,
|
|
518
|
+
abortSignal: controller.signal,
|
|
519
|
+
})
|
|
520
|
+
.then((built) => {
|
|
521
|
+
const graph = { nodes: deepFreeze(built.nodes), anchors: deepFreeze(built.anchors) };
|
|
522
|
+
// A cleared cache or a newer build for this key has replaced this one: do not store a stale graph.
|
|
523
|
+
if (graphBuilds.get(key)?.promise !== promise) return graph;
|
|
524
|
+
graphCache.delete(key);
|
|
525
|
+
graphCache.set(key, { stamp, startedAtMs, graph });
|
|
526
|
+
while (graphCache.size > GRAPH_CACHE_MAX_ENTRIES) {
|
|
527
|
+
const oldest = graphCache.keys().next().value;
|
|
528
|
+
if (oldest === undefined) break;
|
|
529
|
+
graphCache.delete(oldest);
|
|
530
|
+
}
|
|
531
|
+
return graph;
|
|
532
|
+
})
|
|
533
|
+
.finally(() => {
|
|
534
|
+
if (graphBuilds.get(key)?.promise === promise) graphBuilds.delete(key);
|
|
535
|
+
});
|
|
536
|
+
const build: GraphBuild = { stamp, promise, controller, waiting: 0 };
|
|
537
|
+
graphBuilds.set(key, build);
|
|
538
|
+
return build;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Wait for `build` on behalf of one caller. The caller's own signal ends only its own wait. When the last
|
|
543
|
+
* waiting caller has left, the build is cancelled and dropped, so abandoned recalls do not leave full scans running
|
|
544
|
+
* (cancelling a build that already finished does nothing).
|
|
545
|
+
*/
|
|
546
|
+
function joinGraphBuild(key: string, build: GraphBuild, signal: AbortSignal | undefined): Promise<ProjectedHarmonicGraph> {
|
|
547
|
+
build.waiting++;
|
|
548
|
+
return raceAbort(build.promise, signal, "harmonic retrieval aborted").finally(() => {
|
|
549
|
+
build.waiting--;
|
|
550
|
+
if (build.waiting > 0) return;
|
|
551
|
+
if (graphBuilds.get(key) === build) graphBuilds.delete(key);
|
|
552
|
+
build.controller.abort();
|
|
553
|
+
});
|
|
488
554
|
}
|
|
489
555
|
|
|
490
556
|
/**
|
|
@@ -495,9 +561,9 @@ export function clearHarmonicGraphCache(): void {
|
|
|
495
561
|
* cold-write version sentinels (bumped by every memory write, in any process) and
|
|
496
562
|
* the graph version sentinel (bumped by node and anchor writes). A stamp read is
|
|
497
563
|
* 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.
|
|
499
|
-
* cache so tests stay deterministic. The
|
|
500
|
-
* mutate them.
|
|
564
|
+
* cannot see, which is wall-clock validity expiry. Concurrent misses share one
|
|
565
|
+
* build. An explicit `nowMs` bypasses the cache so tests stay deterministic. The
|
|
566
|
+
* cached arrays are frozen: callers must not mutate them.
|
|
501
567
|
*/
|
|
502
568
|
export async function readProjectedHarmonicGraph(options: {
|
|
503
569
|
memoryDir: string;
|
|
@@ -529,20 +595,12 @@ export async function readProjectedHarmonicGraph(options: {
|
|
|
529
595
|
graphCache.set(key, hit);
|
|
530
596
|
return hit.graph;
|
|
531
597
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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);
|
|
598
|
+
const running = graphBuilds.get(key);
|
|
599
|
+
if (running?.stamp === stamp) {
|
|
600
|
+
harmonicGraphCacheStats.shared++;
|
|
601
|
+
return joinGraphBuild(key, running, options.abortSignal);
|
|
544
602
|
}
|
|
545
|
-
return
|
|
603
|
+
return joinGraphBuild(key, startGraphBuild(key, stamp, options), options.abortSignal);
|
|
546
604
|
}
|
|
547
605
|
|
|
548
606
|
export async function searchHarmonicRetrieval(options: {
|
|
@@ -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 { readHarmonicGraphStamp } from \"./harmonic-graph-version.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 */\nasync function buildProjectedHarmonicGraph(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\nconst GRAPH_CACHE_TTL_MS = 30_000;\nconst GRAPH_CACHE_MAX_ENTRIES = 8;\n\ninterface GraphCacheEntry {\n stamp: string;\n startedAtMs: number;\n graph: ProjectedHarmonicGraph;\n}\n\nconst graphCache = new Map<string, GraphCacheEntry>();\n/** Counters for tests and diagnostics. */\nexport const harmonicGraphCacheStats = { hits: 0, misses: 0 };\n\n/** Freeze records and their nested arrays/objects so a caller cannot corrupt the shared cache. */\nfunction deepFreeze<T extends object>(value: T): T {\n // isFrozen is true for primitives and null, and a frozen value is not revisited, so this ends on cycles too.\n if (Object.isFrozen(value)) return value;\n Object.freeze(value);\n for (const child of Object.values(value)) deepFreeze(child);\n return value;\n}\n\nexport function clearHarmonicGraphCache(): void {\n graphCache.clear();\n}\n\n/**\n * `readProjectedHarmonicGraph` with an opt-in in-memory cache. Every recall used\n * to re-read all nodes, anchors and source memories from disk (about 4.7 s on a\n * 2107 node, 9199 anchor, 4105 source graph). With `useCache` a completed graph is\n * reused while the stamp below is unchanged: the memory corpus, memory status and\n * cold-write version sentinels (bumped by every memory write, in any process) and\n * the graph version sentinel (bumped by node and anchor writes). A stamp read is\n * four stats, not a scan of the memories. The TTL bounds the one input a sentinel\n * cannot see, which is wall-clock validity expiry. An explicit `nowMs` bypasses the\n * cache so tests stay deterministic. The cached arrays are frozen: callers must not\n * mutate them.\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 useCache?: boolean;\n}): Promise<ProjectedHarmonicGraph> {\n if (options.useCache !== true || options.nowMs !== undefined) return buildProjectedHarmonicGraph(options);\n // Same cancellation contract as the builder: a cache hit must not mask an aborted signal.\n throwIfAborted(options.abortSignal, \"harmonic retrieval aborted\");\n const nodeStoreDir = resolveAbstractionNodeStoreDir(options.memoryDir, options.abstractionNodeStoreDir);\n const key = JSON.stringify([\n options.memoryDir,\n nodeStoreDir,\n options.anchorsEnabled === true,\n options.temporalExpiredInInjection === true,\n ]);\n // The stamp is read BEFORE the build, so a write that lands mid-build leaves the\n // entry stale and the next call rebuilds.\n const stamp = readHarmonicGraphStamp(options.memoryDir, nodeStoreDir);\n const startedAtMs = Date.now();\n const hit = graphCache.get(key);\n if (hit && hit.stamp === stamp && startedAtMs - hit.startedAtMs < GRAPH_CACHE_TTL_MS) {\n harmonicGraphCacheStats.hits++;\n graphCache.delete(key);\n graphCache.set(key, hit);\n return hit.graph;\n }\n harmonicGraphCacheStats.misses++;\n const built = await buildProjectedHarmonicGraph(options);\n const graph = {\n nodes: deepFreeze(built.nodes),\n anchors: deepFreeze(built.anchors),\n };\n graphCache.delete(key);\n graphCache.set(key, { stamp, startedAtMs, graph });\n while (graphCache.size > GRAPH_CACHE_MAX_ENTRIES) {\n const oldest = graphCache.keys().next().value;\n if (oldest === undefined) break;\n graphCache.delete(oldest);\n }\n return graph;\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 /** Share one projected graph between recalls until a write bumps a version sentinel (see readProjectedHarmonicGraph). */\n useGraphCache?: 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({\n ...options,\n useCache: options.useGraphCache === true,\n });\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":[]}
|
|
File without changes
|
|
File without changes
|