@remnic/core 10.0.51 → 10.0.52
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-cli.js +1 -1
- package/dist/{chunk-SJD334AG.js → chunk-UM3ZLT2X.js} +187 -146
- package/dist/chunk-UM3ZLT2X.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/orchestration/extraction-persist-deps.d.ts +2 -0
- package/dist/orchestration/extraction-persist-deps.d.ts.map +1 -1
- package/dist/orchestration/extraction-persist-promotion.d.ts +40 -10
- package/dist/orchestration/extraction-persist-promotion.d.ts.map +1 -1
- package/dist/orchestration/extraction-persist.d.ts.map +1 -1
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +1 -1
- package/package.json +2 -2
- package/src/orchestration/extraction-persist-deps.ts +3 -0
- package/src/orchestration/extraction-persist-promotion.ts +104 -16
- package/src/orchestration/extraction-persist.ts +66 -77
- package/src/orchestrator-entity-write-failure.test.ts +370 -0
- package/src/orchestrator.ts +1 -0
- package/dist/chunk-SJD334AG.js.map +0 -1
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
|
|
5
|
+
import { SmartBuffer } from "./buffer.js";
|
|
6
|
+
import { resetExtractionLlmHealthForTests } from "./extraction-llm-health.js";
|
|
7
|
+
import { Orchestrator } from "./orchestrator.js";
|
|
8
|
+
import { ContentHashIndex } from "./storage.js";
|
|
9
|
+
import {
|
|
10
|
+
cleanupDir,
|
|
11
|
+
harmonicNodeStoreStatus,
|
|
12
|
+
makeHarmonicLifecycleConfig,
|
|
13
|
+
makeLifecycleConfig,
|
|
14
|
+
mkTempMemoryDir,
|
|
15
|
+
stubExtraction,
|
|
16
|
+
} from "./testing/orchestrator-lite.js";
|
|
17
|
+
import type { ExtractionResult } from "./types.js";
|
|
18
|
+
|
|
19
|
+
/** Owner contract for the private Orchestrator temporal/tag batch hook. */
|
|
20
|
+
const FACT = "Project Alpha uses a queue for background processing.";
|
|
21
|
+
|
|
22
|
+
for (const includeFact of [false, true]) {
|
|
23
|
+
test(`#3228: entity write failure retains input${includeFact ? " after a committed fact" : ""}`, async () => {
|
|
24
|
+
const memoryDir = await mkTempMemoryDir("entity-write-failure");
|
|
25
|
+
const config = makeLifecycleConfig(memoryDir, {
|
|
26
|
+
chunkingEnabled: false,
|
|
27
|
+
multiGraphMemoryEnabled: false,
|
|
28
|
+
factDeduplicationEnabled: true,
|
|
29
|
+
extractionMinImportanceLevel: "trivial",
|
|
30
|
+
entityActivityLogEnabled: false,
|
|
31
|
+
});
|
|
32
|
+
const orchestrator = new Orchestrator(config);
|
|
33
|
+
const storage = await orchestrator.getStorageForNamespace();
|
|
34
|
+
const writeEntity = storage.writeEntity.bind(storage);
|
|
35
|
+
try {
|
|
36
|
+
await storage.ensureDirectories();
|
|
37
|
+
const entityName = storage.normalizeEntityName("Alpha", "project");
|
|
38
|
+
await orchestrator.buffer.load();
|
|
39
|
+
const bufferKey = "entity-write-failure";
|
|
40
|
+
await orchestrator.buffer.addTurn(bufferKey, {
|
|
41
|
+
role: "user",
|
|
42
|
+
content: FACT,
|
|
43
|
+
sessionKey: bufferKey,
|
|
44
|
+
timestamp: "2026-10-01T12:00:00.000Z",
|
|
45
|
+
sourceValidAt: "2026-10-01T12:00:00.000Z",
|
|
46
|
+
persistProcessedFingerprint: true,
|
|
47
|
+
});
|
|
48
|
+
await orchestrator.buffer.flushPendingSave({ throwOnFailure: true });
|
|
49
|
+
const turns = orchestrator.buffer.getTurns(bufferKey);
|
|
50
|
+
assert.equal(turns.length, 1, "the source turn must be durably buffered before extraction");
|
|
51
|
+
const before = await storage.loadMeta();
|
|
52
|
+
const extractionCalls = stubExtraction(orchestrator, (): ExtractionResult => ({
|
|
53
|
+
facts: includeFact
|
|
54
|
+
? [{ content: FACT, category: "fact", confidence: 0.9, tags: [] }]
|
|
55
|
+
: [],
|
|
56
|
+
entities: [{ name: "Alpha", type: "project", facts: ["Uses a queue"] }],
|
|
57
|
+
questions: [],
|
|
58
|
+
profileUpdates: [],
|
|
59
|
+
}));
|
|
60
|
+
const writeFailure = Object.assign(new Error("synthetic entity write denied"), { code: "EACCES" });
|
|
61
|
+
let entityWriteAttempts = 0;
|
|
62
|
+
storage.writeEntity = async (...args: Parameters<typeof writeEntity>) => {
|
|
63
|
+
entityWriteAttempts += 1;
|
|
64
|
+
assert.deepEqual(args.slice(0, 3), ["Alpha", "project", ["Uses a queue"]]);
|
|
65
|
+
throw writeFailure;
|
|
66
|
+
};
|
|
67
|
+
let commits = 0;
|
|
68
|
+
const options = { bufferKey, onDurableCommit: () => { commits += 1; } };
|
|
69
|
+
|
|
70
|
+
await assert.rejects(
|
|
71
|
+
orchestrator.runExtraction(turns, options),
|
|
72
|
+
(error: Error) => error === writeFailure,
|
|
73
|
+
"the original entity I/O failure must reach the caller",
|
|
74
|
+
);
|
|
75
|
+
assert.equal(entityWriteAttempts, 1, "the failure must come from the real entity persistence path");
|
|
76
|
+
assert.equal(commits, 0, "a failed batch must not signal durable completion");
|
|
77
|
+
assert.deepEqual(orchestrator.buffer.getTurns(bufferKey), turns);
|
|
78
|
+
const retainedBuffer = new SmartBuffer(config, storage);
|
|
79
|
+
await retainedBuffer.load();
|
|
80
|
+
assert.deepEqual(retainedBuffer.getTurns(bufferKey), turns, "source turns must remain on disk too");
|
|
81
|
+
const failedMeta = await storage.loadMeta();
|
|
82
|
+
assert.equal(failedMeta.processedExtractionFingerprints?.length ?? 0, 0);
|
|
83
|
+
assert.equal(failedMeta.extractionCount, before.extractionCount);
|
|
84
|
+
assert.equal(await storage.readEntity(entityName), "", "the failed entity was not written");
|
|
85
|
+
|
|
86
|
+
const factsBeforeRetry = (await storage.readAllMemories()).filter((m) => m.frontmatter.category === "fact");
|
|
87
|
+
assert.equal(factsBeforeRetry.length, includeFact ? 1 : 0);
|
|
88
|
+
if (includeFact) {
|
|
89
|
+
const hashes = new ContentHashIndex(path.join(storage.dir, "state"));
|
|
90
|
+
await hashes.load();
|
|
91
|
+
assert.equal(hashes.has(FACT), true, "the earlier fact's deferred hash must be durable before rejection");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
storage.writeEntity = writeEntity;
|
|
95
|
+
const retried = await orchestrator.runExtraction(turns, options);
|
|
96
|
+
assert.equal(retried.status, "completed");
|
|
97
|
+
assert.equal(retried.persistedCount, 1, "retry writes the entity, not another copy of an earlier fact");
|
|
98
|
+
assert.equal(commits, 1);
|
|
99
|
+
assert.match(await storage.readEntity(entityName), /Uses a queue/);
|
|
100
|
+
assert.equal((await storage.listEntityNames()).length, 1);
|
|
101
|
+
const factsAfterRetry = (await storage.readAllMemories()).filter((m) => m.frontmatter.category === "fact");
|
|
102
|
+
assert.deepEqual(
|
|
103
|
+
factsAfterRetry.map((m) => m.frontmatter.id),
|
|
104
|
+
factsBeforeRetry.map((m) => m.frontmatter.id),
|
|
105
|
+
"retry must preserve the already committed fact without duplicating it",
|
|
106
|
+
);
|
|
107
|
+
assert.equal((await storage.loadMeta()).processedExtractionFingerprints?.length, 1);
|
|
108
|
+
assert.equal(orchestrator.buffer.getTurns(bufferKey).length, 0);
|
|
109
|
+
const clearedBuffer = new SmartBuffer(config, storage);
|
|
110
|
+
await clearedBuffer.load();
|
|
111
|
+
assert.equal(clearedBuffer.getTurns(bufferKey).length, 0);
|
|
112
|
+
assert.equal((await orchestrator.runExtraction(turns, options)).reason, "processed_fingerprint");
|
|
113
|
+
assert.equal(extractionCalls.length, 2, "only the failed attempt and successful retry call the extractor");
|
|
114
|
+
assert.equal(commits, 1, "processed replay must not emit a second commit");
|
|
115
|
+
} finally {
|
|
116
|
+
storage.writeEntity = writeEntity;
|
|
117
|
+
try {
|
|
118
|
+
await orchestrator.destroy();
|
|
119
|
+
} finally {
|
|
120
|
+
resetExtractionLlmHealthForTests();
|
|
121
|
+
await cleanupDir(memoryDir);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
test("#3231: entity failure after a committed fact still runs deferred post-write maintenance", async () => {
|
|
128
|
+
// Partial-success path: fact (preference) is durable, later writeEntity throws.
|
|
129
|
+
// Batch-end finalization in persistExtraction (hash flush, behavior ledger,
|
|
130
|
+
// harmonic construction, temporal/tag index) and runExtraction's
|
|
131
|
+
// requestQmdMaintenance must still cover the earlier durable write. Retry
|
|
132
|
+
// exact-dedupes the fact (`continue`), so those effects are not re-queued.
|
|
133
|
+
const memoryDir = await mkTempMemoryDir("entity-write-failure-maintenance");
|
|
134
|
+
const preference =
|
|
135
|
+
"Prefers terse queue names for background jobs in Project Alpha.";
|
|
136
|
+
const config = makeHarmonicLifecycleConfig(memoryDir, {
|
|
137
|
+
chunkingEnabled: false,
|
|
138
|
+
multiGraphMemoryEnabled: false,
|
|
139
|
+
factDeduplicationEnabled: true,
|
|
140
|
+
extractionMinImportanceLevel: "trivial",
|
|
141
|
+
entityActivityLogEnabled: false,
|
|
142
|
+
queryAwareIndexingEnabled: true,
|
|
143
|
+
});
|
|
144
|
+
const orchestrator = new Orchestrator(config);
|
|
145
|
+
const storage = await orchestrator.getStorageForNamespace();
|
|
146
|
+
const writeEntity = storage.writeEntity.bind(storage);
|
|
147
|
+
const temporalIdBatches: string[][] = [];
|
|
148
|
+
// Bracket access is TypeScript's typed escape hatch for the private method; the spy only wraps and restores it.
|
|
149
|
+
const originalTemporal = orchestrator["updateTemporalTagIndexes"].bind(orchestrator);
|
|
150
|
+
orchestrator["updateTemporalTagIndexes"] = async (target, persistedIds) => {
|
|
151
|
+
temporalIdBatches.push([...persistedIds]);
|
|
152
|
+
return originalTemporal(target, persistedIds);
|
|
153
|
+
};
|
|
154
|
+
let qmdMaintenanceCalls = 0;
|
|
155
|
+
const originalQmd = orchestrator.requestQmdMaintenance.bind(orchestrator);
|
|
156
|
+
orchestrator.requestQmdMaintenance = () => {
|
|
157
|
+
qmdMaintenanceCalls += 1;
|
|
158
|
+
originalQmd();
|
|
159
|
+
};
|
|
160
|
+
try {
|
|
161
|
+
await storage.ensureDirectories();
|
|
162
|
+
await orchestrator.buffer.load();
|
|
163
|
+
const bufferKey = "entity-write-failure-maintenance";
|
|
164
|
+
await orchestrator.buffer.addTurn(bufferKey, {
|
|
165
|
+
role: "user",
|
|
166
|
+
content: preference,
|
|
167
|
+
sessionKey: bufferKey,
|
|
168
|
+
timestamp: "2026-10-01T12:00:00.000Z",
|
|
169
|
+
sourceValidAt: "2026-10-01T12:00:00.000Z",
|
|
170
|
+
persistProcessedFingerprint: true,
|
|
171
|
+
});
|
|
172
|
+
await orchestrator.buffer.flushPendingSave({ throwOnFailure: true });
|
|
173
|
+
const turns = orchestrator.buffer.getTurns(bufferKey);
|
|
174
|
+
assert.equal(turns.length, 1);
|
|
175
|
+
stubExtraction(orchestrator, (): ExtractionResult => ({
|
|
176
|
+
facts: [
|
|
177
|
+
{
|
|
178
|
+
content: preference,
|
|
179
|
+
category: "preference",
|
|
180
|
+
confidence: 0.95,
|
|
181
|
+
tags: ["queue-naming"],
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
entities: [{ name: "Alpha", type: "project", facts: ["Uses a queue"] }],
|
|
185
|
+
questions: [],
|
|
186
|
+
profileUpdates: [],
|
|
187
|
+
}));
|
|
188
|
+
const writeFailure = Object.assign(new Error("synthetic entity write denied"), {
|
|
189
|
+
code: "EACCES",
|
|
190
|
+
});
|
|
191
|
+
storage.writeEntity = async () => {
|
|
192
|
+
throw writeFailure;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
await assert.rejects(
|
|
196
|
+
orchestrator.runExtraction(turns, { bufferKey }),
|
|
197
|
+
(error: Error) => error === writeFailure,
|
|
198
|
+
"entity I/O failure must still reach the caller",
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const facts = (await storage.readAllMemories()).filter(
|
|
202
|
+
(m) => m.frontmatter.category === "preference",
|
|
203
|
+
);
|
|
204
|
+
assert.equal(facts.length, 1, "the preference fact must remain durable");
|
|
205
|
+
const factId = facts[0]!.frontmatter.id;
|
|
206
|
+
const hashes = new ContentHashIndex(path.join(storage.dir, "state"));
|
|
207
|
+
await hashes.load();
|
|
208
|
+
assert.equal(hashes.has(preference), true, "deferred fact hash must flush before rejection");
|
|
209
|
+
|
|
210
|
+
const signals = await storage.readBehaviorSignals(50);
|
|
211
|
+
assert.ok(
|
|
212
|
+
signals.some(
|
|
213
|
+
(s) => s.memoryId === factId && s.signalType === "preference_affinity",
|
|
214
|
+
),
|
|
215
|
+
"batch-end behavior-signal append must cover the committed preference before rethrow",
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const harmonic = await harmonicNodeStoreStatus(memoryDir);
|
|
219
|
+
assert.equal(harmonic.nodes.invalid, 0);
|
|
220
|
+
assert.ok(
|
|
221
|
+
harmonic.nodes.valid > 0 && (harmonic.latestNode?.sourceMemoryIds ?? []).includes(factId),
|
|
222
|
+
"batch-end harmonic construction must cover the committed fact before rethrow",
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
assert.ok(
|
|
226
|
+
temporalIdBatches.some((ids) => ids.includes(factId)),
|
|
227
|
+
"batch-end temporal/tag index update must include the committed fact before rethrow",
|
|
228
|
+
);
|
|
229
|
+
assert.ok(
|
|
230
|
+
qmdMaintenanceCalls > 0,
|
|
231
|
+
"partial durable writes must still schedule QMD reindex before the entity error propagates",
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// Retry exact-dedupes the preference (`continue`), so missing deferred
|
|
235
|
+
// effects are not re-queued. Prove the failure-path finalization is the
|
|
236
|
+
// only chance they have to land.
|
|
237
|
+
storage.writeEntity = writeEntity;
|
|
238
|
+
const retried = await orchestrator.runExtraction(turns, { bufferKey });
|
|
239
|
+
assert.equal(retried.status, "completed");
|
|
240
|
+
assert.equal(
|
|
241
|
+
(await storage.readAllMemories()).filter((m) => m.frontmatter.category === "preference").length,
|
|
242
|
+
1,
|
|
243
|
+
"retry must not duplicate the already-committed preference",
|
|
244
|
+
);
|
|
245
|
+
assert.ok(
|
|
246
|
+
(await storage.readBehaviorSignals(50)).some(
|
|
247
|
+
(s) => s.memoryId === factId && s.signalType === "preference_affinity",
|
|
248
|
+
),
|
|
249
|
+
"behavior signals must survive retry without relying on re-queue from the deduped fact",
|
|
250
|
+
);
|
|
251
|
+
assert.ok(
|
|
252
|
+
((await harmonicNodeStoreStatus(memoryDir)).latestNode?.sourceMemoryIds ?? []).includes(factId),
|
|
253
|
+
"harmonic records must survive retry without relying on re-queue from the deduped fact",
|
|
254
|
+
);
|
|
255
|
+
} finally {
|
|
256
|
+
storage.writeEntity = writeEntity;
|
|
257
|
+
orchestrator.requestQmdMaintenance = originalQmd;
|
|
258
|
+
orchestrator["updateTemporalTagIndexes"] = originalTemporal;
|
|
259
|
+
try {
|
|
260
|
+
await orchestrator.destroy();
|
|
261
|
+
} finally {
|
|
262
|
+
resetExtractionLlmHealthForTests();
|
|
263
|
+
await cleanupDir(memoryDir);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("#3231 P2: a post-write contradiction-resolve failure still finalizes the durable fact", async () => {
|
|
269
|
+
// The fact is durable (writeSealedMemory returned) when
|
|
270
|
+
// applyDeferredContradictionResolve rejects. Its behavior signal, harmonic
|
|
271
|
+
// node, temporal/tag index entry and QMD request must not depend on code
|
|
272
|
+
// that runs after that catch; retry exact-dedupes the fact.
|
|
273
|
+
const memoryDir = await mkTempMemoryDir("contradiction-resolve-failure-maintenance");
|
|
274
|
+
const preference = "Prefers terse queue names for background jobs in Project Beta.";
|
|
275
|
+
const config = makeHarmonicLifecycleConfig(memoryDir, {
|
|
276
|
+
chunkingEnabled: false,
|
|
277
|
+
multiGraphMemoryEnabled: false,
|
|
278
|
+
factDeduplicationEnabled: true,
|
|
279
|
+
extractionMinImportanceLevel: "trivial",
|
|
280
|
+
entityActivityLogEnabled: false,
|
|
281
|
+
queryAwareIndexingEnabled: true,
|
|
282
|
+
});
|
|
283
|
+
const orchestrator = new Orchestrator(config);
|
|
284
|
+
const storage = await orchestrator.getStorageForNamespace();
|
|
285
|
+
const temporalIdBatches: string[][] = [];
|
|
286
|
+
const originalTemporal = orchestrator["updateTemporalTagIndexes"].bind(orchestrator);
|
|
287
|
+
orchestrator["updateTemporalTagIndexes"] = async (target, persistedIds) => {
|
|
288
|
+
temporalIdBatches.push([...persistedIds]);
|
|
289
|
+
return originalTemporal(target, persistedIds);
|
|
290
|
+
};
|
|
291
|
+
let qmdMaintenanceCalls = 0;
|
|
292
|
+
const originalQmd = orchestrator.requestQmdMaintenance.bind(orchestrator);
|
|
293
|
+
orchestrator.requestQmdMaintenance = () => {
|
|
294
|
+
qmdMaintenanceCalls += 1;
|
|
295
|
+
originalQmd();
|
|
296
|
+
};
|
|
297
|
+
const originalResolve = orchestrator["applyDeferredContradictionResolve"].bind(orchestrator);
|
|
298
|
+
const resolveFailure = new Error("synthetic contradiction resolve failure");
|
|
299
|
+
orchestrator["applyDeferredContradictionResolve"] = async () => {
|
|
300
|
+
throw resolveFailure;
|
|
301
|
+
};
|
|
302
|
+
try {
|
|
303
|
+
await storage.ensureDirectories();
|
|
304
|
+
await orchestrator.buffer.load();
|
|
305
|
+
const bufferKey = "contradiction-resolve-failure-maintenance";
|
|
306
|
+
await orchestrator.buffer.addTurn(bufferKey, {
|
|
307
|
+
role: "user",
|
|
308
|
+
content: preference,
|
|
309
|
+
sessionKey: bufferKey,
|
|
310
|
+
timestamp: "2026-10-01T12:00:00.000Z",
|
|
311
|
+
sourceValidAt: "2026-10-01T12:00:00.000Z",
|
|
312
|
+
persistProcessedFingerprint: true,
|
|
313
|
+
});
|
|
314
|
+
await orchestrator.buffer.flushPendingSave({ throwOnFailure: true });
|
|
315
|
+
const turns = orchestrator.buffer.getTurns(bufferKey);
|
|
316
|
+
stubExtraction(orchestrator, (): ExtractionResult => ({
|
|
317
|
+
facts: [
|
|
318
|
+
{
|
|
319
|
+
content: preference,
|
|
320
|
+
category: "preference",
|
|
321
|
+
confidence: 0.95,
|
|
322
|
+
tags: ["queue-naming"],
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
entities: [],
|
|
326
|
+
questions: [],
|
|
327
|
+
profileUpdates: [],
|
|
328
|
+
}));
|
|
329
|
+
|
|
330
|
+
await assert.rejects(
|
|
331
|
+
orchestrator.runExtraction(turns, { bufferKey }),
|
|
332
|
+
(error: Error) => error === resolveFailure,
|
|
333
|
+
"the post-write failure must still reach the caller",
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const facts = (await storage.readAllMemories()).filter(
|
|
337
|
+
(m) => m.frontmatter.category === "preference",
|
|
338
|
+
);
|
|
339
|
+
assert.equal(facts.length, 1, "the preference fact must remain durable");
|
|
340
|
+
const factId = facts[0]!.frontmatter.id;
|
|
341
|
+
assert.ok(
|
|
342
|
+
(await storage.readBehaviorSignals(50)).some(
|
|
343
|
+
(s) => s.memoryId === factId && s.signalType === "preference_affinity",
|
|
344
|
+
),
|
|
345
|
+
"the durable fact's behavior signal must land before the rethrow",
|
|
346
|
+
);
|
|
347
|
+
assert.ok(
|
|
348
|
+
((await harmonicNodeStoreStatus(memoryDir)).latestNode?.sourceMemoryIds ?? []).includes(factId),
|
|
349
|
+
"the durable fact's harmonic node must land before the rethrow",
|
|
350
|
+
);
|
|
351
|
+
assert.ok(
|
|
352
|
+
temporalIdBatches.some((ids) => ids.includes(factId)),
|
|
353
|
+
"the durable fact's temporal/tag index update must run before the rethrow",
|
|
354
|
+
);
|
|
355
|
+
assert.ok(
|
|
356
|
+
qmdMaintenanceCalls > 0,
|
|
357
|
+
"a durable fact that is the only write must still schedule QMD reindex",
|
|
358
|
+
);
|
|
359
|
+
} finally {
|
|
360
|
+
orchestrator["applyDeferredContradictionResolve"] = originalResolve;
|
|
361
|
+
orchestrator.requestQmdMaintenance = originalQmd;
|
|
362
|
+
orchestrator["updateTemporalTagIndexes"] = originalTemporal;
|
|
363
|
+
try {
|
|
364
|
+
await orchestrator.destroy();
|
|
365
|
+
} finally {
|
|
366
|
+
resetExtractionLlmHealthForTests();
|
|
367
|
+
await cleanupDir(memoryDir);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
});
|
package/src/orchestrator.ts
CHANGED
|
@@ -865,6 +865,7 @@ export class Orchestrator {
|
|
|
865
865
|
invalidateGraphEdgeCache: (storage) => this.graphIndexFor(storage).invalidateEdgeCache(),
|
|
866
866
|
updateTemporalTagIndexes: (storage, persistedIds) =>
|
|
867
867
|
this.updateTemporalTagIndexes(storage, persistedIds),
|
|
868
|
+
requestQmdMaintenance: () => this.requestQmdMaintenance(),
|
|
868
869
|
});
|
|
869
870
|
}
|
|
870
871
|
return this._extractionPersistCoordinator;
|