@remnic/core 9.3.651 → 9.3.652
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 +9 -9
- package/dist/access-http.d.ts +3 -2
- package/dist/access-http.js +10 -10
- package/dist/access-mcp.d.ts +3 -2
- package/dist/access-mcp.js +9 -9
- package/dist/{access-service-DIZRHQ7Q.d.ts → access-service-CdJFd3_b.d.ts} +23 -2
- package/dist/access-service.d.ts +3 -2
- package/dist/access-service.js +8 -8
- package/dist/bootstrap.d.ts +2 -1
- package/dist/{chunk-QT4THOLT.js → chunk-2DGQLOOM.js} +1 -1
- package/dist/chunk-2DGQLOOM.js.map +1 -0
- package/dist/{chunk-SLYD3AH4.js → chunk-5V3TAB7D.js} +176 -4
- package/dist/chunk-5V3TAB7D.js.map +1 -0
- package/dist/{chunk-FOVPSMGI.js → chunk-7WEB3FLJ.js} +2 -2
- package/dist/{chunk-WJK75OCH.js → chunk-GI45G4BK.js} +2 -2
- package/dist/{chunk-76QTEJ2Q.js → chunk-JBHXMCYN.js} +2 -2
- package/dist/{chunk-4PTKFBST.js → chunk-JVRPJ7D4.js} +126 -26
- package/dist/chunk-JVRPJ7D4.js.map +1 -0
- package/dist/{chunk-TQUWNX7C.js → chunk-JX2RINDR.js} +2 -2
- package/dist/{chunk-RSS2KWN6.js → chunk-MGGNV3H2.js} +2 -2
- package/dist/{chunk-I4COC5XW.js → chunk-PYWNNF2I.js} +47 -9
- package/dist/chunk-PYWNNF2I.js.map +1 -0
- package/dist/{chunk-5WSDHTBO.js → chunk-TCX4WLKK.js} +7 -4
- package/dist/chunk-TCX4WLKK.js.map +1 -0
- package/dist/{chunk-RKN5J4RO.js → chunk-WSFNYPAT.js} +6 -6
- package/dist/{chunk-LFTLXOFX.js → chunk-WTI35CVJ.js} +2 -2
- package/dist/{chunk-6UKL6IXM.js → chunk-YM3LR4LS.js} +5 -5
- package/dist/{chunk-MF32AL7N.js → chunk-YOVKPOMD.js} +3 -3
- package/dist/{cli-BG4ybtJr.d.ts → cli-DDo7Qgs-.d.ts} +2 -2
- package/dist/cli.d.ts +4 -3
- package/dist/cli.js +13 -13
- package/dist/explicit-capture.d.ts +2 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +14 -14
- package/dist/mcp-memory-inspector-app.d.ts +3 -2
- package/dist/namespaces/migrate.js +8 -8
- package/dist/namespaces/search.d.ts +18 -1
- package/dist/namespaces/search.js +7 -7
- package/dist/operator-toolkit.js +9 -9
- package/dist/{orchestrator-CX-oqwJq.d.ts → orchestrator-8fTZsa0y.d.ts} +2 -0
- package/dist/orchestrator.d.ts +2 -1
- package/dist/orchestrator.js +8 -8
- package/dist/qmd.d.ts +2 -1
- package/dist/qmd.js +2 -2
- package/dist/schemas.d.ts +22 -22
- package/dist/search/factory.js +6 -6
- package/dist/search/index.js +6 -6
- package/dist/search/lancedb-backend.js +2 -2
- package/dist/search/meilisearch-backend.js +2 -2
- package/dist/search/orama-backend.js +2 -2
- package/dist/search/port.d.ts +6 -0
- package/dist/search/port.js +1 -1
- package/dist/transfer/types.d.ts +12 -12
- package/package.json +1 -1
- package/src/access-service-health.test.ts +402 -0
- package/src/access-service.ts +274 -2
- package/src/namespaces/search.test.ts +258 -3
- package/src/namespaces/search.ts +184 -30
- package/src/orchestrator.ts +11 -1
- package/src/qmd.test.ts +102 -0
- package/src/qmd.ts +54 -7
- package/src/search/port.ts +6 -0
- package/dist/chunk-4PTKFBST.js.map +0 -1
- package/dist/chunk-5WSDHTBO.js.map +0 -1
- package/dist/chunk-I4COC5XW.js.map +0 -1
- package/dist/chunk-QT4THOLT.js.map +0 -1
- package/dist/chunk-SLYD3AH4.js.map +0 -1
- /package/dist/{chunk-FOVPSMGI.js.map → chunk-7WEB3FLJ.js.map} +0 -0
- /package/dist/{chunk-WJK75OCH.js.map → chunk-GI45G4BK.js.map} +0 -0
- /package/dist/{chunk-76QTEJ2Q.js.map → chunk-JBHXMCYN.js.map} +0 -0
- /package/dist/{chunk-TQUWNX7C.js.map → chunk-JX2RINDR.js.map} +0 -0
- /package/dist/{chunk-RSS2KWN6.js.map → chunk-MGGNV3H2.js.map} +0 -0
- /package/dist/{chunk-RKN5J4RO.js.map → chunk-WSFNYPAT.js.map} +0 -0
- /package/dist/{chunk-LFTLXOFX.js.map → chunk-WTI35CVJ.js.map} +0 -0
- /package/dist/{chunk-6UKL6IXM.js.map → chunk-YM3LR4LS.js.map} +0 -0
- /package/dist/{chunk-MF32AL7N.js.map → chunk-YOVKPOMD.js.map} +0 -0
|
@@ -6,17 +6,22 @@ import type {
|
|
|
6
6
|
SearchExecutionOptions,
|
|
7
7
|
SearchQueryOptions,
|
|
8
8
|
} from "../search/port.js";
|
|
9
|
+
import type { QmdCapabilities, QmdVersionStatus } from "../qmd.js";
|
|
9
10
|
import type { PluginConfig, QmdSearchResult } from "../types.js";
|
|
10
11
|
|
|
11
12
|
type CollectionState = "present" | "missing" | "unknown" | "skipped";
|
|
12
13
|
|
|
13
14
|
class FakeBackend implements SearchBackend {
|
|
14
15
|
updates = 0;
|
|
16
|
+
disposed = 0;
|
|
17
|
+
available = true;
|
|
15
18
|
calls: Array<{
|
|
16
19
|
method: string;
|
|
17
20
|
collection: string | undefined;
|
|
18
21
|
maxResults: number | undefined;
|
|
19
22
|
}> = [];
|
|
23
|
+
availabilitySignals: Array<AbortSignal | undefined> = [];
|
|
24
|
+
probeCalls = 0;
|
|
20
25
|
ensureSignals: Array<AbortSignal | undefined> = [];
|
|
21
26
|
ensureCollections: Array<string | undefined> = [];
|
|
22
27
|
checkSignals: Array<AbortSignal | undefined> = [];
|
|
@@ -29,6 +34,11 @@ class FakeBackend implements SearchBackend {
|
|
|
29
34
|
check?: CollectionState;
|
|
30
35
|
ensure?: CollectionState;
|
|
31
36
|
} = {},
|
|
37
|
+
private readonly daemonMode = false,
|
|
38
|
+
private readonly diagnostics: {
|
|
39
|
+
debugStatus?: string;
|
|
40
|
+
versionStatus?: QmdVersionStatus;
|
|
41
|
+
} = {},
|
|
32
42
|
) {}
|
|
33
43
|
|
|
34
44
|
private limitedResults(maxResults: number | undefined): QmdSearchResult[] {
|
|
@@ -38,15 +48,33 @@ class FakeBackend implements SearchBackend {
|
|
|
38
48
|
}
|
|
39
49
|
|
|
40
50
|
async probe(): Promise<boolean> {
|
|
41
|
-
|
|
51
|
+
this.probeCalls += 1;
|
|
52
|
+
return this.available;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async checkAvailability(execution?: SearchExecutionOptions): Promise<boolean> {
|
|
56
|
+
this.availabilitySignals.push(execution?.signal);
|
|
57
|
+
return this.available;
|
|
42
58
|
}
|
|
43
59
|
|
|
44
60
|
isAvailable(): boolean {
|
|
45
|
-
return
|
|
61
|
+
return this.available;
|
|
46
62
|
}
|
|
47
63
|
|
|
48
64
|
debugStatus(): string {
|
|
49
|
-
return "fake";
|
|
65
|
+
return this.diagnostics.debugStatus ?? "fake";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
isDaemonMode(): boolean {
|
|
69
|
+
return this.daemonMode;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getVersionStatus(): QmdVersionStatus | null {
|
|
73
|
+
return this.diagnostics.versionStatus ?? null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async dispose(): Promise<void> {
|
|
77
|
+
this.disposed += 1;
|
|
50
78
|
}
|
|
51
79
|
|
|
52
80
|
async search(
|
|
@@ -154,6 +182,41 @@ function config(): PluginConfig {
|
|
|
154
182
|
} as PluginConfig;
|
|
155
183
|
}
|
|
156
184
|
|
|
185
|
+
function qmdCapabilities(enabled: boolean): QmdCapabilities {
|
|
186
|
+
return {
|
|
187
|
+
version: enabled ? "2.5.3" : null,
|
|
188
|
+
parsedVersion: enabled ? [2, 5, 3] : null,
|
|
189
|
+
stableSdk: enabled,
|
|
190
|
+
unifiedSearch: enabled,
|
|
191
|
+
getDocumentBody: enabled,
|
|
192
|
+
maintenanceApi: enabled,
|
|
193
|
+
legacySkillInstall: enabled,
|
|
194
|
+
intentHints: enabled,
|
|
195
|
+
explainTraces: enabled,
|
|
196
|
+
candidateLimit: enabled,
|
|
197
|
+
v2McpQueryTool: enabled,
|
|
198
|
+
structuredSearches: enabled,
|
|
199
|
+
queryRerankToggle: enabled,
|
|
200
|
+
chunkStrategy: enabled,
|
|
201
|
+
qmdBench: enabled,
|
|
202
|
+
perCollectionModels: enabled,
|
|
203
|
+
jsonLineNumbers: enabled,
|
|
204
|
+
editorLinks: enabled,
|
|
205
|
+
doctor: enabled,
|
|
206
|
+
versionedSkills: enabled,
|
|
207
|
+
absoluteSnippetLines: enabled,
|
|
208
|
+
fullQueryOutput: enabled,
|
|
209
|
+
forceCpu: enabled,
|
|
210
|
+
gpuBackendOverride: enabled,
|
|
211
|
+
embedParallelism: enabled,
|
|
212
|
+
modelEnvConsistency: enabled,
|
|
213
|
+
scopedEmbed: enabled,
|
|
214
|
+
safeStatusDeviceProbe: enabled,
|
|
215
|
+
mcpIndexSelection: enabled,
|
|
216
|
+
outputFormatFlag: enabled,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
157
220
|
test("updateNamespaces runs a global-update backend only once", async () => {
|
|
158
221
|
const created: FakeBackend[] = [];
|
|
159
222
|
const router = new NamespaceSearchRouter(
|
|
@@ -304,6 +367,198 @@ test("legacy default namespace root fail-opens missing guarded collections", asy
|
|
|
304
367
|
assert.deepEqual(backend.ensureCollections, []);
|
|
305
368
|
});
|
|
306
369
|
|
|
370
|
+
test("healthForNamespace checks namespace collection without auto-creating or caching state", async () => {
|
|
371
|
+
const created: FakeBackend[] = [];
|
|
372
|
+
const router = new NamespaceSearchRouter(
|
|
373
|
+
config(),
|
|
374
|
+
{ storageFor: async (namespace: string) => ({ dir: `/tmp/remnic/${namespace}` }) },
|
|
375
|
+
() => {
|
|
376
|
+
const backend = new FakeBackend(false, [], created.length === 0
|
|
377
|
+
? { check: "missing" }
|
|
378
|
+
: { ensure: "present" });
|
|
379
|
+
created.push(backend);
|
|
380
|
+
return backend;
|
|
381
|
+
},
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
const health = await router.healthForNamespace("shared");
|
|
385
|
+
|
|
386
|
+
assert.equal(health.collectionState, "missing");
|
|
387
|
+
assert.equal(health.collection, "openclaw-engram--ns-736861726564");
|
|
388
|
+
assert.deepEqual(created[0]?.checkCollections, ["openclaw-engram--ns-736861726564"]);
|
|
389
|
+
assert.deepEqual(created[0]?.ensureCollections, []);
|
|
390
|
+
assert.equal(created[0]?.probeCalls, 0);
|
|
391
|
+
assert.equal(created[0]?.availabilitySignals.length, 1);
|
|
392
|
+
assert.equal(created[0]?.disposed, 1);
|
|
393
|
+
|
|
394
|
+
const ensured = await router.ensureNamespaceCollection("shared");
|
|
395
|
+
|
|
396
|
+
assert.equal(ensured, "present");
|
|
397
|
+
assert.equal(created.length, 2);
|
|
398
|
+
assert.deepEqual(created[1]?.ensureCollections, ["openclaw-engram--ns-736861726564"]);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("healthForNamespace reports daemon mode from live cached namespace backend", async () => {
|
|
402
|
+
const created: FakeBackend[] = [];
|
|
403
|
+
const router = new NamespaceSearchRouter(
|
|
404
|
+
config(),
|
|
405
|
+
{ storageFor: async (namespace: string) => ({ dir: `/tmp/remnic/${namespace}` }) },
|
|
406
|
+
() => {
|
|
407
|
+
const backend = created.length === 0
|
|
408
|
+
? new FakeBackend(false, [
|
|
409
|
+
{
|
|
410
|
+
path: "facts/a.md",
|
|
411
|
+
docid: "a",
|
|
412
|
+
score: 1,
|
|
413
|
+
snippet: "a",
|
|
414
|
+
},
|
|
415
|
+
], { ensure: "present" }, true, {
|
|
416
|
+
debugStatus: "live-daemon",
|
|
417
|
+
versionStatus: {
|
|
418
|
+
installedVersion: "qmd 2.5.3",
|
|
419
|
+
supportedVersion: "2.5.3",
|
|
420
|
+
supported: true,
|
|
421
|
+
newerThanSupported: false,
|
|
422
|
+
upgradeAvailable: false,
|
|
423
|
+
capabilities: qmdCapabilities(true),
|
|
424
|
+
},
|
|
425
|
+
})
|
|
426
|
+
: new FakeBackend(false, [], { check: "present" }, false, {
|
|
427
|
+
debugStatus: "probe-unavailable",
|
|
428
|
+
versionStatus: {
|
|
429
|
+
installedVersion: null,
|
|
430
|
+
supportedVersion: "2.5.3",
|
|
431
|
+
supported: false,
|
|
432
|
+
newerThanSupported: false,
|
|
433
|
+
upgradeAvailable: false,
|
|
434
|
+
capabilities: qmdCapabilities(false),
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
if (created.length === 1) {
|
|
438
|
+
backend.available = false;
|
|
439
|
+
}
|
|
440
|
+
created.push(backend);
|
|
441
|
+
return backend;
|
|
442
|
+
},
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
await router.searchAcrossNamespaces({
|
|
446
|
+
query: "a",
|
|
447
|
+
namespaces: ["shared"],
|
|
448
|
+
maxResults: 1,
|
|
449
|
+
});
|
|
450
|
+
const health = await router.healthForNamespace("shared");
|
|
451
|
+
|
|
452
|
+
assert.equal(health.available, true);
|
|
453
|
+
assert.equal(health.daemonMode, true);
|
|
454
|
+
assert.equal(health.debugStatus, "live-daemon");
|
|
455
|
+
assert.equal(health.installedVersion, "qmd 2.5.3");
|
|
456
|
+
assert.equal(health.supportedVersion, "2.5.3");
|
|
457
|
+
assert.equal(health.supported, true);
|
|
458
|
+
assert.equal(health.upgradeAvailable, false);
|
|
459
|
+
assert.equal(health.doctorAvailable, true);
|
|
460
|
+
assert.equal(health.collectionState, "unknown");
|
|
461
|
+
assert.equal(created.length, 2);
|
|
462
|
+
assert.equal(created[0]?.disposed, 0);
|
|
463
|
+
assert.equal(created[1]?.disposed, 1);
|
|
464
|
+
assert.deepEqual(created[1]?.checkCollections, []);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test("healthForNamespace preserves cached missing collection state", async () => {
|
|
468
|
+
const created: FakeBackend[] = [];
|
|
469
|
+
const router = new NamespaceSearchRouter(
|
|
470
|
+
config(),
|
|
471
|
+
{ storageFor: async (namespace: string) => ({ dir: `/tmp/remnic/${namespace}` }) },
|
|
472
|
+
() => {
|
|
473
|
+
const backend = created.length === 0
|
|
474
|
+
? new FakeBackend(false, [], { ensure: "missing" }, true)
|
|
475
|
+
: new FakeBackend(false, [], { check: "present" }, false);
|
|
476
|
+
created.push(backend);
|
|
477
|
+
return backend;
|
|
478
|
+
},
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
assert.deepEqual(
|
|
482
|
+
await router.searchAcrossNamespaces({
|
|
483
|
+
query: "a",
|
|
484
|
+
namespaces: ["shared"],
|
|
485
|
+
maxResults: 1,
|
|
486
|
+
}),
|
|
487
|
+
[],
|
|
488
|
+
);
|
|
489
|
+
const health = await router.healthForNamespace("shared");
|
|
490
|
+
|
|
491
|
+
assert.equal(health.available, true);
|
|
492
|
+
assert.equal(health.daemonMode, true);
|
|
493
|
+
assert.equal(health.collectionState, "missing");
|
|
494
|
+
assert.equal(created.length, 2);
|
|
495
|
+
assert.deepEqual(created[1]?.checkCollections, ["openclaw-engram--ns-736861726564"]);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test("healthForNamespace stops waiting when namespace availability probe aborts", async () => {
|
|
499
|
+
const backend = new class extends FakeBackend {
|
|
500
|
+
override async checkAvailability(execution?: SearchExecutionOptions): Promise<boolean> {
|
|
501
|
+
this.availabilitySignals.push(execution?.signal);
|
|
502
|
+
return await new Promise<boolean>(() => {});
|
|
503
|
+
}
|
|
504
|
+
}(false);
|
|
505
|
+
const router = new NamespaceSearchRouter(
|
|
506
|
+
config(),
|
|
507
|
+
{ storageFor: async (namespace: string) => ({ dir: `/tmp/remnic/${namespace}` }) },
|
|
508
|
+
() => backend,
|
|
509
|
+
);
|
|
510
|
+
const controller = new AbortController();
|
|
511
|
+
controller.abort();
|
|
512
|
+
|
|
513
|
+
const health = await router.healthForNamespace("shared", {
|
|
514
|
+
signal: controller.signal,
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
assert.equal(health.available, false);
|
|
518
|
+
assert.equal(health.collectionState, "unknown");
|
|
519
|
+
assert.equal(backend.probeCalls, 0);
|
|
520
|
+
assert.equal(backend.availabilitySignals[0], controller.signal);
|
|
521
|
+
assert.deepEqual(backend.checkCollections, []);
|
|
522
|
+
assert.deepEqual(backend.ensureCollections, []);
|
|
523
|
+
assert.equal(backend.disposed, 1);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test("ensureNamespaceCollection does not cache aborted namespace backend probes", async () => {
|
|
527
|
+
const created: FakeBackend[] = [];
|
|
528
|
+
const router = new NamespaceSearchRouter(
|
|
529
|
+
config(),
|
|
530
|
+
{ storageFor: async (namespace: string) => ({ dir: `/tmp/remnic/${namespace}` }) },
|
|
531
|
+
() => {
|
|
532
|
+
const backend = created.length === 0
|
|
533
|
+
? new class extends FakeBackend {
|
|
534
|
+
override async probe(): Promise<boolean> {
|
|
535
|
+
return await new Promise<boolean>(() => {});
|
|
536
|
+
}
|
|
537
|
+
}(false)
|
|
538
|
+
: new FakeBackend(false, [], { ensure: "present" });
|
|
539
|
+
created.push(backend);
|
|
540
|
+
return backend;
|
|
541
|
+
},
|
|
542
|
+
);
|
|
543
|
+
const controller = new AbortController();
|
|
544
|
+
controller.abort();
|
|
545
|
+
|
|
546
|
+
await assert.rejects(
|
|
547
|
+
() => router.ensureNamespaceCollection("shared", { signal: controller.signal }),
|
|
548
|
+
/operation aborted/,
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
assert.equal(created[0]?.disposed, 1);
|
|
552
|
+
assert.deepEqual(created[0]?.checkCollections, []);
|
|
553
|
+
assert.deepEqual(created[0]?.ensureCollections, []);
|
|
554
|
+
|
|
555
|
+
const ensured = await router.ensureNamespaceCollection("shared");
|
|
556
|
+
|
|
557
|
+
assert.equal(ensured, "present");
|
|
558
|
+
assert.equal(created.length, 2);
|
|
559
|
+
assert.deepEqual(created[1]?.ensureCollections, ["openclaw-engram--ns-736861726564"]);
|
|
560
|
+
});
|
|
561
|
+
|
|
307
562
|
test("legacy default namespace root filters nested namespace search results", async () => {
|
|
308
563
|
const router = new NamespaceSearchRouter(
|
|
309
564
|
config(),
|
package/src/namespaces/search.ts
CHANGED
|
@@ -44,12 +44,32 @@ type NamespaceBackendRecord = {
|
|
|
44
44
|
filtersNestedNamespaces: boolean;
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
-
type CollectionState = "present" | "missing" | "unknown" | "skipped";
|
|
47
|
+
export type CollectionState = "present" | "missing" | "unknown" | "skipped";
|
|
48
|
+
|
|
49
|
+
export interface NamespaceSearchHealth {
|
|
50
|
+
collection: string;
|
|
51
|
+
memoryDir: string;
|
|
52
|
+
available: boolean;
|
|
53
|
+
collectionState: CollectionState;
|
|
54
|
+
debugStatus: string;
|
|
55
|
+
installedVersion: string | null;
|
|
56
|
+
supportedVersion: string | null;
|
|
57
|
+
supported: boolean | null;
|
|
58
|
+
upgradeAvailable: boolean | null;
|
|
59
|
+
doctorAvailable: boolean | null;
|
|
60
|
+
daemonMode: boolean | null;
|
|
61
|
+
}
|
|
48
62
|
|
|
49
63
|
type NamespaceScopedSearchConfig = PluginConfig & {
|
|
50
64
|
hostEmbeddingProviderScope?: string;
|
|
51
65
|
};
|
|
52
66
|
|
|
67
|
+
type BackendRecordOptions = {
|
|
68
|
+
autoCreateCollection: boolean;
|
|
69
|
+
abortAsUnavailable: boolean;
|
|
70
|
+
failOpenMissingGuardedCollection: boolean;
|
|
71
|
+
};
|
|
72
|
+
|
|
53
73
|
export class NamespaceSearchRouter {
|
|
54
74
|
private readonly cache = new Map<string, Promise<NamespaceBackendRecord>>();
|
|
55
75
|
|
|
@@ -184,6 +204,67 @@ export class NamespaceSearchRouter {
|
|
|
184
204
|
return record.collectionState;
|
|
185
205
|
}
|
|
186
206
|
|
|
207
|
+
async healthForNamespace(
|
|
208
|
+
namespace: string,
|
|
209
|
+
execution?: SearchExecutionOptions,
|
|
210
|
+
): Promise<NamespaceSearchHealth> {
|
|
211
|
+
const key = namespace.trim() || this.config.defaultNamespace;
|
|
212
|
+
const record = await this.createBackendRecordFor(
|
|
213
|
+
key,
|
|
214
|
+
execution,
|
|
215
|
+
{
|
|
216
|
+
autoCreateCollection: false,
|
|
217
|
+
abortAsUnavailable: true,
|
|
218
|
+
failOpenMissingGuardedCollection: false,
|
|
219
|
+
},
|
|
220
|
+
);
|
|
221
|
+
try {
|
|
222
|
+
const liveRecord = await this.liveCachedRecordForHealth(key, record, execution);
|
|
223
|
+
const diagnosticBackend = liveRecord?.backend ?? record.backend;
|
|
224
|
+
const versionStatus =
|
|
225
|
+
"getVersionStatus" in diagnosticBackend &&
|
|
226
|
+
typeof diagnosticBackend.getVersionStatus === "function"
|
|
227
|
+
? diagnosticBackend.getVersionStatus()
|
|
228
|
+
: null;
|
|
229
|
+
const daemonMode = daemonModeForBackend(diagnosticBackend);
|
|
230
|
+
const collectionState =
|
|
231
|
+
liveRecord?.collectionState === "missing"
|
|
232
|
+
? "missing"
|
|
233
|
+
: record.collectionState;
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
collection: record.collection,
|
|
237
|
+
memoryDir: record.memoryDir,
|
|
238
|
+
available: liveRecord?.available ?? record.available,
|
|
239
|
+
collectionState,
|
|
240
|
+
debugStatus: diagnosticBackend.debugStatus(),
|
|
241
|
+
installedVersion: versionStatus?.installedVersion ?? null,
|
|
242
|
+
supportedVersion: versionStatus?.supportedVersion ?? null,
|
|
243
|
+
supported: versionStatus?.supported ?? null,
|
|
244
|
+
upgradeAvailable: versionStatus?.upgradeAvailable ?? null,
|
|
245
|
+
doctorAvailable: versionStatus?.capabilities?.doctor ?? null,
|
|
246
|
+
daemonMode,
|
|
247
|
+
};
|
|
248
|
+
} finally {
|
|
249
|
+
const dispose = (record.backend as { dispose?: () => void | Promise<void> }).dispose;
|
|
250
|
+
await dispose?.call(record.backend);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private async liveCachedRecordForHealth(
|
|
255
|
+
key: string,
|
|
256
|
+
disposableRecord: NamespaceBackendRecord,
|
|
257
|
+
execution?: SearchExecutionOptions,
|
|
258
|
+
): Promise<NamespaceBackendRecord | null> {
|
|
259
|
+
const pending = this.cache.get(key);
|
|
260
|
+
if (!pending) return null;
|
|
261
|
+
const cachedRecord = await awaitWithAbort(pending, execution?.signal).catch(() => null);
|
|
262
|
+
if (!cachedRecord) return null;
|
|
263
|
+
if (cachedRecord.collection !== disposableRecord.collection) return null;
|
|
264
|
+
if (cachedRecord.memoryDir !== disposableRecord.memoryDir) return null;
|
|
265
|
+
return cachedRecord;
|
|
266
|
+
}
|
|
267
|
+
|
|
187
268
|
/** Clear cached backend records so the next access re-probes availability. */
|
|
188
269
|
clearCache(): void {
|
|
189
270
|
this.cache.clear();
|
|
@@ -211,31 +292,69 @@ export class NamespaceSearchRouter {
|
|
|
211
292
|
const existing = this.cache.get(key);
|
|
212
293
|
if (existing) return await existing;
|
|
213
294
|
|
|
214
|
-
const pending = (
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
295
|
+
const pending = this.createBackendRecordFor(key, execution, {
|
|
296
|
+
autoCreateCollection: true,
|
|
297
|
+
abortAsUnavailable: false,
|
|
298
|
+
failOpenMissingGuardedCollection: true,
|
|
299
|
+
}).catch((error) => {
|
|
300
|
+
this.cache.delete(key);
|
|
301
|
+
throw error;
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
this.cache.set(key, pending);
|
|
305
|
+
return await pending;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private async createBackendRecordFor(
|
|
309
|
+
namespace: string,
|
|
310
|
+
execution: SearchExecutionOptions | undefined,
|
|
311
|
+
options: BackendRecordOptions,
|
|
312
|
+
): Promise<NamespaceBackendRecord> {
|
|
313
|
+
const key = namespace.trim() || this.config.defaultNamespace;
|
|
314
|
+
const storage = await this.storageRouter.storageFor(key);
|
|
315
|
+
const useLegacyDefaultCollection =
|
|
316
|
+
key === this.config.defaultNamespace && storage.dir === this.config.memoryDir;
|
|
317
|
+
const filtersNestedNamespaces =
|
|
318
|
+
this.config.namespacesEnabled === true && useLegacyDefaultCollection;
|
|
319
|
+
const rootHostEmbeddingScope =
|
|
320
|
+
(this.config as NamespaceScopedSearchConfig).hostEmbeddingProviderScope ??
|
|
321
|
+
this.config.memoryDir;
|
|
322
|
+
const scopedConfig: NamespaceScopedSearchConfig = {
|
|
323
|
+
...this.config,
|
|
324
|
+
memoryDir: storage.dir,
|
|
325
|
+
hostEmbeddingProviderScope: rootHostEmbeddingScope,
|
|
326
|
+
qmdCollection: namespaceCollectionName(this.config.qmdCollection, key, {
|
|
327
|
+
defaultNamespace: this.config.defaultNamespace,
|
|
328
|
+
useLegacyDefaultCollection,
|
|
329
|
+
}),
|
|
330
|
+
};
|
|
232
331
|
|
|
233
|
-
|
|
234
|
-
|
|
332
|
+
const backend = this.createBackend(scopedConfig);
|
|
333
|
+
try {
|
|
334
|
+
const availabilityProbe =
|
|
335
|
+
options.autoCreateCollection || typeof backend.checkAvailability !== "function"
|
|
336
|
+
? backend.probe()
|
|
337
|
+
: backend.checkAvailability({ signal: execution?.signal });
|
|
338
|
+
const available = await awaitWithAbort(availabilityProbe, execution?.signal).catch((error) => {
|
|
339
|
+
if (error instanceof NamespaceSearchAbortError && !options.abortAsUnavailable) {
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
return false;
|
|
343
|
+
});
|
|
235
344
|
const collectionState = available
|
|
236
|
-
? await
|
|
237
|
-
|
|
238
|
-
|
|
345
|
+
? await awaitWithAbort(
|
|
346
|
+
this.collectionStateForBackend(backend, storage.dir, scopedConfig.qmdCollection, {
|
|
347
|
+
autoCreate: options.autoCreateCollection,
|
|
348
|
+
failOpenMissingGuardedCollection: options.failOpenMissingGuardedCollection,
|
|
349
|
+
skipAutoCreate: filtersNestedNamespaces,
|
|
350
|
+
execution,
|
|
351
|
+
}),
|
|
352
|
+
execution?.signal,
|
|
353
|
+
).catch((error) => {
|
|
354
|
+
if (error instanceof NamespaceSearchAbortError && !options.abortAsUnavailable) {
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
return "unknown" as const;
|
|
239
358
|
})
|
|
240
359
|
: "unknown";
|
|
241
360
|
return {
|
|
@@ -246,10 +365,13 @@ export class NamespaceSearchRouter {
|
|
|
246
365
|
collectionState,
|
|
247
366
|
filtersNestedNamespaces,
|
|
248
367
|
};
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
368
|
+
} catch (error) {
|
|
369
|
+
const dispose = (backend as { dispose?: () => void | Promise<void> }).dispose;
|
|
370
|
+
if (dispose) {
|
|
371
|
+
await Promise.resolve(dispose.call(backend)).catch(() => {});
|
|
372
|
+
}
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
253
375
|
}
|
|
254
376
|
|
|
255
377
|
private async collectionStateForBackend(
|
|
@@ -257,21 +379,47 @@ export class NamespaceSearchRouter {
|
|
|
257
379
|
memoryDir: string,
|
|
258
380
|
collection: string,
|
|
259
381
|
options: {
|
|
382
|
+
autoCreate: boolean;
|
|
383
|
+
failOpenMissingGuardedCollection: boolean;
|
|
260
384
|
skipAutoCreate: boolean;
|
|
261
385
|
execution?: SearchExecutionOptions;
|
|
262
386
|
},
|
|
263
387
|
): Promise<CollectionState> {
|
|
264
|
-
if (options.skipAutoCreate) {
|
|
388
|
+
if (!options.autoCreate || options.skipAutoCreate) {
|
|
265
389
|
if (!backend.checkCollection) return "unknown";
|
|
266
390
|
const collectionState = await backend
|
|
267
391
|
.checkCollection(collection, options.execution)
|
|
268
392
|
.catch(() => "unknown" as const);
|
|
269
|
-
return collectionState === "missing"
|
|
393
|
+
return options.failOpenMissingGuardedCollection && collectionState === "missing"
|
|
394
|
+
? "unknown"
|
|
395
|
+
: collectionState;
|
|
270
396
|
}
|
|
271
397
|
return await backend.ensureCollection(memoryDir, collection, options.execution).catch(() => "unknown" as const);
|
|
272
398
|
}
|
|
273
399
|
}
|
|
274
400
|
|
|
401
|
+
class NamespaceSearchAbortError extends Error {
|
|
402
|
+
constructor() {
|
|
403
|
+
super("operation aborted");
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function awaitWithAbort<T>(operation: Promise<T>, signal?: AbortSignal): Promise<T> {
|
|
408
|
+
if (!signal) return operation;
|
|
409
|
+
if (signal.aborted) return Promise.reject(new NamespaceSearchAbortError());
|
|
410
|
+
|
|
411
|
+
return new Promise<T>((resolve, reject) => {
|
|
412
|
+
const onAbort = () => {
|
|
413
|
+
signal.removeEventListener("abort", onAbort);
|
|
414
|
+
reject(new NamespaceSearchAbortError());
|
|
415
|
+
};
|
|
416
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
417
|
+
operation.then(resolve, reject).finally(() => {
|
|
418
|
+
signal.removeEventListener("abort", onAbort);
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
275
423
|
function filterNamespaceSubtreeResults(
|
|
276
424
|
record: NamespaceBackendRecord,
|
|
277
425
|
results: QmdSearchResult[],
|
|
@@ -294,6 +442,12 @@ function backendSearchLimit(
|
|
|
294
442
|
);
|
|
295
443
|
}
|
|
296
444
|
|
|
445
|
+
function daemonModeForBackend(backend: SearchBackend): boolean | null {
|
|
446
|
+
return "isDaemonMode" in backend && typeof backend.isDaemonMode === "function"
|
|
447
|
+
? backend.isDaemonMode() === true
|
|
448
|
+
: null;
|
|
449
|
+
}
|
|
450
|
+
|
|
297
451
|
function pathIsInsideNamespaceSubtree(
|
|
298
452
|
memoryDir: string,
|
|
299
453
|
collection: string,
|
package/src/orchestrator.ts
CHANGED
|
@@ -311,7 +311,10 @@ import {
|
|
|
311
311
|
resolveCodingNamespaceOverlay,
|
|
312
312
|
} from "./coding/coding-namespace.js";
|
|
313
313
|
import type { CodingContext } from "./types.js";
|
|
314
|
-
import {
|
|
314
|
+
import {
|
|
315
|
+
NamespaceSearchRouter,
|
|
316
|
+
type NamespaceSearchHealth,
|
|
317
|
+
} from "./namespaces/search.js";
|
|
315
318
|
import { SharedContextManager } from "./shared-context/manager.js";
|
|
316
319
|
import {
|
|
317
320
|
CompoundingEngine,
|
|
@@ -2324,6 +2327,13 @@ export class Orchestrator {
|
|
|
2324
2327
|
});
|
|
2325
2328
|
}
|
|
2326
2329
|
|
|
2330
|
+
async searchHealthForNamespace(
|
|
2331
|
+
namespace: string,
|
|
2332
|
+
execution?: SearchExecutionOptions,
|
|
2333
|
+
): Promise<NamespaceSearchHealth> {
|
|
2334
|
+
return await this.namespaceSearchRouter.healthForNamespace(namespace, execution);
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2327
2337
|
private isSearchAvailableForNamespaceRouting(): boolean {
|
|
2328
2338
|
if (this.config.namespacesEnabled) return true;
|
|
2329
2339
|
return this.qmd.isAvailable();
|