@remnic/core 10.0.101 → 10.0.103
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-QIPDTAWZ.js → chunk-D77I4MD4.js} +2 -2
- package/dist/{chunk-PL36IQPL.js → chunk-KCRHFW3N.js} +69 -20
- package/dist/chunk-KCRHFW3N.js.map +1 -0
- package/dist/{chunk-MMOXCCHF.js → chunk-T3X4CCHC.js} +2 -2
- package/dist/cli.js +2 -2
- package/dist/explicit-capture.js +2 -2
- package/dist/harmonic-retrieval.d.ts +14 -4
- package/dist/harmonic-retrieval.d.ts.map +1 -1
- package/dist/harmonic-retrieval.js +5 -3
- 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/harmonic-graph-cache.test.ts +172 -7
- package/src/harmonic-retrieval.ts +125 -27
- package/dist/chunk-PL36IQPL.js.map +0 -1
- /package/dist/{chunk-QIPDTAWZ.js.map → chunk-D77I4MD4.js.map} +0 -0
- /package/dist/{chunk-MMOXCCHF.js.map → chunk-T3X4CCHC.js.map} +0 -0
|
@@ -25,13 +25,20 @@ export interface ProjectedHarmonicGraph {
|
|
|
25
25
|
nodes: AbstractionNode[];
|
|
26
26
|
anchors: CueAnchor[];
|
|
27
27
|
}
|
|
28
|
-
/** Counters for tests and diagnostics. `
|
|
28
|
+
/** Counters for tests and diagnostics. `stale` hits also count as `hits`. */
|
|
29
29
|
export declare const harmonicGraphCacheStats: {
|
|
30
30
|
hits: number;
|
|
31
31
|
misses: number;
|
|
32
|
+
stale: number;
|
|
32
33
|
shared: number;
|
|
33
34
|
};
|
|
34
35
|
export declare function clearHarmonicGraphCache(): void;
|
|
36
|
+
/**
|
|
37
|
+
* Resolves once every build running now has settled, and after the failure bookkeeping of a background
|
|
38
|
+
* refresh (that handler was attached when the refresh started, so it runs first). For tests: a time-based
|
|
39
|
+
* wait races a slow build.
|
|
40
|
+
*/
|
|
41
|
+
export declare function settleHarmonicGraphBuilds(): Promise<void>;
|
|
35
42
|
/**
|
|
36
43
|
* `readProjectedHarmonicGraph` with an opt-in in-memory cache. Every recall used
|
|
37
44
|
* to re-read all nodes, anchors and source memories from disk (about 4.7 s on a
|
|
@@ -40,9 +47,12 @@ export declare function clearHarmonicGraphCache(): void;
|
|
|
40
47
|
* cold-write version sentinels (bumped by every memory write, in any process) and
|
|
41
48
|
* the graph version sentinel (bumped by node and anchor writes). A stamp read is
|
|
42
49
|
* four stats, not a scan of the memories. The TTL bounds the one input a sentinel
|
|
43
|
-
* cannot see, which is wall-clock validity expiry
|
|
44
|
-
*
|
|
45
|
-
*
|
|
50
|
+
* cannot see, which is wall-clock validity expiry; past it the stale graph is served
|
|
51
|
+
* while one refresh runs (up to GRAPH_CACHE_MAX_STALE_MS). A changed stamp always
|
|
52
|
+
* waits for a fresh build, because a stale graph could carry governed metadata of a
|
|
53
|
+
* memory that left the active set. Concurrent misses share one build. An explicit
|
|
54
|
+
* `nowMs` bypasses the cache so tests stay deterministic. The cached arrays are
|
|
55
|
+
* frozen: callers must not mutate them.
|
|
46
56
|
*/
|
|
47
57
|
export declare function readProjectedHarmonicGraph(options: {
|
|
48
58
|
memoryDir: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"harmonic-retrieval.d.ts","sourceRoot":"","sources":["../src/harmonic-retrieval.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"harmonic-retrieval.d.ts","sourceRoot":"","sources":["../src/harmonic-retrieval.ts"],"names":[],"mappings":"AAOC,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,wBAAwB,CAAC;AACjC,OAAO,EAA+C,KAAK,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AA+PnH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,aAAa,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,eAAe,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,cAAc,EAAE,qBAAqB,EAAE,CAAC;CACzC;AA+ED,wBAAsB,oBAAoB,CAAC,OAAO,EAAE;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAClC,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAY7B;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAClC,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAavB;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,OAAO,EAAE,SAAS,EAAE,CAAC;CACtB;AAqKD,6EAA6E;AAC7E,eAAO,MAAM,uBAAuB;;;;;CAA8C,CAAC;AAWnF,wBAAgB,uBAAuB,IAAI,IAAI,CAI9C;AAED;;;;GAIG;AACH,wBAAsB,yBAAyB,IAAI,OAAO,CAAC,IAAI,CAAC,CAE/D;AAiDD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,0BAA0B,CAAC,OAAO,EAAE;IACxD,SAAS,EAAE,MAAM,CAAC;IAClB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,cAAc,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAyClC;AAED,wBAAsB,uBAAuB,CAAC,OAAO,EAAE;IACrD,SAAS,EAAE,MAAM,CAAC;IAClB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,yHAAyH;IACzH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,GAAG,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAoFrC"}
|
|
@@ -4,8 +4,9 @@ import {
|
|
|
4
4
|
readAbstractionNodes,
|
|
5
5
|
readCueAnchors,
|
|
6
6
|
readProjectedHarmonicGraph,
|
|
7
|
-
searchHarmonicRetrieval
|
|
8
|
-
|
|
7
|
+
searchHarmonicRetrieval,
|
|
8
|
+
settleHarmonicGraphBuilds
|
|
9
|
+
} from "./chunk-KCRHFW3N.js";
|
|
9
10
|
import "./chunk-HUDPXTXW.js";
|
|
10
11
|
import "./chunk-AZEZSQ5R.js";
|
|
11
12
|
import "./chunk-FL3O34XF.js";
|
|
@@ -132,6 +133,7 @@ export {
|
|
|
132
133
|
readAbstractionNodes,
|
|
133
134
|
readCueAnchors,
|
|
134
135
|
readProjectedHarmonicGraph,
|
|
135
|
-
searchHarmonicRetrieval
|
|
136
|
+
searchHarmonicRetrieval,
|
|
137
|
+
settleHarmonicGraphBuilds
|
|
136
138
|
};
|
|
137
139
|
//# sourceMappingURL=harmonic-retrieval.js.map
|
package/dist/index.js
CHANGED
|
@@ -259,7 +259,7 @@ import {
|
|
|
259
259
|
writeManifest,
|
|
260
260
|
writeMeetingEpisodeMemory,
|
|
261
261
|
writeTimelineState
|
|
262
|
-
} from "./chunk-
|
|
262
|
+
} from "./chunk-T3X4CCHC.js";
|
|
263
263
|
import "./chunk-6P7U3HWG.js";
|
|
264
264
|
import "./chunk-PQM3B54K.js";
|
|
265
265
|
import {
|
|
@@ -717,7 +717,7 @@ import {
|
|
|
717
717
|
import "./chunk-UOBPK2OS.js";
|
|
718
718
|
import "./chunk-2YA5N2BY.js";
|
|
719
719
|
import "./chunk-3HSRWJTC.js";
|
|
720
|
-
import "./chunk-
|
|
720
|
+
import "./chunk-KCRHFW3N.js";
|
|
721
721
|
import "./chunk-HUDPXTXW.js";
|
|
722
722
|
import "./chunk-AZEZSQ5R.js";
|
|
723
723
|
import "./chunk-YAZNBMNF.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BufferDrainRunner
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import "../chunk-
|
|
3
|
+
} from "../chunk-D77I4MD4.js";
|
|
4
|
+
import "../chunk-T3X4CCHC.js";
|
|
5
5
|
import "../chunk-6P7U3HWG.js";
|
|
6
6
|
import "../chunk-PQM3B54K.js";
|
|
7
7
|
import "../chunk-B3DR2JBZ.js";
|
|
@@ -174,7 +174,7 @@ import "../chunk-MDPT6QFK.js";
|
|
|
174
174
|
import "../chunk-UOBPK2OS.js";
|
|
175
175
|
import "../chunk-2YA5N2BY.js";
|
|
176
176
|
import "../chunk-3HSRWJTC.js";
|
|
177
|
-
import "../chunk-
|
|
177
|
+
import "../chunk-KCRHFW3N.js";
|
|
178
178
|
import "../chunk-HUDPXTXW.js";
|
|
179
179
|
import "../chunk-AZEZSQ5R.js";
|
|
180
180
|
import "../chunk-YAZNBMNF.js";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BufferDrainRunner,
|
|
3
3
|
ClaimBufferStorage
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import "../chunk-
|
|
4
|
+
} from "../chunk-D77I4MD4.js";
|
|
5
|
+
import "../chunk-T3X4CCHC.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-KCRHFW3N.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-T3X4CCHC.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-KCRHFW3N.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-T3X4CCHC.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-KCRHFW3N.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.103",
|
|
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.103"
|
|
3390
3390
|
},
|
|
3391
3391
|
"peerDependenciesMeta": {
|
|
3392
3392
|
"@remnic/coding-graph": {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type ProjectedHarmonicGraph,
|
|
12
12
|
readProjectedHarmonicGraph,
|
|
13
13
|
searchHarmonicRetrieval,
|
|
14
|
+
settleHarmonicGraphBuilds,
|
|
14
15
|
} from "./harmonic-retrieval.js";
|
|
15
16
|
import { bumpMemoryCorpusVersionForDir } from "./memory-corpus-version.js";
|
|
16
17
|
import { StorageManager } from "./storage.js";
|
|
@@ -232,6 +233,14 @@ test("pruneOrphanCueAnchors alone invalidates the cached graph", async () => {
|
|
|
232
233
|
// Recall waits for the graph build, so a cache miss costs every recall that lands on it. Production: a
|
|
233
234
|
// 2106 node, 9174 anchor graph rebuilt in 5 s standalone, 7 s inside the daemon, and 17 to 32 s when two
|
|
234
235
|
// recalls overlapped and each ran its own build.
|
|
236
|
+
async function until(condition: () => Promise<boolean>) {
|
|
237
|
+
for (let i = 0; i < 400; i += 1) {
|
|
238
|
+
if (await condition()) return;
|
|
239
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
240
|
+
}
|
|
241
|
+
assert.fail("condition not reached in 2 s");
|
|
242
|
+
}
|
|
243
|
+
|
|
235
244
|
test("concurrent reads on a cold cache share one build", async () => {
|
|
236
245
|
await withDir(async (memoryDir, storage) => {
|
|
237
246
|
const memory = await storage.writeMemory("fact", "theta shared fact", { source: "test" });
|
|
@@ -257,6 +266,93 @@ test("a caller that aborts leaves the shared build for the others", async () =>
|
|
|
257
266
|
});
|
|
258
267
|
});
|
|
259
268
|
|
|
269
|
+
test("a read past the TTL answers from the stale graph and refreshes behind it", async (t) => {
|
|
270
|
+
await withDir(async (memoryDir, storage) => {
|
|
271
|
+
const memory = await storage.writeMemory("fact", "zeta ttl fact", { source: "test" });
|
|
272
|
+
const nodeFile = await recordAbstractionNode({ memoryDir, node: node("n1", "zeta ttl fact", [memory.id]) });
|
|
273
|
+
const start = Date.now();
|
|
274
|
+
const clock = t.mock.method(Date, "now", () => start);
|
|
275
|
+
assert.equal((await read(memoryDir)).nodes.length, 1);
|
|
276
|
+
await rm(nodeFile, { force: true });
|
|
277
|
+
clock.mock.mockImplementation(() => start + 29_000);
|
|
278
|
+
assert.equal((await read(memoryDir)).nodes.length, 1, "still cached inside the TTL");
|
|
279
|
+
const missesBefore = harmonicGraphCacheStats.misses;
|
|
280
|
+
clock.mock.mockImplementation(() => start + 31_000);
|
|
281
|
+
assert.equal((await read(memoryDir)).nodes.length, 1, "past the TTL the caller does not wait for a rebuild");
|
|
282
|
+
assert.equal(harmonicGraphCacheStats.misses, missesBefore + 1, "a refresh was started");
|
|
283
|
+
await until(async () => (await read(memoryDir)).nodes.length === 0);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("a graph past the stale limit is rebuilt before it is served", async (t) => {
|
|
288
|
+
await withDir(async (memoryDir, storage) => {
|
|
289
|
+
const memory = await storage.writeMemory("fact", "kappa limit fact", { source: "test" });
|
|
290
|
+
const nodeFile = await recordAbstractionNode({ memoryDir, node: node("n1", "kappa limit fact", [memory.id]) });
|
|
291
|
+
const start = Date.now();
|
|
292
|
+
const clock = t.mock.method(Date, "now", () => start);
|
|
293
|
+
assert.equal((await read(memoryDir)).nodes.length, 1);
|
|
294
|
+
await rm(nodeFile, { force: true });
|
|
295
|
+
clock.mock.mockImplementation(() => start + 330_000);
|
|
296
|
+
assert.equal((await read(memoryDir)).nodes.length, 0, "the stale window ends 300 s after the TTL, at 330 s");
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("a graph is still served stale one millisecond before the stale window ends", async (t) => {
|
|
301
|
+
await withDir(async (memoryDir, storage) => {
|
|
302
|
+
const memory = await storage.writeMemory("fact", "kappa edge fact", { source: "test" });
|
|
303
|
+
const nodeFile = await recordAbstractionNode({ memoryDir, node: node("n1", "kappa edge fact", [memory.id]) });
|
|
304
|
+
const start = Date.now();
|
|
305
|
+
const clock = t.mock.method(Date, "now", () => start);
|
|
306
|
+
assert.equal((await read(memoryDir)).nodes.length, 1);
|
|
307
|
+
await rm(nodeFile, { force: true });
|
|
308
|
+
clock.mock.mockImplementation(() => start + 329_999);
|
|
309
|
+
assert.equal((await read(memoryDir)).nodes.length, 1, "up to 300 s past the TTL the stale graph is served");
|
|
310
|
+
await settleHarmonicGraphBuilds();
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("an ineligible source's invalid_at does not end the cache entry", async (t) => {
|
|
315
|
+
await withDir(async (memoryDir, storage) => {
|
|
316
|
+
const start = Date.now();
|
|
317
|
+
const clock = t.mock.method(Date, "now", () => start);
|
|
318
|
+
const live = await storage.writeMemory("fact", "pi eligible fact", { source: "test" });
|
|
319
|
+
const gone = await storage.writeMemory("fact", "pi quarantined fact", {
|
|
320
|
+
source: "test",
|
|
321
|
+
invalidAt: new Date(start + 10_000).toISOString(),
|
|
322
|
+
});
|
|
323
|
+
const gonePath = (await storage.getMemoryById(gone.id))!.path;
|
|
324
|
+
await writeFile(gonePath, (await readFile(gonePath, "utf8")).replace("category: fact", "category: fact\nstatus: quarantined"));
|
|
325
|
+
bumpMemoryCorpusVersionForDir(memoryDir); // the out-of-band status edit needs the documented bump, as in the status test above
|
|
326
|
+
await recordAbstractionNode({ memoryDir, node: node("n1", "pi mixed fact", [live.id, gone.id]) });
|
|
327
|
+
assert.equal((await read(memoryDir)).nodes.length, 1);
|
|
328
|
+
const before = { ...harmonicGraphCacheStats };
|
|
329
|
+
clock.mock.mockImplementation(() => start + 20_000);
|
|
330
|
+
assert.equal((await read(memoryDir)).nodes.length, 1);
|
|
331
|
+
assert.equal(harmonicGraphCacheStats.misses, before.misses, "the quarantined source's invalid_at passing starts no rebuild");
|
|
332
|
+
assert.equal(harmonicGraphCacheStats.hits, before.hits + 1);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("a private support passport source's invalid_at does not end the cache entry", async (t) => {
|
|
337
|
+
await withDir(async (memoryDir, storage) => {
|
|
338
|
+
const start = Date.now();
|
|
339
|
+
const clock = t.mock.method(Date, "now", () => start);
|
|
340
|
+
const live = await storage.writeMemory("fact", "rho eligible fact", { source: "test" });
|
|
341
|
+
const card = await storage.writeMemory("fact", "rho passport card fact", {
|
|
342
|
+
source: "test",
|
|
343
|
+
tags: ["support-passport-card"],
|
|
344
|
+
invalidAt: new Date(start + 10_000).toISOString(),
|
|
345
|
+
});
|
|
346
|
+
await recordAbstractionNode({ memoryDir, node: node("n1", "rho mixed fact", [live.id, card.id]) });
|
|
347
|
+
assert.equal((await read(memoryDir)).nodes.length, 1);
|
|
348
|
+
const before = { ...harmonicGraphCacheStats };
|
|
349
|
+
clock.mock.mockImplementation(() => start + 20_000);
|
|
350
|
+
assert.equal((await read(memoryDir)).nodes.length, 1);
|
|
351
|
+
assert.equal(harmonicGraphCacheStats.misses, before.misses, "the passport source's invalid_at passing starts no rebuild");
|
|
352
|
+
assert.equal(harmonicGraphCacheStats.hits, before.hits + 1);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
260
356
|
// Unreadable node store: a file where the nodes directory belongs is not ENOENT, so the strict listing throws
|
|
261
357
|
// (a corrupt node file would not: readAbstractionNodes skips those on purpose).
|
|
262
358
|
async function breakNodeStore(memoryDir: string) {
|
|
@@ -270,6 +366,24 @@ async function breakNodeStore(memoryDir: string) {
|
|
|
270
366
|
};
|
|
271
367
|
}
|
|
272
368
|
|
|
369
|
+
test("a source whose invalid_at passes inside the stale window is never served", async (t) => {
|
|
370
|
+
await withDir(async (memoryDir, storage) => {
|
|
371
|
+
const start = Date.now();
|
|
372
|
+
const clock = t.mock.method(Date, "now", () => start);
|
|
373
|
+
const memory = await storage.writeMemory("fact", "lambda expiry fact", {
|
|
374
|
+
source: "test",
|
|
375
|
+
invalidAt: new Date(start + 60_000).toISOString(),
|
|
376
|
+
});
|
|
377
|
+
await recordAbstractionNode({ memoryDir, node: node("n1", "lambda expiry fact", [memory.id]) });
|
|
378
|
+
assert.equal((await read(memoryDir)).nodes.length, 1);
|
|
379
|
+
clock.mock.mockImplementation(() => start + 31_000);
|
|
380
|
+
assert.equal((await read(memoryDir)).nodes.length, 1, "past the TTL but before the invalid_at: stale is fine");
|
|
381
|
+
await until(async () => harmonicGraphCacheStats.stale >= 1);
|
|
382
|
+
clock.mock.mockImplementation(() => start + 61_000);
|
|
383
|
+
assert.equal((await read(memoryDir)).nodes.length, 0, "after the invalid_at the graph is rebuilt before it is served");
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
273
387
|
test("callers that joined a failing build all reject, and the next read retries and succeeds", async () => {
|
|
274
388
|
await withDir(async (memoryDir, storage) => {
|
|
275
389
|
const memory = await storage.writeMemory("fact", "mu retry fact", { source: "test" });
|
|
@@ -284,18 +398,69 @@ test("callers that joined a failing build all reject, and the next read retries
|
|
|
284
398
|
});
|
|
285
399
|
});
|
|
286
400
|
|
|
287
|
-
test("
|
|
401
|
+
test("a failing refresh is retried at most once per TTL, not on every stale read", async (t) => {
|
|
288
402
|
await withDir(async (memoryDir, storage) => {
|
|
289
|
-
const memory = await storage.writeMemory("fact", "
|
|
290
|
-
|
|
403
|
+
const memory = await storage.writeMemory("fact", "nu backoff fact", { source: "test" });
|
|
404
|
+
await recordAbstractionNode({ memoryDir, node: node("n1", "nu backoff fact", [memory.id]) });
|
|
291
405
|
const start = Date.now();
|
|
292
406
|
const clock = t.mock.method(Date, "now", () => start);
|
|
293
407
|
assert.equal((await read(memoryDir)).nodes.length, 1);
|
|
294
|
-
|
|
295
|
-
clock.mock.mockImplementation(() => start + 29_000);
|
|
296
|
-
assert.equal((await read(memoryDir)).nodes.length, 1, "still cached inside the TTL");
|
|
408
|
+
const restore = await breakNodeStore(memoryDir);
|
|
297
409
|
clock.mock.mockImplementation(() => start + 31_000);
|
|
298
|
-
|
|
410
|
+
const missesBefore = harmonicGraphCacheStats.misses;
|
|
411
|
+
assert.equal((await read(memoryDir)).nodes.length, 1, "the stale graph is still served");
|
|
412
|
+
// The failing build bumps `misses` before it reads the broken store: wait for it to settle, not for a time.
|
|
413
|
+
await settleHarmonicGraphBuilds();
|
|
414
|
+
assert.equal(harmonicGraphCacheStats.misses, missesBefore + 1, "one refresh was started");
|
|
415
|
+
clock.mock.mockImplementation(() => start + 40_000);
|
|
416
|
+
for (let i = 0; i < 5; i += 1) await read(memoryDir);
|
|
417
|
+
await settleHarmonicGraphBuilds();
|
|
418
|
+
assert.equal(harmonicGraphCacheStats.misses, missesBefore + 1, "no new build inside the backoff");
|
|
419
|
+
await restore();
|
|
420
|
+
clock.mock.mockImplementation(() => start + 62_000);
|
|
421
|
+
await read(memoryDir);
|
|
422
|
+
await until(async () => harmonicGraphCacheStats.misses === missesBefore + 2);
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// The build's own clock reading is taken before it reads the anchors, so an invalid_at can pass while a
|
|
427
|
+
// build is still running. Every caller that joined that build must still get an unexpired graph.
|
|
428
|
+
test("a source that expires while the build is running is not served from that build", async () => {
|
|
429
|
+
await withDir(async (memoryDir, storage) => {
|
|
430
|
+
const sources: Array<{ id: string; path: string }> = [];
|
|
431
|
+
for (let i = 0; i < 120; i += 1) {
|
|
432
|
+
const memory = await storage.writeMemory("fact", `xi race fact ${i}`, { source: "test" });
|
|
433
|
+
await recordAbstractionNode({ memoryDir, node: node(`n${i}`, `xi race fact ${i}`, [memory.id]) });
|
|
434
|
+
sources.push({ id: memory.id, path: (await storage.getMemoryById(memory.id))!.path });
|
|
435
|
+
}
|
|
436
|
+
// Anchors are read after the sources are projected, so many of them make the tail the deadline falls in.
|
|
437
|
+
await upsertCueAnchors({
|
|
438
|
+
memoryDir,
|
|
439
|
+
anchors: Array.from({ length: 1500 }, (_, i) => anchor(`xi-a${i}`, `n${i % 120}`, sources[i % 120]!.id)),
|
|
440
|
+
});
|
|
441
|
+
// Calibrate: how long one cold build takes on this machine.
|
|
442
|
+
const calibrationStart = Date.now();
|
|
443
|
+
await read(memoryDir);
|
|
444
|
+
const buildMs = Date.now() - calibrationStart;
|
|
445
|
+
clearHarmonicGraphCache();
|
|
446
|
+
// n0's source expires near the end of the next build, which started before the deadline.
|
|
447
|
+
const first = sources[0]!;
|
|
448
|
+
const deadlineMs = Date.now() + Math.max(2, Math.floor(buildMs * 0.85));
|
|
449
|
+
await writeFile(
|
|
450
|
+
first.path,
|
|
451
|
+
(await readFile(first.path, "utf8")).replace("category: fact", `category: fact\ninvalidAt: ${new Date(deadlineMs).toISOString()}`),
|
|
452
|
+
);
|
|
453
|
+
// The storage layer caches parsed memories by corpus version, so an out-of-band edit needs the documented bump.
|
|
454
|
+
bumpMemoryCorpusVersionForDir(memoryDir);
|
|
455
|
+
const graph = await read(memoryDir);
|
|
456
|
+
const returnedAtMs = Date.now();
|
|
457
|
+
const served = graph.nodes.some((candidate) => candidate.nodeId === "n0");
|
|
458
|
+
// A build that finished before the deadline may legitimately include n0 (a 25 ms margin covers the
|
|
459
|
+
// hand-off); one that returned after it must not.
|
|
460
|
+
if (returnedAtMs >= deadlineMs + 25) {
|
|
461
|
+
assert.equal(served, false, `expired source served; build took about ${buildMs} ms`);
|
|
462
|
+
assert.equal(graph.nodes.length, 119);
|
|
463
|
+
}
|
|
299
464
|
});
|
|
300
465
|
});
|
|
301
466
|
|