@remnic/core 10.0.100 → 10.0.102
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/entity-lazy-compatibility.test.ts +34 -24
- package/src/harmonic-graph-cache.test.ts +152 -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.102",
|
|
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.102"
|
|
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
|
}
|
|
@@ -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,73 @@ 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
|
+
|
|
260
336
|
// Unreadable node store: a file where the nodes directory belongs is not ENOENT, so the strict listing throws
|
|
261
337
|
// (a corrupt node file would not: readAbstractionNodes skips those on purpose).
|
|
262
338
|
async function breakNodeStore(memoryDir: string) {
|
|
@@ -270,6 +346,24 @@ async function breakNodeStore(memoryDir: string) {
|
|
|
270
346
|
};
|
|
271
347
|
}
|
|
272
348
|
|
|
349
|
+
test("a source whose invalid_at passes inside the stale window is never served", async (t) => {
|
|
350
|
+
await withDir(async (memoryDir, storage) => {
|
|
351
|
+
const start = Date.now();
|
|
352
|
+
const clock = t.mock.method(Date, "now", () => start);
|
|
353
|
+
const memory = await storage.writeMemory("fact", "lambda expiry fact", {
|
|
354
|
+
source: "test",
|
|
355
|
+
invalidAt: new Date(start + 60_000).toISOString(),
|
|
356
|
+
});
|
|
357
|
+
await recordAbstractionNode({ memoryDir, node: node("n1", "lambda expiry fact", [memory.id]) });
|
|
358
|
+
assert.equal((await read(memoryDir)).nodes.length, 1);
|
|
359
|
+
clock.mock.mockImplementation(() => start + 31_000);
|
|
360
|
+
assert.equal((await read(memoryDir)).nodes.length, 1, "past the TTL but before the invalid_at: stale is fine");
|
|
361
|
+
await until(async () => harmonicGraphCacheStats.stale >= 1);
|
|
362
|
+
clock.mock.mockImplementation(() => start + 61_000);
|
|
363
|
+
assert.equal((await read(memoryDir)).nodes.length, 0, "after the invalid_at the graph is rebuilt before it is served");
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
273
367
|
test("callers that joined a failing build all reject, and the next read retries and succeeds", async () => {
|
|
274
368
|
await withDir(async (memoryDir, storage) => {
|
|
275
369
|
const memory = await storage.writeMemory("fact", "mu retry fact", { source: "test" });
|
|
@@ -284,18 +378,69 @@ test("callers that joined a failing build all reject, and the next read retries
|
|
|
284
378
|
});
|
|
285
379
|
});
|
|
286
380
|
|
|
287
|
-
test("
|
|
381
|
+
test("a failing refresh is retried at most once per TTL, not on every stale read", async (t) => {
|
|
288
382
|
await withDir(async (memoryDir, storage) => {
|
|
289
|
-
const memory = await storage.writeMemory("fact", "
|
|
290
|
-
|
|
383
|
+
const memory = await storage.writeMemory("fact", "nu backoff fact", { source: "test" });
|
|
384
|
+
await recordAbstractionNode({ memoryDir, node: node("n1", "nu backoff fact", [memory.id]) });
|
|
291
385
|
const start = Date.now();
|
|
292
386
|
const clock = t.mock.method(Date, "now", () => start);
|
|
293
387
|
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");
|
|
388
|
+
const restore = await breakNodeStore(memoryDir);
|
|
297
389
|
clock.mock.mockImplementation(() => start + 31_000);
|
|
298
|
-
|
|
390
|
+
const missesBefore = harmonicGraphCacheStats.misses;
|
|
391
|
+
assert.equal((await read(memoryDir)).nodes.length, 1, "the stale graph is still served");
|
|
392
|
+
// The failing build bumps `misses` before it reads the broken store: wait for it to settle, not for a time.
|
|
393
|
+
await settleHarmonicGraphBuilds();
|
|
394
|
+
assert.equal(harmonicGraphCacheStats.misses, missesBefore + 1, "one refresh was started");
|
|
395
|
+
clock.mock.mockImplementation(() => start + 40_000);
|
|
396
|
+
for (let i = 0; i < 5; i += 1) await read(memoryDir);
|
|
397
|
+
await settleHarmonicGraphBuilds();
|
|
398
|
+
assert.equal(harmonicGraphCacheStats.misses, missesBefore + 1, "no new build inside the backoff");
|
|
399
|
+
await restore();
|
|
400
|
+
clock.mock.mockImplementation(() => start + 62_000);
|
|
401
|
+
await read(memoryDir);
|
|
402
|
+
await until(async () => harmonicGraphCacheStats.misses === missesBefore + 2);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// The build's own clock reading is taken before it reads the anchors, so an invalid_at can pass while a
|
|
407
|
+
// build is still running. Every caller that joined that build must still get an unexpired graph.
|
|
408
|
+
test("a source that expires while the build is running is not served from that build", async () => {
|
|
409
|
+
await withDir(async (memoryDir, storage) => {
|
|
410
|
+
const sources: Array<{ id: string; path: string }> = [];
|
|
411
|
+
for (let i = 0; i < 120; i += 1) {
|
|
412
|
+
const memory = await storage.writeMemory("fact", `xi race fact ${i}`, { source: "test" });
|
|
413
|
+
await recordAbstractionNode({ memoryDir, node: node(`n${i}`, `xi race fact ${i}`, [memory.id]) });
|
|
414
|
+
sources.push({ id: memory.id, path: (await storage.getMemoryById(memory.id))!.path });
|
|
415
|
+
}
|
|
416
|
+
// Anchors are read after the sources are projected, so many of them make the tail the deadline falls in.
|
|
417
|
+
await upsertCueAnchors({
|
|
418
|
+
memoryDir,
|
|
419
|
+
anchors: Array.from({ length: 1500 }, (_, i) => anchor(`xi-a${i}`, `n${i % 120}`, sources[i % 120]!.id)),
|
|
420
|
+
});
|
|
421
|
+
// Calibrate: how long one cold build takes on this machine.
|
|
422
|
+
const calibrationStart = Date.now();
|
|
423
|
+
await read(memoryDir);
|
|
424
|
+
const buildMs = Date.now() - calibrationStart;
|
|
425
|
+
clearHarmonicGraphCache();
|
|
426
|
+
// n0's source expires near the end of the next build, which started before the deadline.
|
|
427
|
+
const first = sources[0]!;
|
|
428
|
+
const deadlineMs = Date.now() + Math.max(2, Math.floor(buildMs * 0.85));
|
|
429
|
+
await writeFile(
|
|
430
|
+
first.path,
|
|
431
|
+
(await readFile(first.path, "utf8")).replace("category: fact", `category: fact\ninvalidAt: ${new Date(deadlineMs).toISOString()}`),
|
|
432
|
+
);
|
|
433
|
+
// The storage layer caches parsed memories by corpus version, so an out-of-band edit needs the documented bump.
|
|
434
|
+
bumpMemoryCorpusVersionForDir(memoryDir);
|
|
435
|
+
const graph = await read(memoryDir);
|
|
436
|
+
const returnedAtMs = Date.now();
|
|
437
|
+
const served = graph.nodes.some((candidate) => candidate.nodeId === "n0");
|
|
438
|
+
// A build that finished before the deadline may legitimately include n0 (a 25 ms margin covers the
|
|
439
|
+
// hand-off); one that returned after it must not.
|
|
440
|
+
if (returnedAtMs >= deadlineMs + 25) {
|
|
441
|
+
assert.equal(served, false, `expired source served; build took about ${buildMs} ms`);
|
|
442
|
+
assert.equal(graph.nodes.length, 119);
|
|
443
|
+
}
|
|
299
444
|
});
|
|
300
445
|
});
|
|
301
446
|
|