@remnic/core 9.23.2 → 9.23.4
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.d.ts +6 -5
- package/dist/access-admin-ops-surface.js +36 -33
- package/dist/access-boundary.d.ts +6 -5
- package/dist/access-boundary.js +36 -33
- package/dist/access-cli.js +92 -90
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +7 -6
- package/dist/access-http.js +46 -43
- package/dist/access-identity-continuity-surface.d.ts +4 -4
- package/dist/access-identity-continuity-surface.js +36 -33
- package/dist/access-lcm-surface.d.ts +6 -5
- package/dist/access-lcm-surface.js +36 -33
- package/dist/access-mcp.d.ts +6 -5
- package/dist/access-mcp.js +41 -38
- package/dist/access-namespace-preflight.js +36 -33
- package/dist/access-observe-write-surface.d.ts +6 -5
- package/dist/access-observe-write-surface.js +36 -33
- package/dist/access-operations-batch.js +38 -35
- package/dist/access-operations.d.ts +6 -5
- package/dist/access-operations.js +40 -37
- package/dist/access-recall-concurrency.d.ts +6 -5
- package/dist/access-recall-concurrency.js +36 -33
- package/dist/access-recall-response.d.ts +6 -5
- package/dist/access-recall-response.js +36 -33
- package/dist/access-recall-surface.d.ts +6 -5
- package/dist/access-recall-surface.js +36 -33
- package/dist/access-schema.js +4 -4
- package/dist/{access-service-CKEROeFP.d.ts → access-service-Pl-h7x4p.d.ts} +6 -3
- package/dist/access-service.d.ts +8 -7
- package/dist/access-service.js +36 -33
- package/dist/access-surface-catalog.d.ts +6 -5
- package/dist/access-wearables-meetings-surface.d.ts +2 -2
- package/dist/active-recall.js +1 -1
- package/dist/adapters/index.js +7 -7
- package/dist/adapters/registry.js +3 -3
- package/dist/bootstrap.d.ts +4 -4
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +7 -7
- package/dist/buffer.d.ts +1 -1
- package/dist/{capsule-crypto-7FJQINUR.js → capsule-crypto-YO5QJ6L3.js} +2 -2
- package/dist/causal-behavior.js +2 -2
- package/dist/causal-chain.js +2 -2
- package/dist/causal-consolidation.js +10 -10
- package/dist/causal-retrieval.js +2 -2
- package/dist/causal-trajectory-graph.js +2 -2
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-L2DPF4DF.js → chunk-3SHB4YAW.js} +4 -4
- package/dist/{chunk-ATEAL5EI.js → chunk-3UD5ZAUA.js} +2 -2
- package/dist/{chunk-ES5IHQ6M.js → chunk-4E3DWNTJ.js} +2 -2
- package/dist/{chunk-QCKQEBIY.js → chunk-5XMJ2P62.js} +5 -5
- package/dist/{chunk-WZVIB5BK.js → chunk-6MFMTNY4.js} +38 -29
- package/dist/chunk-6MFMTNY4.js.map +1 -0
- package/dist/{chunk-7CPOZH6Y.js → chunk-AJ7QCSIS.js} +2 -2
- package/dist/{chunk-M6LJXJ3J.js → chunk-AT7XL2JJ.js} +3 -3
- package/dist/{chunk-2NLLXCJG.js → chunk-BXLOS5AJ.js} +2 -2
- package/dist/{chunk-NRV4Y3PM.js → chunk-CAGPP5NB.js} +1 -1
- package/dist/{chunk-XGEFGNL5.js → chunk-CNUZ2PN2.js} +13 -9
- package/dist/chunk-CNUZ2PN2.js.map +1 -0
- package/dist/chunk-D5ILARSD.js +60 -0
- package/dist/chunk-D5ILARSD.js.map +1 -0
- package/dist/{chunk-X7Y7WX73.js → chunk-DQEMWVMT.js} +1 -1
- package/dist/{chunk-P2BLHUL2.js → chunk-EHOTE26K.js} +5 -5
- package/dist/{chunk-DLKRXYRL.js → chunk-EXH5AK46.js} +6 -6
- package/dist/{chunk-JNIJBXVK.js → chunk-FPJWQZ35.js} +2 -2
- package/dist/{chunk-XWPFZX6S.js → chunk-GCQIMJFH.js} +97 -26
- package/dist/chunk-GCQIMJFH.js.map +1 -0
- package/dist/{chunk-GD3QOBMB.js → chunk-H2M4YA6K.js} +2 -2
- package/dist/{chunk-3ZW6ZQO6.js → chunk-HVYOZAQ7.js} +6 -6
- package/dist/{chunk-3UOSVTFR.js → chunk-JRQBPN63.js} +8 -8
- package/dist/{chunk-3L5O7SMT.js → chunk-JVOA5VDZ.js} +3 -3
- package/dist/{chunk-WWZ35GWW.js → chunk-K3C6CJOF.js} +2 -2
- package/dist/{chunk-4PQ4KLLK.js → chunk-KYP5FSPG.js} +6 -6
- package/dist/{chunk-227UFSEQ.js → chunk-LEPOP46W.js} +4 -4
- package/dist/{chunk-V2XVMCP6.js → chunk-M2PG7OTI.js} +2 -2
- package/dist/{chunk-HSSIWAZP.js → chunk-NSRTLSBX.js} +114 -98
- package/dist/{chunk-HSSIWAZP.js.map → chunk-NSRTLSBX.js.map} +1 -1
- package/dist/{chunk-MU7ZHSZE.js → chunk-PO5LSKQO.js} +2 -2
- package/dist/{chunk-6TWXQ4E2.js → chunk-POHSSW2T.js} +2 -2
- package/dist/{chunk-AU7Q3LSC.js → chunk-RKNJBZ55.js} +7 -7
- package/dist/{chunk-QE63OHLB.js → chunk-RPOSRICW.js} +5 -5
- package/dist/{chunk-ZN4UZWMT.js → chunk-RSLXMGFP.js} +2 -2
- package/dist/{chunk-7I7AYIUQ.js → chunk-SPWOLGXT.js} +5 -5
- package/dist/{chunk-E3ZMFFQZ.js → chunk-TMX4MBSK.js} +2 -2
- package/dist/{chunk-SVZGA2MK.js → chunk-UZ7HGRHK.js} +2 -2
- package/dist/{chunk-JYIFGGEM.js → chunk-UZGJFIIG.js} +2 -2
- package/dist/{chunk-IPLYGWQF.js → chunk-VDX2J7OX.js} +6 -6
- package/dist/{chunk-CUUY5CC4.js → chunk-W3BKLKG6.js} +2 -2
- package/dist/{chunk-HNZKVLAN.js → chunk-W6ROK2KU.js} +2 -2
- package/dist/chunk-XJTYLU6J.js +261 -0
- package/dist/chunk-XJTYLU6J.js.map +1 -0
- package/dist/{chunk-5FZQ74K5.js → chunk-XPLTZEJU.js} +2 -2
- package/dist/{chunk-MAXVH44E.js → chunk-Z54EM5YZ.js} +7 -7
- package/dist/{chunk-KXGE7L62.js → chunk-ZLDMAGCB.js} +59 -59
- package/dist/{cli-BrwT9YMD.d.ts → cli-DuMB83Mw.d.ts} +3 -3
- package/dist/cli.d.ts +7 -6
- package/dist/cli.js +66 -64
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +7 -7
- package/dist/config.js +1 -1
- package/dist/connectors/codex-materialize-runner.js +7 -7
- package/dist/connectors/index.js +7 -7
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/contradiction/index.d.ts +1 -1
- package/dist/conversation-index/backend.js +2 -2
- package/dist/corpus-watermark.d.ts +227 -0
- package/dist/corpus-watermark.js +106 -0
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-retrieval.js +7 -7
- package/dist/explicit-capture.d.ts +4 -4
- package/dist/extraction.js +1 -1
- package/dist/{graph-edge-decay-Z6ENALU2.js → graph-edge-decay-GF3ZSGSP.js} +4 -4
- package/dist/graph-edge-decay-GF3ZSGSP.js.map +1 -0
- package/dist/graph-snapshot.js +3 -3
- package/dist/graph.js +2 -2
- package/dist/index.d.ts +12 -11
- package/dist/index.js +136 -134
- package/dist/lcm/engine.js +3 -3
- package/dist/lcm/index.js +6 -6
- package/dist/maintenance/memory-governance.js +7 -7
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +1 -1
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -7
- package/dist/maintenance/rebuild-memory-projection.d.ts +1 -1
- package/dist/maintenance/rebuild-memory-projection.js +8 -8
- package/dist/{maintenance-C98njYb8.d.ts → maintenance-DGvDB1xF.d.ts} +1 -1
- package/dist/mcp-memory-inspector-app.d.ts +6 -5
- package/dist/memory-worth-outcomes.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +1 -1
- package/dist/namespaces/migrate.js +14 -14
- package/dist/namespaces/search.js +6 -6
- package/dist/namespaces/storage.d.ts +1 -1
- package/dist/namespaces/storage.js +7 -7
- package/dist/operator-doctor-corpus.d.ts +44 -0
- package/dist/operator-doctor-corpus.js +91 -0
- package/dist/operator-doctor-corpus.js.map +1 -0
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +22 -20
- package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +12 -12
- package/dist/{orchestrator-C9ZVppNh.d.ts → orchestrator-RXHefhAy.d.ts} +3 -3
- package/dist/orchestrator.d.ts +4 -4
- package/dist/orchestrator.js +91 -89
- package/dist/resume-bundles.js +1 -1
- package/dist/search/factory.js +5 -5
- package/dist/search/index.js +9 -9
- package/dist/semantic-consolidation.js +8 -8
- package/dist/semantic-rule-promotion.js +7 -7
- package/dist/semantic-rule-verifier.js +7 -7
- package/dist/{service-DgFWlZ-r.d.ts → service-BrWqlsZ4.d.ts} +1 -1
- package/dist/{storage-XGawwqXE.d.ts → storage-DgNVexXm.d.ts} +24 -1
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +6 -6
- package/dist/temporal-supersession.d.ts +1 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/transfer/autodetect.js +1 -1
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +3 -3
- package/dist/transfer/capsule-import.js +2 -2
- package/dist/verified-recall.js +7 -7
- package/package.json +12 -2
- package/src/access-http.test.ts +4 -2
- package/src/access-service.ts +5 -0
- package/src/corpus-watermark.test.ts +840 -0
- package/src/corpus-watermark.ts +531 -0
- package/src/operator-doctor-corpus.test.ts +171 -0
- package/src/operator-doctor-corpus.ts +95 -0
- package/src/operator-toolkit.ts +3 -3
- package/src/recall/archive-scoring.ts +18 -0
- package/src/storage/memory-read-store.ts +91 -22
- package/src/storage.ts +27 -2
- package/dist/chunk-WZVIB5BK.js.map +0 -1
- package/dist/chunk-XGEFGNL5.js.map +0 -1
- package/dist/chunk-XWPFZX6S.js.map +0 -1
- /package/dist/{capsule-crypto-7FJQINUR.js.map → capsule-crypto-YO5QJ6L3.js.map} +0 -0
- /package/dist/{chunk-L2DPF4DF.js.map → chunk-3SHB4YAW.js.map} +0 -0
- /package/dist/{chunk-ATEAL5EI.js.map → chunk-3UD5ZAUA.js.map} +0 -0
- /package/dist/{chunk-ES5IHQ6M.js.map → chunk-4E3DWNTJ.js.map} +0 -0
- /package/dist/{chunk-QCKQEBIY.js.map → chunk-5XMJ2P62.js.map} +0 -0
- /package/dist/{chunk-7CPOZH6Y.js.map → chunk-AJ7QCSIS.js.map} +0 -0
- /package/dist/{chunk-M6LJXJ3J.js.map → chunk-AT7XL2JJ.js.map} +0 -0
- /package/dist/{chunk-2NLLXCJG.js.map → chunk-BXLOS5AJ.js.map} +0 -0
- /package/dist/{chunk-NRV4Y3PM.js.map → chunk-CAGPP5NB.js.map} +0 -0
- /package/dist/{chunk-X7Y7WX73.js.map → chunk-DQEMWVMT.js.map} +0 -0
- /package/dist/{chunk-P2BLHUL2.js.map → chunk-EHOTE26K.js.map} +0 -0
- /package/dist/{chunk-DLKRXYRL.js.map → chunk-EXH5AK46.js.map} +0 -0
- /package/dist/{chunk-JNIJBXVK.js.map → chunk-FPJWQZ35.js.map} +0 -0
- /package/dist/{chunk-GD3QOBMB.js.map → chunk-H2M4YA6K.js.map} +0 -0
- /package/dist/{chunk-3ZW6ZQO6.js.map → chunk-HVYOZAQ7.js.map} +0 -0
- /package/dist/{chunk-3UOSVTFR.js.map → chunk-JRQBPN63.js.map} +0 -0
- /package/dist/{chunk-3L5O7SMT.js.map → chunk-JVOA5VDZ.js.map} +0 -0
- /package/dist/{chunk-WWZ35GWW.js.map → chunk-K3C6CJOF.js.map} +0 -0
- /package/dist/{chunk-4PQ4KLLK.js.map → chunk-KYP5FSPG.js.map} +0 -0
- /package/dist/{chunk-227UFSEQ.js.map → chunk-LEPOP46W.js.map} +0 -0
- /package/dist/{chunk-V2XVMCP6.js.map → chunk-M2PG7OTI.js.map} +0 -0
- /package/dist/{chunk-MU7ZHSZE.js.map → chunk-PO5LSKQO.js.map} +0 -0
- /package/dist/{chunk-6TWXQ4E2.js.map → chunk-POHSSW2T.js.map} +0 -0
- /package/dist/{chunk-AU7Q3LSC.js.map → chunk-RKNJBZ55.js.map} +0 -0
- /package/dist/{chunk-QE63OHLB.js.map → chunk-RPOSRICW.js.map} +0 -0
- /package/dist/{chunk-ZN4UZWMT.js.map → chunk-RSLXMGFP.js.map} +0 -0
- /package/dist/{chunk-7I7AYIUQ.js.map → chunk-SPWOLGXT.js.map} +0 -0
- /package/dist/{chunk-E3ZMFFQZ.js.map → chunk-TMX4MBSK.js.map} +0 -0
- /package/dist/{chunk-SVZGA2MK.js.map → chunk-UZ7HGRHK.js.map} +0 -0
- /package/dist/{chunk-JYIFGGEM.js.map → chunk-UZGJFIIG.js.map} +0 -0
- /package/dist/{chunk-IPLYGWQF.js.map → chunk-VDX2J7OX.js.map} +0 -0
- /package/dist/{chunk-CUUY5CC4.js.map → chunk-W3BKLKG6.js.map} +0 -0
- /package/dist/{chunk-HNZKVLAN.js.map → chunk-W6ROK2KU.js.map} +0 -0
- /package/dist/{chunk-5FZQ74K5.js.map → chunk-XPLTZEJU.js.map} +0 -0
- /package/dist/{chunk-MAXVH44E.js.map → chunk-Z54EM5YZ.js.map} +0 -0
- /package/dist/{chunk-KXGE7L62.js.map → chunk-ZLDMAGCB.js.map} +0 -0
- /package/dist/{graph-edge-decay-Z6ENALU2.js.map → corpus-watermark.js.map} +0 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
/**
|
|
6
|
+
* Corpus-watermark doctor check (issue #2149).
|
|
7
|
+
*
|
|
8
|
+
* Verifies `summarizeCorpusWatermark` reports per-namespace counts + a short
|
|
9
|
+
* digest prefix and always resolves `ok` (this PR has no peer to compare
|
|
10
|
+
* against, so it never warns), and that `runOperatorDoctor` wires it in.
|
|
11
|
+
*/
|
|
12
|
+
import test from "node:test";
|
|
13
|
+
import { parseConfig } from "./config.js";
|
|
14
|
+
import type { CorpusWatermark } from "./corpus-watermark.js";
|
|
15
|
+
import { summarizeCorpusWatermark } from "./operator-doctor-corpus.js";
|
|
16
|
+
import { type OperatorToolkitOrchestrator, runOperatorDoctor } from "./operator-toolkit.js";
|
|
17
|
+
import type { OperatorDoctorCheck } from "./operator-toolkit.js";
|
|
18
|
+
import { StorageManager } from "./storage.js";
|
|
19
|
+
import type { PluginConfig } from "./types.js";
|
|
20
|
+
|
|
21
|
+
const storageFactory = (dir: string) => new StorageManager(dir);
|
|
22
|
+
|
|
23
|
+
// The doctor stores the watermark array under `details.corpus`; `details` is
|
|
24
|
+
// typed `unknown`, so narrow it once here rather than at each read.
|
|
25
|
+
function corpusOf(check: OperatorDoctorCheck | undefined): CorpusWatermark[] {
|
|
26
|
+
const details = check?.details;
|
|
27
|
+
if (!details || typeof details !== "object" || !("corpus" in details)) return [];
|
|
28
|
+
// `details.corpus` is `unknown` after the `in` guard; the doctor always
|
|
29
|
+
// stores the watermark array here.
|
|
30
|
+
const corpus = details.corpus as CorpusWatermark[];
|
|
31
|
+
return corpus;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function writeMemory(memoryDir: string, rel: string): Promise<void> {
|
|
35
|
+
const full = path.join(memoryDir, rel);
|
|
36
|
+
await mkdir(path.dirname(full), { recursive: true });
|
|
37
|
+
await writeFile(full, `---\nid: ${path.basename(rel, ".md")}\n---\n\nbody\n`, "utf-8");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function makeFixture(): Promise<{
|
|
41
|
+
root: string;
|
|
42
|
+
memoryDir: string;
|
|
43
|
+
config: PluginConfig;
|
|
44
|
+
configPath: string;
|
|
45
|
+
orchestrator: OperatorToolkitOrchestrator;
|
|
46
|
+
}> {
|
|
47
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "engram-corpus-doctor-"));
|
|
48
|
+
const memoryDir = path.join(root, "memory");
|
|
49
|
+
const workspaceDir = path.join(root, "workspace");
|
|
50
|
+
await mkdir(memoryDir, { recursive: true });
|
|
51
|
+
await mkdir(workspaceDir, { recursive: true });
|
|
52
|
+
const rawConfig = {
|
|
53
|
+
openaiApiKey: "sk-test",
|
|
54
|
+
memoryDir,
|
|
55
|
+
workspaceDir,
|
|
56
|
+
qmdEnabled: false,
|
|
57
|
+
transcriptEnabled: false,
|
|
58
|
+
hourlySummariesEnabled: false,
|
|
59
|
+
identityEnabled: false,
|
|
60
|
+
identityContinuityEnabled: false,
|
|
61
|
+
sharedContextEnabled: false,
|
|
62
|
+
captureMode: "implicit",
|
|
63
|
+
namespacesEnabled: false,
|
|
64
|
+
defaultNamespace: "global",
|
|
65
|
+
};
|
|
66
|
+
const config = parseConfig(rawConfig);
|
|
67
|
+
const configPath = path.join(root, "openclaw.json");
|
|
68
|
+
await writeFile(
|
|
69
|
+
configPath,
|
|
70
|
+
JSON.stringify({ plugins: { entries: { "openclaw-remnic": { config: rawConfig } } } }, null, 2),
|
|
71
|
+
"utf-8"
|
|
72
|
+
);
|
|
73
|
+
const orchestrator: OperatorToolkitOrchestrator = {
|
|
74
|
+
config,
|
|
75
|
+
storage: new StorageManager(memoryDir),
|
|
76
|
+
qmd: {
|
|
77
|
+
async probe() {
|
|
78
|
+
return false;
|
|
79
|
+
},
|
|
80
|
+
isAvailable() {
|
|
81
|
+
return false;
|
|
82
|
+
},
|
|
83
|
+
async ensureCollection() {
|
|
84
|
+
return "skipped";
|
|
85
|
+
},
|
|
86
|
+
debugStatus() {
|
|
87
|
+
return "disabled";
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
conversationIndexCoordinator: {
|
|
91
|
+
async getHealth() {
|
|
92
|
+
return {
|
|
93
|
+
enabled: false,
|
|
94
|
+
backend: "qmd" as const,
|
|
95
|
+
status: "disabled" as const,
|
|
96
|
+
chunkDocCount: 0,
|
|
97
|
+
lastUpdateAt: null,
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
async rebuild() {
|
|
101
|
+
return { chunks: 0, skipped: true, reason: "disabled", embedded: false, rebuilt: false };
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
return { root, memoryDir, config, configPath, orchestrator };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
test("summarizeCorpusWatermark: reports an ok check with per-namespace count and a short digest prefix", async () => {
|
|
109
|
+
const { root, memoryDir, config } = await makeFixture();
|
|
110
|
+
try {
|
|
111
|
+
await writeMemory(memoryDir, "facts/2026-03-08/a.md");
|
|
112
|
+
await writeMemory(memoryDir, "facts/2026-03-08/b.md");
|
|
113
|
+
await writeMemory(memoryDir, "procedures/2026-03-09/c.md");
|
|
114
|
+
|
|
115
|
+
const check = await summarizeCorpusWatermark(config, storageFactory);
|
|
116
|
+
assert.equal(check.key, "corpus_watermark");
|
|
117
|
+
assert.equal(check.status, "ok");
|
|
118
|
+
const corpus = corpusOf(check);
|
|
119
|
+
assert.equal(corpus.length, 1);
|
|
120
|
+
assert.equal(corpus[0]?.namespace, "global");
|
|
121
|
+
assert.equal(corpus[0]?.memoryFileCount, 3);
|
|
122
|
+
assert.match(check.summary, /global: 3 files/);
|
|
123
|
+
assert.match(check.summary, /digest=[0-9a-f]{12}/);
|
|
124
|
+
} finally {
|
|
125
|
+
await rm(root, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("summarizeCorpusWatermark: an empty corpus still resolves ok with a zero count", async () => {
|
|
130
|
+
const { root, config } = await makeFixture();
|
|
131
|
+
try {
|
|
132
|
+
const check = await summarizeCorpusWatermark(config, storageFactory);
|
|
133
|
+
assert.equal(check.status, "ok");
|
|
134
|
+
const corpus = corpusOf(check);
|
|
135
|
+
assert.equal(corpus.length, 1);
|
|
136
|
+
assert.equal(corpus[0]?.memoryFileCount, 0);
|
|
137
|
+
} finally {
|
|
138
|
+
await rm(root, { recursive: true, force: true });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("runOperatorDoctor: includes the corpus_watermark check", async () => {
|
|
143
|
+
const { root, memoryDir, configPath, orchestrator } = await makeFixture();
|
|
144
|
+
try {
|
|
145
|
+
await writeMemory(memoryDir, "facts/2026-03-08/a.md");
|
|
146
|
+
const report = await runOperatorDoctor({ configPath, orchestrator });
|
|
147
|
+
const check = report.checks.find((c) => c.key === "corpus_watermark");
|
|
148
|
+
assert.ok(check, "expected a corpus_watermark check");
|
|
149
|
+
assert.equal(check?.status, "ok");
|
|
150
|
+
assert.equal(corpusOf(check)[0]?.memoryFileCount, 1);
|
|
151
|
+
} finally {
|
|
152
|
+
await rm(root, { recursive: true, force: true });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("summarizeCorpusWatermark: warns (not ok) when a namespace scan fails", async () => {
|
|
157
|
+
const { root, config } = await makeFixture();
|
|
158
|
+
try {
|
|
159
|
+
// A storage factory that fails every scan (e.g. an EACCES permission
|
|
160
|
+
// regression). The namespace is omitted, so the check must WARN — not
|
|
161
|
+
// certify ok, and not conflate the failure with an empty deployment.
|
|
162
|
+
const throwingFactory = (_dir: string): never => {
|
|
163
|
+
throw new Error("simulated backend read failure");
|
|
164
|
+
};
|
|
165
|
+
const check = await summarizeCorpusWatermark(config, throwingFactory);
|
|
166
|
+
assert.equal(check.status, "warn", "a scan failure must not be reported as ok");
|
|
167
|
+
assert.match(check.summary, /unavailable|unreadable|churning/i);
|
|
168
|
+
} finally {
|
|
169
|
+
await rm(root, { recursive: true, force: true });
|
|
170
|
+
}
|
|
171
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Corpus-watermark doctor check (issue #2149).
|
|
3
|
+
*
|
|
4
|
+
* Lives beside operator-toolkit.ts (which is at its structural-ratchet ceiling)
|
|
5
|
+
* so the god file only gains the wiring. Reports each namespace's memory-file
|
|
6
|
+
* count, newest write, and a short census-digest prefix, and WARNS when a
|
|
7
|
+
* namespace could not be scanned (unreadable/churning) or enumeration failed —
|
|
8
|
+
* a scan failure omits a namespace, so an unconditional "ok" would certify a
|
|
9
|
+
* partial fleet or conflate an unreadable corpus with an empty deployment
|
|
10
|
+
* (issue #2156 round-7). Peer comparison (count/age divergence thresholds) is a
|
|
11
|
+
* follow-up.
|
|
12
|
+
*
|
|
13
|
+
* Namespaces are resolved through the shared resolveCorpusNamespaceRoots helper
|
|
14
|
+
* (issue #2156 finding C) so the doctor and the /health surface enumerate the
|
|
15
|
+
* SAME tenant set and cannot drift. The caller supplies `storageFactory` (a
|
|
16
|
+
* `dir -> CorpusStorage`) so this module never imports the storage god-module
|
|
17
|
+
* directly (issue #1533 ratchet).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
type CorpusStorage,
|
|
22
|
+
type CorpusWatermark,
|
|
23
|
+
computeCorpusWatermarks,
|
|
24
|
+
resolveCorpusNamespaceRoots,
|
|
25
|
+
} from "./corpus-watermark.js";
|
|
26
|
+
import type { OperatorDoctorCheck } from "./operator-toolkit.js";
|
|
27
|
+
import type { PluginConfig } from "./types.js";
|
|
28
|
+
|
|
29
|
+
const DIGEST_PREFIX_LENGTH = 12;
|
|
30
|
+
|
|
31
|
+
export async function summarizeCorpusWatermark(
|
|
32
|
+
config: PluginConfig,
|
|
33
|
+
storageFactory: (dir: string) => CorpusStorage,
|
|
34
|
+
now?: Date
|
|
35
|
+
): Promise<OperatorDoctorCheck> {
|
|
36
|
+
let resolved: string[] = [];
|
|
37
|
+
let watermarks: CorpusWatermark[] = [];
|
|
38
|
+
let enumerationFailed = false;
|
|
39
|
+
try {
|
|
40
|
+
// The doctor orchestrator has no live namespace catalog, so config-driven
|
|
41
|
+
// enumeration (deduped by root inside the shared helper) is the whole set.
|
|
42
|
+
const rootByNamespace = new Map<string, string>();
|
|
43
|
+
for (const root of await resolveCorpusNamespaceRoots({ config })) {
|
|
44
|
+
rootByNamespace.set(root.namespace, root.rootDir);
|
|
45
|
+
}
|
|
46
|
+
resolved = [...rootByNamespace.keys()];
|
|
47
|
+
watermarks = await computeCorpusWatermarks(
|
|
48
|
+
resolved,
|
|
49
|
+
(namespace) => storageFactory(rootByNamespace.get(namespace) ?? config.memoryDir),
|
|
50
|
+
now
|
|
51
|
+
);
|
|
52
|
+
} catch {
|
|
53
|
+
// Namespace ENUMERATION itself failed (distinct from a per-namespace scan).
|
|
54
|
+
enumerationFailed = true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const computed = new Set(watermarks.map((w) => w.namespace));
|
|
58
|
+
const failed = resolved.filter((namespace) => !computed.has(namespace));
|
|
59
|
+
const lines = watermarks.map(
|
|
60
|
+
(w) =>
|
|
61
|
+
`${w.namespace}: ${w.memoryFileCount} files, newest=${w.newestWriteAt ?? "never"}, ` +
|
|
62
|
+
`digest=${w.digest.slice(0, DIGEST_PREFIX_LENGTH)}`
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (enumerationFailed) {
|
|
66
|
+
return {
|
|
67
|
+
key: "corpus_watermark",
|
|
68
|
+
status: "warn",
|
|
69
|
+
summary: "Could not resolve corpus namespaces (enumeration failed).",
|
|
70
|
+
remediation: "Ensure the memory directory and namespace catalog are readable, then rerun `remnic doctor`.",
|
|
71
|
+
details: { corpus: watermarks, resolved: resolved.length, failed: failed.length },
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (failed.length > 0) {
|
|
75
|
+
return {
|
|
76
|
+
key: "corpus_watermark",
|
|
77
|
+
status: "warn",
|
|
78
|
+
summary:
|
|
79
|
+
`Corpus watermark unavailable for ${failed.length} of ${resolved.length} namespace(s) ` +
|
|
80
|
+
`(unreadable or churning): ${failed.join(", ")}.` +
|
|
81
|
+
(lines.length > 0 ? ` Reporting ${lines.length}: ${lines.join("; ")}` : ""),
|
|
82
|
+
remediation: "Inspect the omitted namespace roots for permission/backend errors, then rerun `remnic doctor`.",
|
|
83
|
+
details: { corpus: watermarks, resolved: resolved.length, failed },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
key: "corpus_watermark",
|
|
88
|
+
status: "ok",
|
|
89
|
+
summary:
|
|
90
|
+
resolved.length === 0
|
|
91
|
+
? "No corpus namespaces resolved yet."
|
|
92
|
+
: `Corpus watermark across ${watermarks.length} namespace(s): ${lines.join("; ")}`,
|
|
93
|
+
details: { corpus: watermarks },
|
|
94
|
+
};
|
|
95
|
+
}
|
package/src/operator-toolkit.ts
CHANGED
|
@@ -28,7 +28,7 @@ import { analyzeGraphHealth, type GraphHealthReport } from "./graph.js";
|
|
|
28
28
|
import { resolveAccessSetupCapabilities,
|
|
29
29
|
resolveCapabilities,
|
|
30
30
|
resolveGraphConstructionCapabilities,
|
|
31
|
-
resolveQmdCapabilities,
|
|
31
|
+
resolveQmdCapabilities, resolveIndexingCapabilities,
|
|
32
32
|
resolveEvalCapabilities, resolvePresentationCapabilities, resolveConsolidationCapabilities } from "./capabilities.js";
|
|
33
33
|
import {
|
|
34
34
|
analyzeSessionIntegrity,
|
|
@@ -55,8 +55,7 @@ import type {
|
|
|
55
55
|
} from "./types.js";
|
|
56
56
|
import { reportBufferSurpriseDistribution } from "./buffer-surprise-report.js";
|
|
57
57
|
import { readJudgeVerdictStats } from "./extraction-judge-telemetry.js";
|
|
58
|
-
import {
|
|
59
|
-
|
|
58
|
+
import { summarizeCorpusWatermark } from "./operator-doctor-corpus.js";
|
|
60
59
|
|
|
61
60
|
const OPENCLAW_REMNIC_PLUGIN_IDS = ["openclaw-remnic", "openclaw-engram"] as const;
|
|
62
61
|
|
|
@@ -1501,6 +1500,7 @@ export async function runOperatorDoctor(options: OperatorDoctorOptions): Promise
|
|
|
1501
1500
|
// Surfaces per-phase: enabled status, cadence, threshold values, and the
|
|
1502
1501
|
// best-available last-run timestamp for each of the three pipeline phases.
|
|
1503
1502
|
checks.push(await summarizeDreamsPhases(config, storage));
|
|
1503
|
+
checks.push(await summarizeCorpusWatermark(config, (dir) => new StorageManager(dir))); // corpus watermark #2149
|
|
1504
1504
|
|
|
1505
1505
|
// Security mitigation status (issue #565).
|
|
1506
1506
|
// Reports whether the cross-namespace budget and anomaly detection
|
|
@@ -250,6 +250,17 @@ class ArchiveScoringWorkerPool {
|
|
|
250
250
|
private idle: Worker[] = [];
|
|
251
251
|
private waiters: Array<{ resolve: (worker: Worker) => void; reject: (err: Error) => void }> = [];
|
|
252
252
|
private terminated = false;
|
|
253
|
+
private busy = 0;
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Workers currently checked out and scoring — NOT callers waiting in line.
|
|
257
|
+
* A caller parked in `acquire()` is queued, not running, so this is the only
|
|
258
|
+
* honest measure of real task overlap: with a size-1 pool it never exceeds 1
|
|
259
|
+
* no matter how many callers are queued behind it.
|
|
260
|
+
*/
|
|
261
|
+
get busyWorkers(): number {
|
|
262
|
+
return this.busy;
|
|
263
|
+
}
|
|
253
264
|
|
|
254
265
|
constructor(size: number = defaultPoolSize()) {
|
|
255
266
|
this.targetSize = Math.max(1, size);
|
|
@@ -265,11 +276,13 @@ class ArchiveScoringWorkerPool {
|
|
|
265
276
|
return [];
|
|
266
277
|
}
|
|
267
278
|
let abandoned = false;
|
|
279
|
+
this.busy += 1;
|
|
268
280
|
try {
|
|
269
281
|
return await this.dispatch(worker, task, abortSignal, () => {
|
|
270
282
|
abandoned = true;
|
|
271
283
|
});
|
|
272
284
|
} finally {
|
|
285
|
+
this.busy -= 1;
|
|
273
286
|
if (abandoned) this.retireWorker(worker);
|
|
274
287
|
else this.release(worker);
|
|
275
288
|
}
|
|
@@ -424,6 +437,11 @@ export class OffThreadArchiveScoring implements ArchiveScoringStrategy {
|
|
|
424
437
|
}
|
|
425
438
|
}
|
|
426
439
|
|
|
440
|
+
/** Workers currently scoring; 0 when no pool has been created yet. */
|
|
441
|
+
get busyWorkers(): number {
|
|
442
|
+
return this.pool?.busyWorkers ?? 0;
|
|
443
|
+
}
|
|
444
|
+
|
|
427
445
|
async score(
|
|
428
446
|
items: ReadonlyArray<ArchiveScoreItem>,
|
|
429
447
|
tokens: ReadonlyArray<string>,
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* listing.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import type { Dirent } from "node:fs";
|
|
12
|
+
import type { Dirent, Stats } from "node:fs";
|
|
13
13
|
import { lstat, readdir, realpath, stat } from "node:fs/promises";
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import { toMemoryPathRel } from "../memory-lifecycle-ledger-utils.js";
|
|
@@ -79,6 +79,20 @@ export interface MemoryReadStoreDeps {
|
|
|
79
79
|
readonly wearablesDir: string;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* A backend READ failure worth propagating from a strict (census) scan: an
|
|
84
|
+
* errno-coded fs error that is NOT `ENOENT`. `ENOENT` (an absent dir) and
|
|
85
|
+
* code-less rejections (symlink-escape containment failures from
|
|
86
|
+
* assertPathInsideRoot) are expected and skipped; `EACCES`/`EIO`/… mean the
|
|
87
|
+
* corpus is unreadable and a divergence census must fail loud rather than
|
|
88
|
+
* silently publish a partial count (issue #2156 round-5). Non-strict (recall)
|
|
89
|
+
* callers ignore this and degrade to a best-effort partial scan.
|
|
90
|
+
*/
|
|
91
|
+
function isPropagatableReadError(err: unknown): boolean {
|
|
92
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
93
|
+
return typeof code === "string" && code !== "ENOENT";
|
|
94
|
+
}
|
|
95
|
+
|
|
82
96
|
export class MemoryReadStore {
|
|
83
97
|
constructor(
|
|
84
98
|
private readonly deps: MemoryReadStoreDeps,
|
|
@@ -184,33 +198,66 @@ export class MemoryReadStore {
|
|
|
184
198
|
return latestArtifacts;
|
|
185
199
|
}
|
|
186
200
|
|
|
187
|
-
|
|
201
|
+
/**
|
|
202
|
+
* Recursively collect `*.md` paths under `startDirs`, hardened against symlink
|
|
203
|
+
* escape: a category dir symlinked outside memoryDir (e.g. decisions/ -> an
|
|
204
|
+
* external dir) must NOT pull out-of-store files into a scan (info leak). Same
|
|
205
|
+
* walker-hardening pattern as document-scanner.ts / cli.ts /
|
|
206
|
+
* consolidation-provenance-check.ts; reuses the shared containment helper.
|
|
207
|
+
* Paths only — no frontmatter parse.
|
|
208
|
+
*/
|
|
209
|
+
private async collectContainedMarkdownPaths(
|
|
210
|
+
startDirs: readonly string[],
|
|
211
|
+
propagateReadErrors = false,
|
|
212
|
+
): Promise<string[]> {
|
|
188
213
|
const filePaths: string[] = [];
|
|
189
214
|
|
|
190
|
-
// Resolve the memory root once for containment checks below.
|
|
191
|
-
// symlinked outside memoryDir (e.g. decisions/ -> an external dir) must NOT
|
|
192
|
-
// pull out-of-store files into the QMD-unavailable recall fallback (info
|
|
193
|
-
// leak). Same walker-hardening pattern as document-scanner.ts / cli.ts /
|
|
194
|
-
// consolidation-provenance-check.ts; reuses the shared containment helper.
|
|
215
|
+
// Resolve the memory root once for the containment checks below.
|
|
195
216
|
let memoryRootReal: string;
|
|
196
217
|
try {
|
|
197
218
|
memoryRootReal = await realpath(this.deps.baseDir);
|
|
198
|
-
} catch {
|
|
219
|
+
} catch (err) {
|
|
220
|
+
// ENOENT: a not-yet-created root ⇒ empty. Any other errno is a backend
|
|
221
|
+
// read failure — propagate for strict census callers (never publish an
|
|
222
|
+
// unreadable corpus as empty); non-strict (recall) callers degrade.
|
|
223
|
+
if (propagateReadErrors && isPropagatableReadError(err)) throw err;
|
|
199
224
|
return filePaths;
|
|
200
225
|
}
|
|
201
226
|
|
|
202
|
-
const collectPaths = async (dir: string) => {
|
|
227
|
+
const collectPaths = async (dir: string): Promise<void> => {
|
|
203
228
|
// Directory-level guard, isolated from per-entry handling: skip symlinked
|
|
204
229
|
// or non-directory category dirs and assert the resolved dir stays inside
|
|
205
230
|
// the memory root before reading. A failure here means the whole subtree
|
|
206
231
|
// does not exist or escaped the store — fail closed by skipping it.
|
|
232
|
+
let dirStat: Stats;
|
|
233
|
+
try {
|
|
234
|
+
dirStat = await lstat(dir);
|
|
235
|
+
} catch (err) {
|
|
236
|
+
// ENOENT (absent category dir) is expected; a backend read error
|
|
237
|
+
// (EACCES/EIO/…) propagates in strict census mode.
|
|
238
|
+
if (propagateReadErrors && isPropagatableReadError(err)) throw err;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (dirStat.isSymbolicLink()) return; // never follow symlinks out of the store (both modes)
|
|
242
|
+
if (!dirStat.isDirectory()) {
|
|
243
|
+
// A category root that EXISTS but is not a directory (e.g. `facts/`
|
|
244
|
+
// replaced by a regular file) is a layout/backend failure: a strict
|
|
245
|
+
// census must not silently publish the remaining categories as healthy.
|
|
246
|
+
// Non-strict (recall) stays best-effort and skips it (issue #2156 round-10).
|
|
247
|
+
if (propagateReadErrors) {
|
|
248
|
+
throw new Error(`corpus scan: expected a directory but found a non-directory at ${dir}`);
|
|
249
|
+
}
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
207
252
|
let entries: Dirent[];
|
|
208
253
|
try {
|
|
209
|
-
const dirStat = await lstat(dir);
|
|
210
|
-
if (dirStat.isSymbolicLink() || !dirStat.isDirectory()) return;
|
|
211
254
|
assertPathInsideRoot(memoryRootReal, await realpath(dir), dir);
|
|
212
255
|
entries = await readdir(dir, { withFileTypes: true });
|
|
213
|
-
} catch {
|
|
256
|
+
} catch (err) {
|
|
257
|
+
// A containment failure carries no errno (skip); a backend read error
|
|
258
|
+
// (EACCES …) propagates in strict census mode so a partial subtree is
|
|
259
|
+
// not silently counted.
|
|
260
|
+
if (propagateReadErrors && isPropagatableReadError(err)) throw err;
|
|
214
261
|
return;
|
|
215
262
|
}
|
|
216
263
|
|
|
@@ -231,8 +278,11 @@ export class MemoryReadStore {
|
|
|
231
278
|
try {
|
|
232
279
|
assertPathInsideRoot(memoryRootReal, await realpath(fullPath), fullPath);
|
|
233
280
|
filePaths.push(fullPath);
|
|
234
|
-
} catch {
|
|
235
|
-
//
|
|
281
|
+
} catch (err) {
|
|
282
|
+
// ENOENT (vanished mid-scan) or a code-less containment/symlink
|
|
283
|
+
// rejection ⇒ skip just this file. A backend read error propagates
|
|
284
|
+
// in strict census mode.
|
|
285
|
+
if (propagateReadErrors && isPropagatableReadError(err)) throw err;
|
|
236
286
|
}
|
|
237
287
|
}
|
|
238
288
|
}
|
|
@@ -243,6 +293,13 @@ export class MemoryReadStore {
|
|
|
243
293
|
}
|
|
244
294
|
};
|
|
245
295
|
|
|
296
|
+
for (const dir of startDirs) {
|
|
297
|
+
await collectPaths(dir);
|
|
298
|
+
}
|
|
299
|
+
return filePaths;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async collectActiveMemoryPaths(options?: { propagateReadErrors?: boolean }): Promise<string[]> {
|
|
246
303
|
// Scan EVERY supported memory category directory, not just the legacy four
|
|
247
304
|
// (facts/procedures/reasoning-traces/corrections). Issue #1497: the QMD
|
|
248
305
|
// filesystem-fallback recall path (orchestrator `recent_scan` ->
|
|
@@ -269,14 +326,26 @@ export class MemoryReadStore {
|
|
|
269
326
|
// those files (they have a `---` frontmatter block) and leak queue items
|
|
270
327
|
// into recall. None of these excluded dirs are in RECALL_FALLBACK_DIRS; the
|
|
271
328
|
// exclusion is asserted by tests in storage-fallback-category-dirs.test.ts.
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
329
|
+
return this.collectContainedMarkdownPaths(
|
|
330
|
+
RECALL_FALLBACK_DIRS.map((dir) => path.join(this.deps.baseDir, dir)),
|
|
331
|
+
options?.propagateReadErrors ?? false,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Cold-tier memory paths for the corpus census (issue #2156 finding D).
|
|
337
|
+
* Demoted memories move to `<baseDir>/cold/...` but stay active and reachable
|
|
338
|
+
* via cold recall, so the divergence census MUST count them — otherwise two
|
|
339
|
+
* replicas whose cold tiers differ show a false "converged" digest. Paths
|
|
340
|
+
* only, same symlink/containment hardening as the hot scan. This is NOT part
|
|
341
|
+
* of the recall fallback corpus (collectActiveMemoryPaths), whose scan is
|
|
342
|
+
* intentionally unchanged.
|
|
343
|
+
*/
|
|
344
|
+
async collectColdMemoryPaths(options?: { propagateReadErrors?: boolean }): Promise<string[]> {
|
|
345
|
+
return this.collectContainedMarkdownPaths(
|
|
346
|
+
[this.deps.resolveTierRootDir("cold")],
|
|
347
|
+
options?.propagateReadErrors ?? false,
|
|
348
|
+
);
|
|
280
349
|
}
|
|
281
350
|
|
|
282
351
|
async readMemoriesWindow(options: {
|
package/src/storage.ts
CHANGED
|
@@ -4643,8 +4643,33 @@ export class StorageManager {
|
|
|
4643
4643
|
return Math.max(1, Math.floor(batchSize));
|
|
4644
4644
|
}
|
|
4645
4645
|
|
|
4646
|
-
|
|
4647
|
-
|
|
4646
|
+
/**
|
|
4647
|
+
* Public cheap active-memory path scan (issue #2149 corpus watermark). Lists
|
|
4648
|
+
* active memory file paths without parsing frontmatter — safe on a 100k+
|
|
4649
|
+
* corpus, unlike readAllMemories().
|
|
4650
|
+
*/
|
|
4651
|
+
async collectActiveMemoryPaths(options?: { propagateReadErrors?: boolean }): Promise<string[]> {
|
|
4652
|
+
return this.memoryReadStore.collectActiveMemoryPaths(options);
|
|
4653
|
+
}
|
|
4654
|
+
|
|
4655
|
+
/**
|
|
4656
|
+
* Public cheap cold-tier path scan (issue #2156 finding D corpus census).
|
|
4657
|
+
* Lists demoted-but-reachable memory paths under `cold/` without parsing
|
|
4658
|
+
* frontmatter, so the divergence watermark counts the cold tier too.
|
|
4659
|
+
*/
|
|
4660
|
+
async collectColdMemoryPaths(options?: { propagateReadErrors?: boolean }): Promise<string[]> {
|
|
4661
|
+
return this.memoryReadStore.collectColdMemoryPaths(options);
|
|
4662
|
+
}
|
|
4663
|
+
|
|
4664
|
+
/**
|
|
4665
|
+
* Combined hot+cold corpus-mutation sentinel (issue #2156). Changes whenever
|
|
4666
|
+
* EITHER tier is written, so a divergence census can bracket its hot/cold scan
|
|
4667
|
+
* and detect a tier migration (write-cold-then-unlink-hot) racing the walkers,
|
|
4668
|
+
* then retry for a consistent snapshot instead of caching a transient
|
|
4669
|
+
* double-count or miss.
|
|
4670
|
+
*/
|
|
4671
|
+
getCorpusScanVersion(): string {
|
|
4672
|
+
return `${this.getMemoryCorpusVersion()}:${this.readColdWriteVersion()}`;
|
|
4648
4673
|
}
|
|
4649
4674
|
|
|
4650
4675
|
private async readParsedMemoriesFromPaths(filePaths: string[], batchSize?: number): Promise<MemoryFile[]> {
|