@remnic/core 9.69.52 → 9.69.53
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 +1 -1
- package/dist/access-admin-ops-surface.js +4 -4
- package/dist/access-authorization-probe.d.ts +1 -1
- package/dist/access-authorization-probe.js +5 -5
- package/dist/access-boundary.d.ts +1 -1
- package/dist/access-boundary.js +4 -4
- package/dist/access-cli.js +20 -20
- package/dist/access-extraction-force-flush.d.ts +1 -1
- package/dist/access-extraction-force-flush.js +4 -4
- package/dist/access-http-briefing.d.ts +1 -1
- package/dist/access-http-briefing.js +5 -5
- package/dist/access-http-lcm-compaction.d.ts +1 -1
- package/dist/access-http-lifecycle-flush.d.ts +1 -1
- package/dist/access-http-offline-stream.d.ts +1 -1
- package/dist/access-http-query.js +4 -4
- package/dist/access-http.d.ts +2 -2
- package/dist/access-http.js +16 -16
- package/dist/access-identity-continuity-surface.js +4 -4
- package/dist/access-lcm-surface.d.ts +1 -1
- package/dist/access-lcm-surface.js +4 -4
- package/dist/access-mcp-shared-context-tools.d.ts +1 -1
- package/dist/access-mcp.d.ts +1 -1
- package/dist/access-mcp.js +11 -11
- package/dist/access-namespace-preflight.js +4 -4
- package/dist/access-observe-write-surface.d.ts +1 -1
- package/dist/access-observe-write-surface.js +4 -4
- package/dist/access-operations-batch.js +5 -5
- package/dist/access-operations.d.ts +1 -1
- package/dist/access-operations.js +10 -10
- package/dist/access-recall-concurrency.d.ts +1 -1
- package/dist/access-recall-concurrency.js +4 -4
- package/dist/access-recall-response.d.ts +1 -1
- package/dist/access-recall-response.js +4 -4
- package/dist/access-recall-surface.d.ts +1 -1
- package/dist/access-recall-surface.js +4 -4
- package/dist/access-schema.d.ts +113 -113
- package/dist/{access-service-CUNM5RfA.d.ts → access-service-H-qEdnSs.d.ts} +64 -64
- package/dist/access-service-helpers.d.ts +1 -1
- package/dist/access-service.d.ts +1 -1
- package/dist/access-service.js +4 -4
- package/dist/access-surface-catalog.d.ts +1 -1
- package/dist/access-surface-catalog.js +6 -6
- package/dist/{chunk-TEQQQTQS.js → chunk-2HJKAVAS.js} +2 -2
- package/dist/{chunk-HWT6KRGS.js → chunk-6UEM5EOB.js} +2 -2
- package/dist/{chunk-UCY4EHAG.js → chunk-A4TQFNRX.js} +3 -3
- package/dist/{chunk-72N2P44O.js → chunk-ARRKSBQU.js} +2 -2
- package/dist/{chunk-WBW34YCJ.js → chunk-BCXZ4BDS.js} +2 -2
- package/dist/{chunk-V5BU5PVE.js → chunk-CO7KBRPF.js} +2 -2
- package/dist/{chunk-HG42J2EX.js → chunk-EQNZC3BH.js} +2 -2
- package/dist/{chunk-35EH6KW4.js → chunk-EYK3UHAP.js} +2 -2
- package/dist/{chunk-DCRK7I5E.js → chunk-HUQMVI3A.js} +93 -4
- package/dist/chunk-HUQMVI3A.js.map +1 -0
- package/dist/{chunk-MSTKJI5T.js → chunk-K7FXR75S.js} +442 -318
- package/dist/chunk-K7FXR75S.js.map +1 -0
- package/dist/{chunk-U6CMLAP4.js → chunk-M7HFDWOS.js} +2 -2
- package/dist/{chunk-2UDAABV6.js → chunk-MTJUIIM4.js} +2 -2
- package/dist/{chunk-GRX6YH4E.js → chunk-MXTCV3B5.js} +2 -2
- package/dist/{chunk-6KCQNFDQ.js → chunk-SOOWBRMH.js} +8 -8
- package/dist/{chunk-SOFATSHJ.js → chunk-TS66MUUJ.js} +5 -5
- package/dist/{chunk-72AV2H3R.js → chunk-U3NBO4IX.js} +39 -3
- package/dist/chunk-U3NBO4IX.js.map +1 -0
- package/dist/{chunk-EW3Y73MN.js → chunk-UJBIWY5M.js} +2 -2
- package/dist/{chunk-SVUUOLKM.js → chunk-VMKAA5SU.js} +2 -2
- package/dist/{chunk-UQTXER7M.js → chunk-ZADYEZY4.js} +2 -2
- package/dist/{chunk-2INZSV27.js → chunk-ZANEMAEV.js} +2 -2
- package/dist/{cli-CCc34xV7.d.ts → cli-VGDSn2zY.d.ts} +1 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +17 -17
- package/dist/deep-recall-http-glue.d.ts +1 -1
- package/dist/deep-recall-mcp-tools.d.ts +1 -1
- package/dist/directory-sidecars.d.ts +30 -7
- package/dist/directory-sidecars.js +5 -1
- package/dist/external-wiki-access.d.ts +1 -1
- package/dist/external-wiki-access.js +5 -5
- package/dist/external-wiki-mcp-tools.d.ts +1 -1
- package/dist/index.d.ts +680 -680
- package/dist/index.js +20 -20
- package/dist/mcp-memory-inspector-app.d.ts +1 -1
- package/dist/memory-browse-http-glue.d.ts +1 -1
- package/dist/memory-browse-mcp-tools.d.ts +1 -1
- package/dist/memory-browse-operations.js +5 -5
- package/dist/memory-browse.js +3 -3
- package/dist/memory-subject-cli.d.ts +1 -1
- package/dist/memory-subject.d.ts +1 -1
- package/dist/orchestrator.js +20 -20
- package/dist/{public-http-UWVcqMcb.d.ts → public-http-P0cnHzYB.d.ts} +1 -1
- package/dist/recall-navigation-http-glue.d.ts +1 -1
- package/dist/recall-navigation-mcp-tools.d.ts +1 -1
- package/dist/recall-navigation-operations.js +5 -5
- package/dist/recall-navigation.js +4 -4
- package/dist/recall-state-view-admission.js +3 -3
- package/dist/schemas.d.ts +160 -160
- package/dist/shared-context/manager.d.ts +8 -8
- package/dist/support-passport/index.d.ts +4 -4
- package/dist/support-passport/index.js +7 -7
- package/dist/transfer/types.d.ts +78 -78
- package/dist/who-knows-cli.d.ts +1 -1
- package/dist/who-knows.d.ts +1 -1
- package/package.json +2 -2
- package/src/directory-sidecars.test.ts +113 -0
- package/src/directory-sidecars.ts +70 -8
- package/src/experience/session-experience-mining.test.ts +186 -0
- package/src/experience/session-experience-mining.ts +90 -0
- package/src/orchestration/persistence-index.ts +50 -0
- package/src/orchestration/recall-recognition-index.test.ts +249 -0
- package/src/orchestration/recall-recognition-index.ts +169 -0
- package/src/procedural/procedure-miner.ts +8 -6
- package/src/testing/subjects/recall-recognition-index.test.ts +97 -0
- package/dist/chunk-72AV2H3R.js.map +0 -1
- package/dist/chunk-DCRK7I5E.js.map +0 -1
- package/dist/chunk-MSTKJI5T.js.map +0 -1
- /package/dist/{chunk-TEQQQTQS.js.map → chunk-2HJKAVAS.js.map} +0 -0
- /package/dist/{chunk-HWT6KRGS.js.map → chunk-6UEM5EOB.js.map} +0 -0
- /package/dist/{chunk-UCY4EHAG.js.map → chunk-A4TQFNRX.js.map} +0 -0
- /package/dist/{chunk-72N2P44O.js.map → chunk-ARRKSBQU.js.map} +0 -0
- /package/dist/{chunk-WBW34YCJ.js.map → chunk-BCXZ4BDS.js.map} +0 -0
- /package/dist/{chunk-V5BU5PVE.js.map → chunk-CO7KBRPF.js.map} +0 -0
- /package/dist/{chunk-HG42J2EX.js.map → chunk-EQNZC3BH.js.map} +0 -0
- /package/dist/{chunk-35EH6KW4.js.map → chunk-EYK3UHAP.js.map} +0 -0
- /package/dist/{chunk-U6CMLAP4.js.map → chunk-M7HFDWOS.js.map} +0 -0
- /package/dist/{chunk-2UDAABV6.js.map → chunk-MTJUIIM4.js.map} +0 -0
- /package/dist/{chunk-GRX6YH4E.js.map → chunk-MXTCV3B5.js.map} +0 -0
- /package/dist/{chunk-6KCQNFDQ.js.map → chunk-SOOWBRMH.js.map} +0 -0
- /package/dist/{chunk-SOFATSHJ.js.map → chunk-TS66MUUJ.js.map} +0 -0
- /package/dist/{chunk-EW3Y73MN.js.map → chunk-UJBIWY5M.js.map} +0 -0
- /package/dist/{chunk-SVUUOLKM.js.map → chunk-VMKAA5SU.js.map} +0 -0
- /package/dist/{chunk-UQTXER7M.js.map → chunk-ZADYEZY4.js.map} +0 -0
- /package/dist/{chunk-2INZSV27.js.map → chunk-ZANEMAEV.js.map} +0 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* recall-recognition-index.test.ts — issue #2975 write-path maintenance slice.
|
|
3
|
+
*
|
|
4
|
+
* Pins: when recallRecognitionTier is on, a namespace write upserts the
|
|
5
|
+
* memory id into state/index_recognition.json so loadRecognitionIndex
|
|
6
|
+
* sees a fresh entry. Gate-off is zero index I/O. Descriptions are the
|
|
7
|
+
* first non-empty body line (no recognition-trigger generation here).
|
|
8
|
+
*/
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { mock } from "node:test";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { promises as fsp } from "node:fs";
|
|
14
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
15
|
+
import test from "node:test";
|
|
16
|
+
|
|
17
|
+
import { parseConfig } from "../config.js";
|
|
18
|
+
import { loadRecognitionIndex, recognitionIndexPath } from "../recall-recognition-tier.js";
|
|
19
|
+
import { PersistenceIndexCoordinator } from "./persistence-index.js";
|
|
20
|
+
import { maintainRecognitionIndexAfterWrite } from "./recall-recognition-index.js";
|
|
21
|
+
import type { MemoryFile, PluginConfig } from "../types.js";
|
|
22
|
+
|
|
23
|
+
async function tmpNamespace(): Promise<string> {
|
|
24
|
+
return mkdtemp(path.join(os.tmpdir(), "remnic-recognition-index-"));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function exists(filePath: string): Promise<boolean> {
|
|
28
|
+
try {
|
|
29
|
+
await fsp.stat(filePath);
|
|
30
|
+
return true;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test("acceptance: gate-off write is zero index I/O", async () => {
|
|
37
|
+
const dir = await tmpNamespace();
|
|
38
|
+
const spies = [
|
|
39
|
+
mock.method(fsp, "readFile"),
|
|
40
|
+
mock.method(fsp, "writeFile"),
|
|
41
|
+
mock.method(fsp, "mkdir"),
|
|
42
|
+
mock.method(fsp, "open"),
|
|
43
|
+
mock.method(fsp, "rename"),
|
|
44
|
+
mock.method(fsp, "rm"),
|
|
45
|
+
mock.method(fsp, "unlink"),
|
|
46
|
+
mock.method(fsp, "stat"),
|
|
47
|
+
mock.method(fsp, "access"),
|
|
48
|
+
mock.method(fsp, "readdir"),
|
|
49
|
+
];
|
|
50
|
+
try {
|
|
51
|
+
await maintainRecognitionIndexAfterWrite({
|
|
52
|
+
memoryDir: dir,
|
|
53
|
+
enabled: false,
|
|
54
|
+
changes: [{ action: "upsert", id: "fact-001", content: "SSD inference chips in the lab rack.\nMore body." }],
|
|
55
|
+
});
|
|
56
|
+
for (const spy of spies) {
|
|
57
|
+
assert.equal(spy.mock.calls.length, 0, `${String(spy.mock.calls.length)} unexpected fs calls on the off path`);
|
|
58
|
+
}
|
|
59
|
+
assert.equal(await exists(recognitionIndexPath(dir)), false);
|
|
60
|
+
assert.equal(await loadRecognitionIndex(dir), null);
|
|
61
|
+
} finally {
|
|
62
|
+
for (const spy of spies) spy.mock.restore();
|
|
63
|
+
await rm(dir, { recursive: true, force: true });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("acceptance: gate-on write upserts id so loadRecognitionIndex sees a fresh entry", async () => {
|
|
68
|
+
const dir = await tmpNamespace();
|
|
69
|
+
try {
|
|
70
|
+
await maintainRecognitionIndexAfterWrite({
|
|
71
|
+
memoryDir: dir,
|
|
72
|
+
enabled: true,
|
|
73
|
+
changes: [
|
|
74
|
+
{
|
|
75
|
+
action: "upsert",
|
|
76
|
+
id: "fact-001",
|
|
77
|
+
content: "SSD inference chips in the lab rack.\nRunning the model off a local tier.",
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
});
|
|
81
|
+
const index = await loadRecognitionIndex(dir);
|
|
82
|
+
assert.ok(index, "index must exist after an enabled write");
|
|
83
|
+
assert.deepEqual(index.entries, [
|
|
84
|
+
{ id: "fact-001", description: "SSD inference chips in the lab rack." },
|
|
85
|
+
]);
|
|
86
|
+
} finally {
|
|
87
|
+
await rm(dir, { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("incremental write updates an existing id in place and appends a new id", async () => {
|
|
92
|
+
const dir = await tmpNamespace();
|
|
93
|
+
try {
|
|
94
|
+
await maintainRecognitionIndexAfterWrite({
|
|
95
|
+
memoryDir: dir,
|
|
96
|
+
enabled: true,
|
|
97
|
+
changes: [{ action: "upsert", id: "m-001", content: "first line original" }],
|
|
98
|
+
});
|
|
99
|
+
await maintainRecognitionIndexAfterWrite({
|
|
100
|
+
memoryDir: dir,
|
|
101
|
+
enabled: true,
|
|
102
|
+
changes: [
|
|
103
|
+
{ action: "upsert", id: "m-001", content: "first line rewritten\nbody" },
|
|
104
|
+
{ action: "upsert", id: "m-002", content: "second memory trigger" },
|
|
105
|
+
],
|
|
106
|
+
});
|
|
107
|
+
const index = await loadRecognitionIndex(dir);
|
|
108
|
+
assert.deepEqual(index?.entries, [
|
|
109
|
+
{ id: "m-001", description: "first line rewritten" },
|
|
110
|
+
{ id: "m-002", description: "second memory trigger" },
|
|
111
|
+
]);
|
|
112
|
+
} finally {
|
|
113
|
+
await rm(dir, { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("remove drops the id; blank ids are ignored without writing", async () => {
|
|
118
|
+
const dir = await tmpNamespace();
|
|
119
|
+
try {
|
|
120
|
+
await maintainRecognitionIndexAfterWrite({
|
|
121
|
+
memoryDir: dir,
|
|
122
|
+
enabled: true,
|
|
123
|
+
changes: [
|
|
124
|
+
{ action: "upsert", id: "keep", content: "kept" },
|
|
125
|
+
{ action: "upsert", id: "drop", content: "gone" },
|
|
126
|
+
],
|
|
127
|
+
});
|
|
128
|
+
await maintainRecognitionIndexAfterWrite({
|
|
129
|
+
memoryDir: dir,
|
|
130
|
+
enabled: true,
|
|
131
|
+
changes: [{ action: "remove", id: "drop" }],
|
|
132
|
+
});
|
|
133
|
+
assert.deepEqual((await loadRecognitionIndex(dir))?.entries, [
|
|
134
|
+
{ id: "keep", description: "kept" },
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
const before = await fsp.readFile(recognitionIndexPath(dir), "utf8");
|
|
138
|
+
await maintainRecognitionIndexAfterWrite({
|
|
139
|
+
memoryDir: dir,
|
|
140
|
+
enabled: true,
|
|
141
|
+
changes: [{ action: "upsert", id: " ", content: "blank id is dropped" }],
|
|
142
|
+
});
|
|
143
|
+
assert.equal(await fsp.readFile(recognitionIndexPath(dir), "utf8"), before);
|
|
144
|
+
} finally {
|
|
145
|
+
await rm(dir, { recursive: true, force: true });
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
function memory(id: string, content: string): MemoryFile {
|
|
150
|
+
return {
|
|
151
|
+
path: `/tmp/${id}.md`,
|
|
152
|
+
content,
|
|
153
|
+
frontmatter: { id, category: "fact", created: "2026-08-25T00:00:00.000Z", updated: "2026-08-25T00:00:00.000Z", source: "test", confidence: 1, confidenceTier: "explicit", tags: [] },
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function stubCoordinator(config: PluginConfig) {
|
|
158
|
+
return new PersistenceIndexCoordinator({
|
|
159
|
+
config,
|
|
160
|
+
contentHashIndex: null,
|
|
161
|
+
contentHashIndexForStorage: async () => null,
|
|
162
|
+
contentHashIndexesByStorageDir: new Map(),
|
|
163
|
+
embeddingFallback: { isAvailable: async () => false } as never,
|
|
164
|
+
graphIndexFor: () => {
|
|
165
|
+
throw new Error("graph unused");
|
|
166
|
+
},
|
|
167
|
+
readAllMemoriesForNamespaces: async () => [],
|
|
168
|
+
semanticDedupScopeFor: () => ({}),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
test("persist-path: enabled writes update the namespace index; disabled stays quiet", async () => {
|
|
173
|
+
const dir = await tmpNamespace();
|
|
174
|
+
const memories = new Map<string, MemoryFile>([
|
|
175
|
+
["fact-ssd", memory("fact-ssd", "Running the model off an SSD tier.\nDetails.")],
|
|
176
|
+
]);
|
|
177
|
+
const storage = {
|
|
178
|
+
dir,
|
|
179
|
+
getMemoryById: async (id: string) => memories.get(id) ?? null,
|
|
180
|
+
getMemoryByIdIncludingArchived: async (id: string) => memories.get(id) ?? null,
|
|
181
|
+
readAllMemories: async () => [...memories.values()],
|
|
182
|
+
readAllColdMemories: async () => [],
|
|
183
|
+
};
|
|
184
|
+
try {
|
|
185
|
+
const off = stubCoordinator(parseConfig({}));
|
|
186
|
+
await off.updateTemporalTagIndexes(storage as never, ["fact-ssd"]);
|
|
187
|
+
assert.equal(await exists(recognitionIndexPath(dir)), false, "off-path persist must not mint an index");
|
|
188
|
+
|
|
189
|
+
const on = stubCoordinator(parseConfig({ recallRecognitionTier: true }));
|
|
190
|
+
await on.updateTemporalTagIndexes(storage as never, ["fact-ssd"]);
|
|
191
|
+
const index = await loadRecognitionIndex(dir);
|
|
192
|
+
assert.deepEqual(index?.entries, [
|
|
193
|
+
{ id: "fact-ssd", description: "Running the model off an SSD tier." },
|
|
194
|
+
]);
|
|
195
|
+
} finally {
|
|
196
|
+
await rm(dir, { recursive: true, force: true });
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("persist-path: first post-enable persist bootstraps the active corpus, not just the write", async () => {
|
|
201
|
+
const dir = await tmpNamespace();
|
|
202
|
+
const memories = new Map<string, MemoryFile>([
|
|
203
|
+
["fact-old", memory("fact-old", "Existing SSD rack layout.\nOlder note.")],
|
|
204
|
+
["fact-ssd", memory("fact-ssd", "Running the model off an SSD tier.\nDetails.")],
|
|
205
|
+
]);
|
|
206
|
+
const storage = {
|
|
207
|
+
dir,
|
|
208
|
+
getMemoryById: async (id: string) => memories.get(id) ?? null,
|
|
209
|
+
getMemoryByIdIncludingArchived: async (id: string) => memories.get(id) ?? null,
|
|
210
|
+
readAllMemories: async () => [...memories.values()],
|
|
211
|
+
readAllColdMemories: async () => [],
|
|
212
|
+
};
|
|
213
|
+
try {
|
|
214
|
+
const on = stubCoordinator(parseConfig({ recallRecognitionTier: true }));
|
|
215
|
+
await on.updateTemporalTagIndexes(storage as never, ["fact-ssd"]);
|
|
216
|
+
const index = await loadRecognitionIndex(dir);
|
|
217
|
+
assert.deepEqual(
|
|
218
|
+
index?.entries.map((entry) => entry.id).sort(),
|
|
219
|
+
["fact-old", "fact-ssd"],
|
|
220
|
+
);
|
|
221
|
+
assert.deepEqual(
|
|
222
|
+
index?.entries.find((entry) => entry.id === "fact-old"),
|
|
223
|
+
{ id: "fact-old", description: "Existing SSD rack layout." },
|
|
224
|
+
);
|
|
225
|
+
} finally {
|
|
226
|
+
await rm(dir, { recursive: true, force: true });
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("stale recognition-index lock is broken so a later write can publish", async () => {
|
|
231
|
+
const dir = await tmpNamespace();
|
|
232
|
+
const lockDir = `${recognitionIndexPath(dir)}.lock.d`;
|
|
233
|
+
try {
|
|
234
|
+
await fsp.mkdir(lockDir, { recursive: true });
|
|
235
|
+
const stale = new Date(Date.now() - 120_000);
|
|
236
|
+
await fsp.utimes(lockDir, stale, stale);
|
|
237
|
+
await maintainRecognitionIndexAfterWrite({
|
|
238
|
+
memoryDir: dir,
|
|
239
|
+
enabled: true,
|
|
240
|
+
changes: [{ action: "upsert", id: "fact-001", content: "SSD inference chips in the lab rack." }],
|
|
241
|
+
});
|
|
242
|
+
const index = await loadRecognitionIndex(dir);
|
|
243
|
+
assert.deepEqual(index?.entries, [
|
|
244
|
+
{ id: "fact-001", description: "SSD inference chips in the lab rack." },
|
|
245
|
+
]);
|
|
246
|
+
} finally {
|
|
247
|
+
await rm(dir, { recursive: true, force: true });
|
|
248
|
+
}
|
|
249
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recognition-index write-path maintenance (issue #2975).
|
|
3
|
+
*
|
|
4
|
+
* Sibling of recall-recognition-search.ts at the memory write-path seam:
|
|
5
|
+
* when recallRecognitionTier is enabled, a namespace write upserts (or
|
|
6
|
+
* removes) that memory's id in `<memoryDir>/state/index_recognition.json`
|
|
7
|
+
* so the recall slice's loadRecognitionIndex sees a fresh entry. Off-path
|
|
8
|
+
* (`enabled !== true`) returns immediately — zero index I/O.
|
|
9
|
+
*
|
|
10
|
+
* Descriptions: layer 1 has no generator. This slice keeps ids current and
|
|
11
|
+
* fills description from the first non-empty body line (the compact
|
|
12
|
+
* `id: description` form the tier already renders). Discriminability
|
|
13
|
+
* rewrite is a later tidy pass.
|
|
14
|
+
*
|
|
15
|
+
* Serialization matches temporal-index: an in-process op chain per index
|
|
16
|
+
* path plus a directory lock around load → mutate → save. saveRecognitionIndex
|
|
17
|
+
* is single-writer and does not lock on its own.
|
|
18
|
+
*/
|
|
19
|
+
import { promises as fsp } from "node:fs";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
buildRecognitionIndex,
|
|
25
|
+
loadRecognitionIndex,
|
|
26
|
+
recognitionIndexPath,
|
|
27
|
+
saveRecognitionIndex,
|
|
28
|
+
type RecognitionIndexEntry,
|
|
29
|
+
} from "../recall-recognition-tier.js";
|
|
30
|
+
|
|
31
|
+
export type RecognitionIndexChange =
|
|
32
|
+
| { readonly action: "upsert"; readonly id: string; readonly content: string }
|
|
33
|
+
| { readonly action: "remove"; readonly id: string };
|
|
34
|
+
|
|
35
|
+
const writeChains = new Map<string, Promise<void>>();
|
|
36
|
+
const LOCK_POLL_MS = 10;
|
|
37
|
+
const LOCK_ATTEMPTS = 50;
|
|
38
|
+
const LOCK_STALE_MS = 60_000;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* First non-empty line of a memory body. Layer 1 has no description
|
|
42
|
+
* generator; this is the compact line the tier already renders.
|
|
43
|
+
*/
|
|
44
|
+
export function recognitionDescriptionFromContent(content: string): string {
|
|
45
|
+
const trimmed = content.trim();
|
|
46
|
+
if (trimmed.length === 0) return "";
|
|
47
|
+
const newline = trimmed.indexOf("\n");
|
|
48
|
+
return newline === -1 ? trimmed : trimmed.slice(0, newline).trimEnd();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function withIndexChain<T>(indexPath: string, op: () => Promise<T>): Promise<T> {
|
|
52
|
+
const previous = writeChains.get(indexPath) ?? Promise.resolve();
|
|
53
|
+
const run = previous.then(op, op);
|
|
54
|
+
const tail = run.then(() => undefined, () => undefined);
|
|
55
|
+
writeChains.set(indexPath, tail);
|
|
56
|
+
void tail.then(() => {
|
|
57
|
+
if (writeChains.get(indexPath) === tail) writeChains.delete(indexPath);
|
|
58
|
+
});
|
|
59
|
+
return run;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function removeAbandonedRecognitionLock(
|
|
63
|
+
lockDir: string,
|
|
64
|
+
): Promise<"removed" | "wait" | "blocked"> {
|
|
65
|
+
try {
|
|
66
|
+
const info = await fsp.lstat(lockDir);
|
|
67
|
+
if (info.isSymbolicLink()) return "blocked";
|
|
68
|
+
if (!info.isDirectory()) {
|
|
69
|
+
await fsp.rm(lockDir, { force: true });
|
|
70
|
+
return "removed";
|
|
71
|
+
}
|
|
72
|
+
if (Date.now() - info.mtimeMs < LOCK_STALE_MS) return "wait";
|
|
73
|
+
await fsp.rm(lockDir, { recursive: true, force: true });
|
|
74
|
+
return "removed";
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if ((error as NodeJS.ErrnoException)?.code === "ENOENT") return "removed";
|
|
77
|
+
return "blocked";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function withRecognitionIndexLock<T>(memoryDir: string, op: () => Promise<T>): Promise<T> {
|
|
82
|
+
const lockDir = `${recognitionIndexPath(memoryDir)}.lock.d`;
|
|
83
|
+
for (let attempt = 0; attempt < LOCK_ATTEMPTS; attempt++) {
|
|
84
|
+
try {
|
|
85
|
+
await fsp.mkdir(lockDir);
|
|
86
|
+
try {
|
|
87
|
+
return await op();
|
|
88
|
+
} finally {
|
|
89
|
+
await fsp.rm(lockDir, { recursive: true, force: true }).catch(() => undefined);
|
|
90
|
+
}
|
|
91
|
+
} catch (error) {
|
|
92
|
+
const code = (error as NodeJS.ErrnoException)?.code;
|
|
93
|
+
if (code === "ENOENT") {
|
|
94
|
+
await fsp.mkdir(path.dirname(lockDir), { recursive: true }).catch(() => undefined);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (code !== "EEXIST") throw error;
|
|
98
|
+
const cleanup = await removeAbandonedRecognitionLock(lockDir);
|
|
99
|
+
if (cleanup === "blocked") {
|
|
100
|
+
throw new Error(`recognition index lock blocked at ${lockDir}`);
|
|
101
|
+
}
|
|
102
|
+
if (cleanup === "removed") continue;
|
|
103
|
+
await delay(LOCK_POLL_MS);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
throw new Error(`recognition index lock timed out at ${lockDir}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function applyChanges(
|
|
110
|
+
entries: readonly RecognitionIndexEntry[],
|
|
111
|
+
changes: readonly RecognitionIndexChange[],
|
|
112
|
+
): RecognitionIndexEntry[] {
|
|
113
|
+
const next = entries.map((entry) => ({ id: entry.id, description: entry.description }));
|
|
114
|
+
const byId = new Map(next.map((entry, index) => [entry.id, index]));
|
|
115
|
+
for (const change of changes) {
|
|
116
|
+
const id = change.id.trim();
|
|
117
|
+
if (id.length === 0) continue;
|
|
118
|
+
if (change.action === "remove") {
|
|
119
|
+
const index = byId.get(id);
|
|
120
|
+
if (index === undefined) continue;
|
|
121
|
+
next.splice(index, 1);
|
|
122
|
+
byId.delete(id);
|
|
123
|
+
for (const [key, value] of byId) {
|
|
124
|
+
if (value > index) byId.set(key, value - 1);
|
|
125
|
+
}
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const description = recognitionDescriptionFromContent(change.content);
|
|
129
|
+
const existing = byId.get(id);
|
|
130
|
+
if (existing !== undefined) {
|
|
131
|
+
next[existing] = { id, description };
|
|
132
|
+
} else {
|
|
133
|
+
byId.set(id, next.length);
|
|
134
|
+
next.push({ id, description });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return next;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function meaningfulChanges(changes: readonly RecognitionIndexChange[]): RecognitionIndexChange[] {
|
|
141
|
+
return changes.filter((change) => change.id.trim().length > 0);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Incrementally maintain a namespace recognition index after memory writes.
|
|
146
|
+
* `enabled: false` is a proven no-op: no reads, no writes, no locks.
|
|
147
|
+
*/
|
|
148
|
+
export async function maintainRecognitionIndexAfterWrite(args: {
|
|
149
|
+
memoryDir: string;
|
|
150
|
+
enabled: boolean;
|
|
151
|
+
changes: readonly RecognitionIndexChange[];
|
|
152
|
+
}): Promise<void> {
|
|
153
|
+
if (args.enabled !== true) return;
|
|
154
|
+
const changes = meaningfulChanges(args.changes);
|
|
155
|
+
if (changes.length === 0) return;
|
|
156
|
+
|
|
157
|
+
const indexPath = recognitionIndexPath(args.memoryDir);
|
|
158
|
+
await withIndexChain(indexPath, () =>
|
|
159
|
+
withRecognitionIndexLock(args.memoryDir, async () => {
|
|
160
|
+
const loaded = await loadRecognitionIndex(args.memoryDir);
|
|
161
|
+
// `changes` is the full set the caller wants published. Persist-path
|
|
162
|
+
// bootstraps from the active corpus when the file is absent so a
|
|
163
|
+
// first post-enable write cannot mint a 1-id index that recall would
|
|
164
|
+
// treat as authoritative.
|
|
165
|
+
const entries = applyChanges(loaded?.entries ?? [], changes);
|
|
166
|
+
await saveRecognitionIndex(args.memoryDir, buildRecognitionIndex(entries));
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
@@ -4,14 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
import type { PluginConfig } from "../types.js";
|
|
6
6
|
import type { StorageManager } from "../storage.js";
|
|
7
|
-
import type { CausalTrajectoryRecord } from "../causal-trajectory.js";
|
|
8
7
|
import { createHash } from "node:crypto";
|
|
9
8
|
import { mkdir, rm, stat } from "node:fs/promises";
|
|
10
9
|
import path from "node:path";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
filterTrajectoriesByLookbackDays,
|
|
14
|
-
} from "../causal-trajectory.js";
|
|
10
|
+
import { readCausalTrajectoryRecords, type CausalTrajectoryRecord } from "../causal-trajectory.js";
|
|
11
|
+
import { collectProcedureMiningRecords } from "../experience/session-experience-mining.js";
|
|
15
12
|
import { buildProcedurePersistBody, normalizeProcedureSteps, type ProcedureStep } from "./procedure-types.js";
|
|
16
13
|
import { clusterByKey } from "./reinforcement-core.js";
|
|
17
14
|
import { log } from "../logger.js";
|
|
@@ -207,7 +204,12 @@ export async function runProcedureMining(options: {
|
|
|
207
204
|
memoryDir: options.memoryDir,
|
|
208
205
|
causalTrajectoryStoreDir: trajectoryDir,
|
|
209
206
|
});
|
|
210
|
-
const recent =
|
|
207
|
+
const recent = await collectProcedureMiningRecords({
|
|
208
|
+
trajectories,
|
|
209
|
+
lookbackDays: cfg.lookbackDays,
|
|
210
|
+
storage: options.storage,
|
|
211
|
+
experienceEnabled: options.config.sessionExperience?.enabled === true,
|
|
212
|
+
});
|
|
211
213
|
|
|
212
214
|
const clusters = clusterByKey(recent, clusterKey);
|
|
213
215
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recognition-index write-path subject for the scenario-matrix harness
|
|
3
|
+
* (issue #2975).
|
|
4
|
+
*
|
|
5
|
+
* Every canonical row drives the real maintainRecognitionIndexAfterWrite
|
|
6
|
+
* seam twice: gate off (zero index I/O, no index file) and gate on
|
|
7
|
+
* (the write's id is visible to loadRecognitionIndex). Restart rows
|
|
8
|
+
* reload from disk; replay rows prove an id upsert is idempotent.
|
|
9
|
+
*/
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { promises as fsp } from "node:fs";
|
|
14
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
15
|
+
|
|
16
|
+
import { loadRecognitionIndex, recognitionIndexPath } from "../../recall-recognition-tier.js";
|
|
17
|
+
import { maintainRecognitionIndexAfterWrite } from "../../orchestration/recall-recognition-index.js";
|
|
18
|
+
import { type LifecycleSubject, type MatrixRow, runLifecycleMatrix } from "../lifecycle-matrix.js";
|
|
19
|
+
|
|
20
|
+
interface IndexState {
|
|
21
|
+
rowId: string;
|
|
22
|
+
dir: string;
|
|
23
|
+
restart: boolean;
|
|
24
|
+
dedupeOrReplay: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const subject: LifecycleSubject<IndexState> = {
|
|
28
|
+
async setup(row: MatrixRow): Promise<IndexState> {
|
|
29
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-recognition-index-subject-"));
|
|
30
|
+
return {
|
|
31
|
+
rowId: String(row.id),
|
|
32
|
+
dir,
|
|
33
|
+
restart: row.dimensions.restart,
|
|
34
|
+
dedupeOrReplay: row.dimensions.dedupeOrReplay,
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
async exercise(state): Promise<void> {
|
|
39
|
+
const id = `fact-${state.rowId}`;
|
|
40
|
+
await maintainRecognitionIndexAfterWrite({
|
|
41
|
+
memoryDir: state.dir,
|
|
42
|
+
enabled: false,
|
|
43
|
+
changes: [{ action: "upsert", id, content: `off-path body for ${state.rowId}` }],
|
|
44
|
+
});
|
|
45
|
+
assert.equal(await loadRecognitionIndex(state.dir), null);
|
|
46
|
+
try {
|
|
47
|
+
await fsp.access(recognitionIndexPath(state.dir));
|
|
48
|
+
assert.fail("off-path must not create the recognition index file");
|
|
49
|
+
} catch (err) {
|
|
50
|
+
assert.equal((err as NodeJS.ErrnoException).code, "ENOENT");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await maintainRecognitionIndexAfterWrite({
|
|
54
|
+
memoryDir: state.dir,
|
|
55
|
+
enabled: true,
|
|
56
|
+
changes: [{ action: "upsert", id, content: `SSD inference chips — ${state.rowId}\nbody` }],
|
|
57
|
+
});
|
|
58
|
+
if (state.dedupeOrReplay) {
|
|
59
|
+
await maintainRecognitionIndexAfterWrite({
|
|
60
|
+
memoryDir: state.dir,
|
|
61
|
+
enabled: true,
|
|
62
|
+
changes: [{ action: "upsert", id, content: `SSD inference chips — ${state.rowId} replayed\nbody` }],
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
async invariants(state): Promise<void> {
|
|
68
|
+
const id = `fact-${state.rowId}`;
|
|
69
|
+
const index = await loadRecognitionIndex(state.dir);
|
|
70
|
+
assert.ok(index, "enabled write must persist an index loadable after the row");
|
|
71
|
+
assert.equal(index.entries.length, 1);
|
|
72
|
+
assert.equal(index.entries[0]?.id, id);
|
|
73
|
+
const expected = state.dedupeOrReplay
|
|
74
|
+
? `SSD inference chips — ${state.rowId} replayed`
|
|
75
|
+
: `SSD inference chips — ${state.rowId}`;
|
|
76
|
+
assert.equal(index.entries[0]?.description, expected);
|
|
77
|
+
|
|
78
|
+
if (state.restart) {
|
|
79
|
+
const reloaded = await loadRecognitionIndex(state.dir);
|
|
80
|
+
assert.deepEqual(reloaded, index, "restart must see the same on-disk index");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const before = { ...index };
|
|
84
|
+
await maintainRecognitionIndexAfterWrite({
|
|
85
|
+
memoryDir: state.dir,
|
|
86
|
+
enabled: false,
|
|
87
|
+
changes: [{ action: "remove", id }],
|
|
88
|
+
});
|
|
89
|
+
assert.deepEqual(await loadRecognitionIndex(state.dir), before, "off-path after on-path must not mutate the index");
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
async teardown(state): Promise<void> {
|
|
93
|
+
await rm(state.dir, { recursive: true, force: true });
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
runLifecycleMatrix("recall-recognition-index", subject);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/directory-sidecars.ts"],"sourcesContent":["/**\n * @remnic/core — per-directory abstract/overview sidecars (issue #2977).\n *\n * One derived `overview.md` per memory directory (category roots and every\n * directory beneath them, including `namespaces/<ns>/` subtrees), rendered\n * bottom-up from existing memory content:\n *\n * - the ABSTRACT is a single line (<=256 chars) composed from child titles;\n * - the OVERVIEW body lists one preview line per child memory and embeds\n * each child DIRECTORY's abstract, so a parent summarizes its subtree\n * without embedding full child overviews.\n *\n * Determinism: no LLM, no embeddings. Content is assembled with the same\n * preview machinery the OKF index files use (`normalizeProjectionPreview`).\n *\n * Invalidation: each sidecar stores a content-free fingerprint of its subtree\n * (child name|size|mtime for files, recursive fingerprints for child dirs).\n * Any add/edit/delete anywhere below a directory changes its fingerprint, so\n * `loadDirectorySidecar` can detect staleness with `readdir`+`stat` alone and\n * refresh without reading child memory files first. Eager regeneration runs\n * through {@link runDirectorySidecarMaintenance}; lazy refresh on read covers\n * direct-write paths that bypass scheduled maintenance (Gotcha #43).\n *\n * Isolation: sidecars are derived artifacts, excluded from generic recall via\n * `isGenericRecallExcludedPath` (basename match, the same tradeoff OKF makes\n * for `index.md`/`log.md`) while staying full-text searchable. Files at the\n * sidecar path WITHOUT the marker are user content: never overwritten, never\n * removed.\n *\n * Layer 2 (this slice): {@link refreshDirectorySidecarsAfterWrite} refreshes\n * only the changed directory and its category ancestors, so a memory write\n * populates sidecars without walking the rest of the store. `enabled: false`\n * is a proven no-op (no reads, no writes). Config parsing lives here;\n * `parseConfig` wiring, retrieval drill-down, xray, and LLM summarization\n * remain later layers on #2977 (`config.ts` is at its file-size ratchet).\n */\n\nimport { createHash } from \"node:crypto\";\nimport { lstatSync, readFileSync, readdirSync, statSync, unlinkSync } from \"node:fs\";\nimport path from \"node:path\";\n\nimport { coerceBooleanLike } from \"./connectors/coerce.js\";\nimport { writeFileAtomically } from \"./maintenance/atomic-file.js\";\nimport { normalizeProjectionPreview } from \"./memory-projection-format.js\";\nimport { OKF_RESERVED_BASENAMES } from \"./okf/type-mapping.js\";\nimport { RECALL_FALLBACK_DIRS } from \"./utils/category-dir.js\";\n\nexport const DIRECTORY_SIDECAR_BASENAME = \"overview.md\";\nexport const DIRECTORY_SIDECAR_MARKER = \"<!-- remnic-directory-sidecar -->\";\n\nconst ABSTRACT_MAX_CHARS = 256;\nconst OVERVIEW_MAX_CHARS = 4000;\nconst DEFAULT_QUERY_LIMIT = 5;\nconst CATEGORY_ROOTS: ReadonlySet<string> = new Set(RECALL_FALLBACK_DIRS);\n\n/** Config fields this slice owns. Mix into `PluginConfig` in a later layer. */\nexport interface DirectorySidecarSettings {\n /** Populate sidecars on memory writes. Default false. */\n directorySidecarsEnabled: boolean;\n}\n\n/** Strict off-default: only an explicit true/\"true\"/1 opts in. */\nexport function parseDirectorySidecarsEnabled(raw: unknown): boolean {\n return coerceBooleanLike(raw, \"directorySidecarsEnabled\") === true;\n}\n\n/** Basenames never treated as child memories (ours plus OKF's reserved ones). */\nconst RESERVED_CHILD_BASENAMES: Readonly<Record<string, true>> = Object.freeze({\n ...OKF_RESERVED_BASENAMES,\n [DIRECTORY_SIDECAR_BASENAME]: true,\n});\n\nexport interface DirectorySidecar {\n /** Absolute directory the sidecar describes. */\n readonly dir: string;\n /** Single-line L0 abstract (<=256 chars). */\n readonly abstract: string;\n /** Abstract plus the per-child preview body (<=4000 chars). */\n readonly overview: string;\n /** Number of memory `.md` children in this directory. */\n readonly childCount: number;\n /** False when the stored fingerprint no longer matches the subtree. */\n readonly fresh: boolean;\n}\n\nexport interface DirectorySidecarMatch extends DirectorySidecar {\n readonly score: number;\n}\n\nexport interface DirectorySidecarReport {\n written: string[];\n removed: string[];\n}\n\ninterface ParsedSidecar {\n abstract: string;\n overview: string;\n childCount: number;\n fingerprint: string;\n}\n\nfunction listEntries(dir: string) {\n try {\n return readdirSync(dir, { withFileTypes: true });\n } catch {\n return [];\n }\n}\n\n/** Root check that refuses symlinks (fs-boundary rule, PRs #1938/#1999). */\nfunction isRealDirectory(entryPath: string): boolean {\n try {\n const stats = lstatSync(entryPath);\n return !stats.isSymbolicLink() && stats.isDirectory();\n } catch {\n return false;\n }\n}\n\nfunction memoryChildren(dir: string): string[] {\n // Dirent.isFile() is false for symlinks, so a linked memory never enters the corpus.\n return listEntries(dir)\n .filter((entry) => entry.isFile() && !RESERVED_CHILD_BASENAMES[entry.name] && entry.name.endsWith(\".md\"))\n .map((entry) => entry.name)\n .sort((a, b) => a.localeCompare(b));\n}\n\nfunction directoryChildren(dir: string): string[] {\n // Dirent.isDirectory() is false for symlinks (fs-boundary rule, PRs #1938/#1999).\n return listEntries(dir)\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name)\n .sort((a, b) => a.localeCompare(b));\n}\n\n/**\n * Content-free subtree fingerprint: sha256 over sorted child descriptors.\n * Files contribute `name|size|mtimeMs`; directories contribute their own\n * recursive fingerprint, so any change below a directory invalidates every\n * ancestor. Reads no file contents.\n */\nexport function directoryFingerprint(dir: string): string {\n const hash = createHash(\"sha256\");\n for (const name of memoryChildren(dir)) {\n try {\n const stats = statSync(path.join(dir, name));\n hash.update(`f|${name}|${stats.size}|${Math.trunc(stats.mtimeMs)}\\n`);\n } catch {\n hash.update(`f|${name}|missing\\n`);\n }\n }\n for (const name of directoryChildren(dir)) {\n hash.update(`d|${name}|${directoryFingerprint(path.join(dir, name))}\\n`);\n }\n return hash.digest(\"hex\");\n}\n\nfunction readFileOrNull(target: string): string | null {\n try {\n return readFileSync(target, \"utf8\");\n } catch {\n return null;\n }\n}\n\nfunction parseSidecar(content: string): ParsedSidecar | null {\n if (!content.startsWith(DIRECTORY_SIDECAR_MARKER)) return null;\n const fingerprint = /^fingerprint: ([0-9a-f]{64})$/m.exec(content)?.[1];\n if (!fingerprint) return null;\n const abstract = /^## Abstract\\n([^\\n]+)$/m.exec(content)?.[1] ?? \"\";\n const overviewStart = content.indexOf(\"## Abstract\");\n const overview =\n overviewStart === -1 ? \"\" : content.slice(overviewStart, overviewStart + OVERVIEW_MAX_CHARS).trim();\n // The `* name — preview` child bullets are the rendered child count.\n const childCount = (content.match(/^\\* .+$/gm) ?? []).length;\n return { abstract, overview, childCount, fingerprint };\n}\n\nfunction sidecarPath(dir: string): string {\n return path.join(dir, DIRECTORY_SIDECAR_BASENAME);\n}\n\nfunction renderSidecar(dir: string, fingerprint: string, childAbstracts: Map<string, string>): string | null {\n const children = memoryChildren(dir);\n const childDirs = directoryChildren(dir).filter((name) => childAbstracts.has(path.join(dir, name)));\n if (children.length === 0 && childDirs.length === 0) return null;\n\n const previews = children.map((name) => {\n try {\n return normalizeProjectionPreview(readFileSync(path.join(dir, name), \"utf8\"), 120);\n } catch {\n return \"\";\n }\n });\n const titles = children.map(\n (name, index) => previews[index].split(\" \").slice(0, 8).join(\" \") || name.replace(/\\.md$/, \"\"),\n );\n const abstract = normalizeProjectionPreview(\n `${path.basename(dir)}/ — ${children.length} memories: ${titles.slice(0, 5).join(\" · \")}`,\n ABSTRACT_MAX_CHARS,\n );\n\n const lines = [\n DIRECTORY_SIDECAR_MARKER,\n `fingerprint: ${fingerprint}`,\n `# ${path.basename(dir)}/ overview`,\n \"\",\n \"## Abstract\",\n abstract,\n \"\",\n \"## Children\",\n ];\n for (const [index, name] of children.entries()) {\n lines.push(`* ${name} — ${previews[index]}`);\n }\n for (const name of childDirs) {\n lines.push(`### ${name}/`);\n lines.push(childAbstracts.get(path.join(dir, name)) ?? \"\");\n }\n lines.push(\"\");\n return lines.join(\"\\n\");\n}\n\n/** Scope roots whose category subtrees carry sidecars: the memory root plus every namespace root. */\nfunction scopeRoots(memoryDir: string): string[] {\n const roots = [memoryDir];\n const namespacesRoot = path.join(memoryDir, \"namespaces\");\n for (const entry of listEntries(namespacesRoot)) {\n if (entry.isDirectory() && isRealDirectory(path.join(namespacesRoot, entry.name))) {\n roots.push(path.join(namespacesRoot, entry.name));\n }\n }\n return roots;\n}\n\n/**\n * Every directory that should carry a sidecar, deepest first (post-order) so\n * parents render after their children and can embed child abstracts.\n */\nfunction collectSidecarDirs(scopeRoot: string): string[] {\n const dirs: string[] = [];\n const walk = (dir: string, isCategoryRoot: boolean): boolean => {\n let carries = memoryChildren(dir).length > 0;\n for (const name of directoryChildren(dir)) {\n carries = walk(path.join(dir, name), false) || carries;\n }\n // A directory that stopped carrying memories must still be visited so its\n // orphaned sidecar can be pruned or rewritten (cache-invalidation completeness).\n const hasSidecar = readFileOrNull(sidecarPath(dir))?.startsWith(DIRECTORY_SIDECAR_MARKER) === true;\n if (isCategoryRoot || carries || hasSidecar) dirs.push(dir);\n return carries;\n };\n for (const category of RECALL_FALLBACK_DIRS) {\n const categoryDir = path.join(scopeRoot, category);\n if (!isRealDirectory(categoryDir)) continue;\n walk(categoryDir, true);\n }\n return dirs;\n}\n\nasync function syncSidecarDir(\n dir: string,\n enabled: boolean,\n childAbstracts: Map<string, string>,\n written: string[],\n removed: string[],\n): Promise<void> {\n const target = sidecarPath(dir);\n const raw = readFileOrNull(target);\n const existing = raw !== null && raw.startsWith(DIRECTORY_SIDECAR_MARKER) ? raw : null;\n if (!enabled) {\n if (existing !== null) {\n unlinkSync(target);\n removed.push(target);\n }\n return;\n }\n const next = renderSidecar(dir, directoryFingerprint(dir), childAbstracts);\n if (next === null) {\n if (existing !== null) {\n unlinkSync(target);\n removed.push(target);\n }\n return;\n }\n const parsedNext = parseSidecar(next);\n if (parsedNext) childAbstracts.set(dir, parsedNext.abstract);\n if (raw === next) return;\n if (existing === null && raw !== null) return; // user-authored file: hands off\n await writeFileAtomically(target, next);\n written.push(target);\n}\n\nfunction childAbstractsFromDisk(dir: string): Map<string, string> {\n const abstracts = new Map<string, string>();\n for (const name of directoryChildren(dir)) {\n const childDir = path.join(dir, name);\n const parsed = parseSidecar(readFileOrNull(sidecarPath(childDir)) ?? \"\");\n if (parsed) abstracts.set(childDir, parsed.abstract);\n }\n return abstracts;\n}\n\n/**\n * Category-root ancestry of a changed memory path, deepest first.\n * Paths outside a recall category return `[]` so writes to `state/` or\n * `namespaces/<ns>/` itself never mint a sidecar.\n */\nexport function directorySidecarAncestry(memoryDir: string, changedPath: string): string[] {\n const root = path.resolve(memoryDir);\n const abs = path.isAbsolute(changedPath) ? path.resolve(changedPath) : path.resolve(root, changedPath);\n const rel = path.relative(root, abs);\n if (rel.startsWith(\"..\") || path.isAbsolute(rel)) return [];\n const parts = rel.split(path.sep).filter(Boolean);\n const dirParts = isRealDirectory(abs) ? parts : parts.slice(0, -1);\n let categoryAt = -1;\n if (dirParts[0] === \"namespaces\" && dirParts.length >= 3 && CATEGORY_ROOTS.has(dirParts[2] ?? \"\")) {\n categoryAt = 2;\n } else if (CATEGORY_ROOTS.has(dirParts[0] ?? \"\")) {\n categoryAt = 0;\n }\n if (categoryAt === -1) return [];\n const dirs: string[] = [];\n for (let i = dirParts.length; i > categoryAt; i--) {\n dirs.push(path.join(root, ...dirParts.slice(0, i)));\n }\n return dirs;\n}\n\n/**\n * Regenerates directory sidecars incrementally. Unchanged sidecars are left\n * untouched; emptied directories lose theirs; user files at the sidecar path\n * (no marker) are never written or removed. With `enabled: false` every\n * generated sidecar is removed — the OKF index-maintenance contract.\n */\nexport async function runDirectorySidecarMaintenance(\n memoryDir: string,\n enabled: boolean,\n): Promise<DirectorySidecarReport> {\n const written: string[] = [];\n const removed: string[] = [];\n for (const scope of scopeRoots(memoryDir)) {\n const childAbstracts = new Map<string, string>();\n for (const dir of collectSidecarDirs(scope)) {\n await syncSidecarDir(dir, enabled, childAbstracts, written, removed);\n }\n }\n return { written, removed };\n}\n\n/**\n * Incremental write-path refresh: only the changed directory and its\n * category ancestors, using the same fingerprint/render path as maintenance.\n * `enabled: false` returns immediately — no readdir, no writes.\n */\nexport async function refreshDirectorySidecarsAfterWrite(\n memoryDir: string,\n changedPath: string,\n enabled: boolean,\n): Promise<DirectorySidecarReport> {\n if (!enabled) return { written: [], removed: [] };\n const written: string[] = [];\n const removed: string[] = [];\n for (const dir of directorySidecarAncestry(memoryDir, changedPath)) {\n await syncSidecarDir(dir, true, childAbstractsFromDisk(dir), written, removed);\n }\n return { written, removed };\n}\n\n/**\n * Read a directory's sidecar for retrieval. By default a stale sidecar is\n * refreshed in place (child directories first), which is what makes direct\n * writes that bypass maintenance still converge. With `{ refresh: false }`\n * the stored sidecar is returned as-is with `fresh: false` so callers can\n * observe staleness without writes.\n */\nexport async function loadDirectorySidecar(\n dir: string,\n options: { refresh?: boolean } = {},\n): Promise<DirectorySidecar | null> {\n const refresh = options.refresh !== false;\n const target = sidecarPath(dir);\n const stored = readFileOrNull(target);\n const generated = stored !== null && stored.startsWith(DIRECTORY_SIDECAR_MARKER);\n const parsed = generated ? parseSidecar(stored) : null;\n const fingerprint = directoryFingerprint(dir);\n if (parsed !== null && (parsed.fingerprint === fingerprint || !refresh)) {\n return {\n dir,\n abstract: parsed.abstract,\n overview: parsed.overview,\n childCount: parsed.childCount,\n fresh: parsed.fingerprint === fingerprint,\n };\n }\n if (!refresh) return null;\n\n // Refresh bottom-up so this directory's body embeds current child abstracts.\n const childAbstracts = new Map<string, string>();\n for (const name of directoryChildren(dir)) {\n const child = await loadDirectorySidecar(path.join(dir, name));\n if (child) childAbstracts.set(path.join(dir, name), child.abstract);\n }\n const next = renderSidecar(dir, directoryFingerprint(dir), childAbstracts);\n if (next === null) {\n if (generated) unlinkSync(target);\n return null;\n }\n if (readFileOrNull(target) !== next) {\n if (stored !== null && !generated) {\n // User-authored file at the sidecar path: serve nothing rather than clobber it.\n return null;\n }\n await writeFileAtomically(target, next);\n }\n const refreshed = parseSidecar(next);\n return refreshed\n ? {\n dir,\n abstract: refreshed.abstract,\n overview: refreshed.overview,\n childCount: refreshed.childCount,\n fresh: true,\n }\n : null;\n}\n\n/**\n * Overview-query primitive for hierarchical retrieval (layer 1 of #2977):\n * score every sidecar's abstract (+3 per query term) and overview (+1 per\n * term) lexically, then return the most specific matches — when both a\n * directory and its descendant match, only the descendant is kept, matching\n * the drill-down semantics the full retrieval mode will build on.\n */\nexport async function findDirectorySidecarsForQuery(\n memoryDir: string,\n query: string,\n options: { namespace?: string; limit?: number } = {},\n): Promise<DirectorySidecarMatch[]> {\n const scope = options.namespace ? path.join(memoryDir, \"namespaces\", options.namespace) : memoryDir;\n if (!isRealDirectory(scope)) return [];\n const terms = query\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter((term) => term.length >= 2);\n\n const matches: DirectorySidecarMatch[] = [];\n for (const root of scopeRoots(scope)) {\n for (const dir of collectSidecarDirs(root)) {\n const sidecar = await loadDirectorySidecar(dir);\n if (!sidecar) continue;\n const abstractLower = sidecar.abstract.toLowerCase();\n const overviewLower = sidecar.overview.toLowerCase();\n let score = 0;\n for (const term of terms) {\n if (abstractLower.includes(term)) score += 3;\n else if (overviewLower.includes(term)) score += 1;\n }\n if (score > 0) matches.push({ ...sidecar, score });\n }\n }\n matches.sort((a, b) => b.score - a.score || a.dir.localeCompare(b.dir));\n\n const limit = options.limit ?? DEFAULT_QUERY_LIMIT;\n return matches\n .filter((candidate) => !matches.some((other) => other.dir.startsWith(`${candidate.dir}${path.sep}`)))\n .slice(0, limit);\n}\n"],"mappings":"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","names":[]}
|