@remnic/core 9.25.6 → 9.25.7
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 +2 -2
- package/dist/access-authorization-probe.d.ts +2 -2
- package/dist/access-boundary.d.ts +2 -2
- package/dist/access-cli.js +1 -1
- package/dist/access-http.d.ts +2 -2
- package/dist/access-identity-continuity-surface.d.ts +1 -1
- package/dist/access-lcm-surface.d.ts +2 -2
- package/dist/access-mcp.d.ts +2 -2
- package/dist/access-observe-write-surface.d.ts +2 -2
- package/dist/access-operations.d.ts +5 -5
- package/dist/access-recall-concurrency.d.ts +2 -2
- package/dist/access-recall-response.d.ts +2 -2
- package/dist/access-recall-surface.d.ts +2 -2
- package/dist/access-schema.d.ts +76 -76
- package/dist/{access-service-BJQ22URi.d.ts → access-service-CIdWkP6J.d.ts} +1 -1
- package/dist/access-service.d.ts +2 -2
- package/dist/access-surface-catalog.d.ts +2 -2
- package/dist/bootstrap.d.ts +1 -1
- package/dist/{chunk-FR75B4UU.js → chunk-KZVPTJNZ.js} +334 -647
- package/dist/chunk-KZVPTJNZ.js.map +1 -0
- package/dist/{cli-1W7cNjJJ.d.ts → cli-Da5kesWC.d.ts} +2 -2
- package/dist/cli.d.ts +3 -3
- package/dist/explicit-capture.d.ts +1 -1
- package/dist/index.d.ts +392 -392
- package/dist/index.js +1 -1
- package/dist/mcp-memory-inspector-app.d.ts +2 -2
- package/dist/{orchestrator-BtA1wC5K.d.ts → orchestrator-CFJ_TG6D.d.ts} +15 -10
- package/dist/orchestrator.d.ts +1 -1
- package/dist/orchestrator.js +1 -1
- package/dist/schemas.d.ts +76 -76
- package/dist/shared-context/manager.d.ts +8 -8
- package/dist/transfer/types.d.ts +66 -66
- package/package.json +2 -2
- package/src/meetings/recall-exclusion.test.ts +4 -2
- package/src/orchestration/generic-recall-paths.ts +119 -0
- package/src/orchestration/namespace-read-fanout.ts +0 -12
- package/src/orchestration/orchestrator-helpers.ts +0 -35
- package/src/orchestration/recall-internal.ts +6 -6
- package/src/orchestration/recall-search-pipeline.test.ts +194 -28
- package/src/orchestration/recall-search-pipeline.ts +39 -89
- package/src/orchestrator.ts +5 -17
- package/src/recall-cold-deadline.test.ts +11 -13
- package/src/testing/subjects/generic-recall-paths.test.ts +157 -0
- package/dist/chunk-FR75B4UU.js.map +0 -1
- package/src/recall/archive-scoring.ts +0 -544
|
@@ -100,7 +100,7 @@ import {
|
|
|
100
100
|
type QueryAwarePrefilter,
|
|
101
101
|
type RecallInvocationOptions,
|
|
102
102
|
} from "../orchestrator.js";
|
|
103
|
-
import { isGenericRecallExcludedPath } from "./
|
|
103
|
+
import { isGenericRecallExcludedPath } from "./generic-recall-paths.js";
|
|
104
104
|
|
|
105
105
|
import type {
|
|
106
106
|
RecallSectionAppendOptions,
|
|
@@ -4165,8 +4165,7 @@ export class RecallInternalCoordinator {
|
|
|
4165
4165
|
memoryResults = memoryResults.filter((r) =>
|
|
4166
4166
|
recallNamespaces.includes(r.namespace ?? this.deps.namespaceFromPath(r.path)));
|
|
4167
4167
|
}
|
|
4168
|
-
|
|
4169
|
-
memoryResults = memoryResults.filter((r) => !isGenericRecallExcludedPath(r.path, this.deps.config.memoryDir));
|
|
4168
|
+
memoryResults = memoryResults.filter((r) => !isGenericRecallExcludedPath(r.path, this.deps.config, "qmd"));
|
|
4170
4169
|
|
|
4171
4170
|
const isFullModeGraphAssist =
|
|
4172
4171
|
graphCaps.multiGraphMemory &&
|
|
@@ -4297,6 +4296,7 @@ export class RecallInternalCoordinator {
|
|
|
4297
4296
|
}
|
|
4298
4297
|
}
|
|
4299
4298
|
}
|
|
4299
|
+
memoryResults = memoryResults.filter((r) => !isGenericRecallExcludedPath(r.path, this.deps.config, "qmd"));
|
|
4300
4300
|
|
|
4301
4301
|
// Apply mandatory recall safety filters before deadline-bound scoring
|
|
4302
4302
|
// enrichment. If scoring times out, we must fall back to this filtered
|
|
@@ -4550,7 +4550,7 @@ export class RecallInternalCoordinator {
|
|
|
4550
4550
|
recallNamespaces,
|
|
4551
4551
|
resolveNamespace: (p) => this.deps.namespaceFromPath(p),
|
|
4552
4552
|
limit: embeddingFetchLimit,
|
|
4553
|
-
|
|
4553
|
+
pathPolicy: this.deps.config,
|
|
4554
4554
|
},
|
|
4555
4555
|
);
|
|
4556
4556
|
const boostedScoped = await this.deps.boostSearchResults(
|
|
@@ -4745,7 +4745,7 @@ export class RecallInternalCoordinator {
|
|
|
4745
4745
|
recallNamespaces,
|
|
4746
4746
|
resolveNamespace: (p) => this.deps.namespaceFromPath(p),
|
|
4747
4747
|
limit: embeddingFetchLimit,
|
|
4748
|
-
|
|
4748
|
+
pathPolicy: this.deps.config,
|
|
4749
4749
|
},
|
|
4750
4750
|
);
|
|
4751
4751
|
const boostedScoped = await this.deps.boostSearchResults(
|
|
@@ -4868,7 +4868,7 @@ export class RecallInternalCoordinator {
|
|
|
4868
4868
|
typeof asOfMs === "number" && Number.isFinite(asOfMs);
|
|
4869
4869
|
const activeMemories = memories.filter(
|
|
4870
4870
|
(m) => {
|
|
4871
|
-
if (isGenericRecallExcludedPath(m.path, this.deps.config
|
|
4871
|
+
if (isGenericRecallExcludedPath(m.path, this.deps.config)) return false;
|
|
4872
4872
|
const status = m.frontmatter.status;
|
|
4873
4873
|
if (!status || status === "active") return true;
|
|
4874
4874
|
if (status === "superseded") {
|
|
@@ -78,13 +78,12 @@ function scopedCandidatesStub(
|
|
|
78
78
|
seed.filter((r) => candidatePaths.has(r.path)).slice(0, Math.max(0, limit));
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
function queryAwareFallbackCoordinator(
|
|
82
|
+
seed: QmdSearchResult[],
|
|
83
|
+
): RecallSearchPipelineCoordinator {
|
|
84
84
|
const namespaceFromPath: RecallSearchPipelineDeps["namespaceFromPath"] = () => "default";
|
|
85
85
|
const deps = {
|
|
86
86
|
searchScopedMemoryCandidates: scopedCandidatesStub(seed),
|
|
87
|
-
readArchivedMemoriesForNamespaces,
|
|
88
87
|
namespaceFromPath,
|
|
89
88
|
config: { memoryDir: "/mem" },
|
|
90
89
|
} as unknown as RecallSearchPipelineDeps;
|
|
@@ -107,48 +106,85 @@ const SEED_PATHS = [MEETING_RECORD, "facts/a.md", "artifacts/y.md"];
|
|
|
107
106
|
const SEED_WITH_NON_RECALLABLE = SEED_PATHS.map((p) => result("default", p));
|
|
108
107
|
const PREFILTER = prefilter(SEED_PATHS);
|
|
109
108
|
|
|
110
|
-
test("
|
|
111
|
-
const coordinator =
|
|
112
|
-
|
|
113
|
-
// `return scopedSeedResults` early path at the archived-empty guard.
|
|
114
|
-
const out = await coordinator.searchLongTermArchiveFallback("quarterly report", ["default"], 10, PREFILTER);
|
|
109
|
+
test("query-aware fallback filters non-recallable scoped candidates", async () => {
|
|
110
|
+
const coordinator = queryAwareFallbackCoordinator(SEED_WITH_NON_RECALLABLE);
|
|
111
|
+
const out = await coordinator.searchQueryAwareFallback("quarterly report", 10, PREFILTER);
|
|
115
112
|
assert.deepEqual(
|
|
116
113
|
out.map((r) => r.path),
|
|
117
114
|
["facts/a.md"],
|
|
118
|
-
"meeting
|
|
115
|
+
"meeting records and artifacts must not leak through query-aware fallback",
|
|
119
116
|
);
|
|
120
117
|
});
|
|
121
118
|
|
|
122
|
-
test("
|
|
123
|
-
const coordinator =
|
|
124
|
-
|
|
125
|
-
// before archive scoring is even attempted.
|
|
126
|
-
const out = await coordinator.searchLongTermArchiveFallback("", ["default"], 10, PREFILTER);
|
|
119
|
+
test("query-aware fallback filters non-recallable scoped candidates for an empty query", async () => {
|
|
120
|
+
const coordinator = queryAwareFallbackCoordinator(SEED_WITH_NON_RECALLABLE);
|
|
121
|
+
const out = await coordinator.searchQueryAwareFallback("", 10, PREFILTER);
|
|
127
122
|
assert.deepEqual(out.map((r) => r.path), ["facts/a.md"]);
|
|
128
123
|
});
|
|
129
124
|
|
|
130
|
-
test("
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
// not early-return that hit and (archive empty) yields nothing.
|
|
134
|
-
const coordinator = fallbackCoordinator([result("default", MEETING_RECORD)], []);
|
|
135
|
-
const out = await coordinator.searchLongTermArchiveFallback("quarterly", ["default"], 1, prefilter([MEETING_RECORD]));
|
|
125
|
+
test("query-aware fallback applies the cap after filtering", async () => {
|
|
126
|
+
const coordinator = queryAwareFallbackCoordinator([result("default", MEETING_RECORD)]);
|
|
127
|
+
const out = await coordinator.searchQueryAwareFallback("quarterly", 1, prefilter([MEETING_RECORD]));
|
|
136
128
|
assert.deepEqual(out.map((r) => r.path), []);
|
|
137
129
|
});
|
|
138
130
|
|
|
139
|
-
test("
|
|
131
|
+
test("query-aware fallback excludes root archive candidates before its cap", async () => {
|
|
132
|
+
const archivePath = "archive/2026-02-23/fact-archived.md";
|
|
133
|
+
const coordinator = queryAwareFallbackCoordinator([
|
|
134
|
+
result("default", archivePath),
|
|
135
|
+
result("default", "facts/a.md"),
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
const out = await coordinator.searchQueryAwareFallback(
|
|
139
|
+
"quarterly",
|
|
140
|
+
1,
|
|
141
|
+
prefilter([archivePath, "facts/a.md"]),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
assert.deepEqual(out.map((r) => r.path), ["facts/a.md"]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("query-aware fallback retains non-active candidates for historical filtering", async () => {
|
|
148
|
+
let optionsSeen: { allowArchived?: boolean } | undefined;
|
|
149
|
+
const searchScopedMemoryCandidates: RecallSearchPipelineDeps["searchScopedMemoryCandidates"] = async (
|
|
150
|
+
candidatePaths,
|
|
151
|
+
_query,
|
|
152
|
+
limit,
|
|
153
|
+
options,
|
|
154
|
+
) => {
|
|
155
|
+
optionsSeen = options;
|
|
156
|
+
return [result("default", "facts/superseded.md")]
|
|
157
|
+
.filter((candidate) => candidatePaths.has(candidate.path))
|
|
158
|
+
.slice(0, limit);
|
|
159
|
+
};
|
|
160
|
+
const coordinator = new RecallSearchPipelineCoordinator({
|
|
161
|
+
config: { memoryDir: "/mem" },
|
|
162
|
+
namespaceFromPath: () => "default",
|
|
163
|
+
searchScopedMemoryCandidates,
|
|
164
|
+
} as unknown as RecallSearchPipelineDeps);
|
|
165
|
+
|
|
166
|
+
const out = await coordinator.searchQueryAwareFallback(
|
|
167
|
+
"historical incident",
|
|
168
|
+
1,
|
|
169
|
+
prefilter(["facts/superseded.md"]),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
assert.deepEqual(out.map((candidate) => candidate.path), ["facts/superseded.md"]);
|
|
173
|
+
assert.deepEqual(optionsSeen, { allowArchived: true });
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("query-aware fallback overfetches scoped candidates before exclusion", async () => {
|
|
140
177
|
// The scoped candidate order puts the NON-recallable meeting record FIRST;
|
|
141
178
|
// capping the scoped fetch to the caller's limit (1) BEFORE the generic-recall
|
|
142
179
|
// exclusion would return only the meeting record, which is then filtered out —
|
|
143
180
|
// dropping facts/a.md with no refill. Overfetching the full candidate set first
|
|
144
181
|
// and excluding afterward must still surface the recallable hit at the cap.
|
|
145
|
-
const coordinator =
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
);
|
|
149
|
-
const out = await coordinator.
|
|
182
|
+
const coordinator = queryAwareFallbackCoordinator([
|
|
183
|
+
result("default", MEETING_RECORD),
|
|
184
|
+
result("default", "facts/a.md"),
|
|
185
|
+
]);
|
|
186
|
+
const out = await coordinator.searchQueryAwareFallback(
|
|
150
187
|
"quarterly",
|
|
151
|
-
["default"],
|
|
152
188
|
1,
|
|
153
189
|
prefilter([MEETING_RECORD, "facts/a.md"]),
|
|
154
190
|
);
|
|
@@ -159,6 +195,7 @@ test("finding 1 — overfetches scoped seeds before exclusion so a capped fetch
|
|
|
159
195
|
);
|
|
160
196
|
});
|
|
161
197
|
|
|
198
|
+
|
|
162
199
|
function hotSeedCoordinator(seed: QmdSearchResult[]): RecallSearchPipelineCoordinator {
|
|
163
200
|
const deps = {
|
|
164
201
|
config: { memoryDir: "/mem", qmdSearchStrategy: "lex", searchBackend: "qmd" },
|
|
@@ -250,3 +287,132 @@ test("finding 1 (round-5) — excluded seeds must not fill the merge cap and sta
|
|
|
250
287
|
"excluded seeds ranked ahead must not fill the fetchLimit cap and starve the recallable primary hit",
|
|
251
288
|
);
|
|
252
289
|
});
|
|
290
|
+
|
|
291
|
+
test("QMD widening continues past archive-only pages", async () => {
|
|
292
|
+
const archived = Array.from({ length: 45 }, (_, index) =>
|
|
293
|
+
resultScored("default", `archive/2026-07-25/archive-${index}.md`, 2),
|
|
294
|
+
);
|
|
295
|
+
const live = resultScored("default", "facts/live.md", 1);
|
|
296
|
+
const ranked = [...archived, live];
|
|
297
|
+
const coordinator = new RecallSearchPipelineCoordinator({
|
|
298
|
+
config: { memoryDir: "/mem", qmdSearchStrategy: "lex", searchBackend: "qmd" },
|
|
299
|
+
qmd: {},
|
|
300
|
+
namespaceFromPath: () => "default",
|
|
301
|
+
searchAcrossNamespaces: async ({ maxResults }: { maxResults: number }) =>
|
|
302
|
+
ranked.slice(0, maxResults),
|
|
303
|
+
} as unknown as RecallSearchPipelineDeps);
|
|
304
|
+
|
|
305
|
+
const out = await coordinator.fetchQmdMemoryResultsWithArtifactTopUp(
|
|
306
|
+
"archive-starved",
|
|
307
|
+
20,
|
|
308
|
+
20,
|
|
309
|
+
{
|
|
310
|
+
namespacesEnabled: false,
|
|
311
|
+
recallNamespaces: ["default"],
|
|
312
|
+
resolveNamespace: () => "default",
|
|
313
|
+
queryAwarePrefilter: {
|
|
314
|
+
candidatePaths: null,
|
|
315
|
+
temporalFromDate: null,
|
|
316
|
+
matchedTags: [],
|
|
317
|
+
expandedTags: [],
|
|
318
|
+
combination: "none",
|
|
319
|
+
filteredToFullSearch: false,
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
assert.deepEqual(out.map((candidate) => candidate.path), ["facts/live.md"]);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("QMD hybrid top-up runs when primary results are archive-only", async () => {
|
|
328
|
+
const archived = Array.from({ length: 20 }, (_, index) =>
|
|
329
|
+
resultScored("default", `archive/2026-07-25/archive-${index}.md`, 2),
|
|
330
|
+
);
|
|
331
|
+
const live = resultScored("default", "facts/hybrid-live.md", 1);
|
|
332
|
+
let hybridCalls = 0;
|
|
333
|
+
const coordinator = new RecallSearchPipelineCoordinator({
|
|
334
|
+
config: {
|
|
335
|
+
memoryDir: "/mem",
|
|
336
|
+
qmdSearchStrategy: "hybrid",
|
|
337
|
+
searchBackend: "qmd",
|
|
338
|
+
},
|
|
339
|
+
qmd: {
|
|
340
|
+
search: async (
|
|
341
|
+
_query: string,
|
|
342
|
+
_collection: string,
|
|
343
|
+
maxResults: number,
|
|
344
|
+
) => archived.slice(0, maxResults),
|
|
345
|
+
hybridSearch: async () => {
|
|
346
|
+
hybridCalls += 1;
|
|
347
|
+
return [live];
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
namespaceFromPath: () => "default",
|
|
351
|
+
} as unknown as RecallSearchPipelineDeps);
|
|
352
|
+
|
|
353
|
+
const out = await coordinator.fetchQmdMemoryResultsWithArtifactTopUp(
|
|
354
|
+
"archive-starved-hybrid",
|
|
355
|
+
2,
|
|
356
|
+
2,
|
|
357
|
+
{
|
|
358
|
+
namespacesEnabled: false,
|
|
359
|
+
recallNamespaces: ["default"],
|
|
360
|
+
resolveNamespace: () => "default",
|
|
361
|
+
collection: "openclaw-engram",
|
|
362
|
+
queryAwarePrefilter: {
|
|
363
|
+
candidatePaths: null,
|
|
364
|
+
temporalFromDate: null,
|
|
365
|
+
matchedTags: [],
|
|
366
|
+
expandedTags: [],
|
|
367
|
+
combination: "none",
|
|
368
|
+
filteredToFullSearch: false,
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
assert.ok(hybridCalls > 0, "archive-only primary pages must trigger hybrid top-up");
|
|
374
|
+
assert.deepEqual(out.map((candidate) => candidate.path), ["facts/hybrid-live.md"]);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("QMD stops after an underfilled archive-only page", async () => {
|
|
378
|
+
const archived = [resultScored("default", "archive/2026-07-25/only.md", 2)];
|
|
379
|
+
let searchCalls = 0;
|
|
380
|
+
const coordinator = new RecallSearchPipelineCoordinator({
|
|
381
|
+
config: {
|
|
382
|
+
memoryDir: "/mem",
|
|
383
|
+
qmdSearchStrategy: "hybrid",
|
|
384
|
+
searchBackend: "qmd",
|
|
385
|
+
},
|
|
386
|
+
qmd: {
|
|
387
|
+
search: async () => {
|
|
388
|
+
searchCalls += 1;
|
|
389
|
+
return archived;
|
|
390
|
+
},
|
|
391
|
+
hybridSearch: async () => [],
|
|
392
|
+
},
|
|
393
|
+
namespaceFromPath: () => "default",
|
|
394
|
+
} as unknown as RecallSearchPipelineDeps);
|
|
395
|
+
|
|
396
|
+
const out = await coordinator.fetchQmdMemoryResultsWithArtifactTopUp(
|
|
397
|
+
"archive-underfilled",
|
|
398
|
+
20,
|
|
399
|
+
20,
|
|
400
|
+
{
|
|
401
|
+
namespacesEnabled: false,
|
|
402
|
+
recallNamespaces: ["default"],
|
|
403
|
+
resolveNamespace: () => "default",
|
|
404
|
+
collection: "openclaw-engram",
|
|
405
|
+
queryAwarePrefilter: {
|
|
406
|
+
candidatePaths: null,
|
|
407
|
+
temporalFromDate: null,
|
|
408
|
+
matchedTags: [],
|
|
409
|
+
expandedTags: [],
|
|
410
|
+
combination: "none",
|
|
411
|
+
filteredToFullSearch: false,
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
assert.deepEqual(out, []);
|
|
417
|
+
assert.equal(searchCalls, 1, "an underfilled raw page must not trigger widening");
|
|
418
|
+
});
|
|
@@ -28,7 +28,6 @@ import { NamespaceStorageRouter } from "../namespaces/storage.js";
|
|
|
28
28
|
import { NegativeExampleStore } from "../negative.js";
|
|
29
29
|
import { qmdCollectionPathParts } from "./qmd-result-resolver.js";
|
|
30
30
|
import type { GraphRecallExpandedEntry } from "../recall-state.js";
|
|
31
|
-
import { getDefaultArchiveScoring, memoryFileToScoreItem } from "../recall/archive-scoring.js";
|
|
32
31
|
import { RelevanceStore } from "../relevance.js";
|
|
33
32
|
import { RerankCache, rerankLocalOrNoop, reorderByRankedKeys } from "../rerank.js";
|
|
34
33
|
import type { SearchBackend, SearchDegradation, SearchExecutionOptions, SearchQueryOptions } from "../search/port.js";
|
|
@@ -55,11 +54,11 @@ import {
|
|
|
55
54
|
computeQmdHybridFetchLimit,
|
|
56
55
|
filterRecallCandidates,
|
|
57
56
|
throwIfRecallAborted,
|
|
58
|
-
tokenizeRecallQuery,
|
|
59
57
|
type QmdRecallSnapshot,
|
|
60
58
|
type QueryAwarePrefilter,
|
|
61
59
|
} from "../orchestrator.js";
|
|
62
|
-
import { isActivityDigestPath
|
|
60
|
+
import { isActivityDigestPath } from "./orchestrator-helpers.js";
|
|
61
|
+
import { isGenericRecallExcludedPath, isTopLevelArchivePath } from "./generic-recall-paths.js";
|
|
63
62
|
|
|
64
63
|
export interface RecallSearchPipelineDeps {
|
|
65
64
|
applyMemoryWorthRerank(
|
|
@@ -185,7 +184,6 @@ export interface RecallSearchPipelineDeps {
|
|
|
185
184
|
namespaceFromPath(p: string): string;
|
|
186
185
|
readonly negatives: NegativeExampleStore;
|
|
187
186
|
readonly qmd: SearchBackend;
|
|
188
|
-
readArchivedMemoriesForNamespaces(namespaces: string[]): Promise<MemoryFile[]>;
|
|
189
187
|
readQmdResultMemory(
|
|
190
188
|
resultPath: string,
|
|
191
189
|
fallbackStorage: StorageManager,
|
|
@@ -215,9 +213,8 @@ export interface RecallSearchPipelineDeps {
|
|
|
215
213
|
searchOptions?: SearchQueryOptions;
|
|
216
214
|
execution?: SearchExecutionOptions;
|
|
217
215
|
}): Promise<QmdSearchResult[]>;
|
|
218
|
-
|
|
216
|
+
searchQueryAwareFallback(
|
|
219
217
|
prompt: string,
|
|
220
|
-
recallNamespaces: string[],
|
|
221
218
|
limit: number,
|
|
222
219
|
queryAwarePrefilter?: QueryAwarePrefilter,
|
|
223
220
|
abortSignal?: AbortSignal,
|
|
@@ -422,26 +419,19 @@ export class RecallSearchPipelineCoordinator {
|
|
|
422
419
|
? await this.deps.searchScopedMemoryCandidates(
|
|
423
420
|
queryAwarePrefilter.candidatePaths,
|
|
424
421
|
prompt,
|
|
425
|
-
//
|
|
426
|
-
// exclusion `filterRecallCandidates` applies below (L464). Capping to
|
|
427
|
-
// `qmdFetchLimit` here lets excluded paths (artifacts, activity digests,
|
|
428
|
-
// meeting records) consume the caller's budget and drop legitimate hits
|
|
429
|
-
// with no refill — the hot-path mirror of the archive-fallback fix. The
|
|
430
|
-
// candidate set is an already-bounded prefilter, so its size is the
|
|
431
|
-
// natural ceiling; the post-exclusion cap is applied by filterRecallCandidates.
|
|
422
|
+
// Exclude dedicated surfaces before capping the bounded prefilter set.
|
|
432
423
|
queryAwarePrefilter.candidatePaths.size,
|
|
433
424
|
{ allowArchived: options.collection !== undefined },
|
|
434
425
|
)
|
|
435
426
|
: [];
|
|
436
427
|
// Drop generic-recall-excluded records before the fetchLimit cap so they can't starve valid memories (#1995).
|
|
437
|
-
const recallable = (r: QmdSearchResult) => !isGenericRecallExcludedPath(r.path, this.deps.config
|
|
428
|
+
const recallable = (r: QmdSearchResult) => !isGenericRecallExcludedPath(r.path, this.deps.config, "qmd");
|
|
438
429
|
|
|
439
430
|
let fetchLimit = Math.max(qmdFetchLimit, qmdHybridFetchLimit);
|
|
440
431
|
const maxFetchLimit = Math.min(
|
|
441
432
|
320,
|
|
442
433
|
Math.max(fetchLimit, qmdFetchLimit * 5),
|
|
443
434
|
);
|
|
444
|
-
const MAX_ATTEMPTS = 2;
|
|
445
435
|
const qmdRecallBudgetMs = this.deps.config.recallEnrichmentDeadlineMs ?? 25_000;
|
|
446
436
|
const qmdRecallBudgetEnabled = qmdRecallBudgetMs > 0;
|
|
447
437
|
const startedAtMs = Date.now();
|
|
@@ -475,7 +465,7 @@ export class RecallSearchPipelineCoordinator {
|
|
|
475
465
|
recallNamespaces: options.recallNamespaces,
|
|
476
466
|
resolveNamespace: options.resolveNamespace,
|
|
477
467
|
limit: qmdFetchLimit,
|
|
478
|
-
|
|
468
|
+
pathPolicy: this.deps.config,
|
|
479
469
|
});
|
|
480
470
|
const emitDebugSnapshot = async (
|
|
481
471
|
results: QmdSearchResult[],
|
|
@@ -508,7 +498,7 @@ export class RecallSearchPipelineCoordinator {
|
|
|
508
498
|
return [];
|
|
509
499
|
}
|
|
510
500
|
|
|
511
|
-
for (
|
|
501
|
+
for (;;) {
|
|
512
502
|
throwIfRecallAborted(options.abortSignal);
|
|
513
503
|
if (
|
|
514
504
|
qmdRecallBudgetEnabled &&
|
|
@@ -554,10 +544,15 @@ export class RecallSearchPipelineCoordinator {
|
|
|
554
544
|
lastHybridTopUpUsed = false;
|
|
555
545
|
lastHybridTopUpSkippedReason = undefined;
|
|
556
546
|
let mergedResults = primaryResults;
|
|
547
|
+
const primaryRecallableCount = primaryResults.filter(recallable).length;
|
|
548
|
+
const primaryIncludesArchivePath = primaryResults.some((result) =>
|
|
549
|
+
isTopLevelArchivePath(result.path, this.deps.config, "qmd"),
|
|
550
|
+
);
|
|
557
551
|
|
|
558
|
-
// Backfill with hybrid results
|
|
552
|
+
// Backfill with hybrid results when the recallable primary page underfills.
|
|
559
553
|
if (
|
|
560
|
-
|
|
554
|
+
primaryRecallableCount < qmdFetchLimit &&
|
|
555
|
+
(primaryResults.length < qmdFetchLimit || primaryIncludesArchivePath) &&
|
|
561
556
|
(!qmdRecallBudgetEnabled ||
|
|
562
557
|
Date.now() - startedAtMs < qmdRecallBudgetMs)
|
|
563
558
|
) {
|
|
@@ -620,7 +615,7 @@ export class RecallSearchPipelineCoordinator {
|
|
|
620
615
|
recallNamespaces: options.recallNamespaces,
|
|
621
616
|
resolveNamespace: options.resolveNamespace,
|
|
622
617
|
limit: fetchLimit,
|
|
623
|
-
|
|
618
|
+
pathPolicy: this.deps.config,
|
|
624
619
|
});
|
|
625
620
|
|
|
626
621
|
if (filteredResults.length >= qmdFetchLimit) {
|
|
@@ -631,11 +626,9 @@ export class RecallSearchPipelineCoordinator {
|
|
|
631
626
|
if (filteredResults.length > bestFiltered.length) {
|
|
632
627
|
bestFiltered = filteredResults;
|
|
633
628
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
}
|
|
638
|
-
if (mergedResults.length < fetchLimit && filteredResults.length > 0) {
|
|
629
|
+
// A full raw page may filter down to nothing (for example, archive-only
|
|
630
|
+
// hits). Keep widening until the backend itself underfills the page.
|
|
631
|
+
if (primaryResults.length < fetchLimit) {
|
|
639
632
|
await emitDebugSnapshot(filteredResults, fetchLimit);
|
|
640
633
|
return filteredResults;
|
|
641
634
|
}
|
|
@@ -679,20 +672,18 @@ export class RecallSearchPipelineCoordinator {
|
|
|
679
672
|
}
|
|
680
673
|
|
|
681
674
|
/**
|
|
682
|
-
*
|
|
683
|
-
*
|
|
675
|
+
* Cold fallback retains qualifying query-aware candidates. Archive records
|
|
676
|
+
* belong to dedicated surfaces and never re-enter generic recall.
|
|
684
677
|
*/
|
|
685
|
-
async
|
|
678
|
+
async searchQueryAwareFallback(
|
|
686
679
|
prompt: string,
|
|
687
|
-
recallNamespaces: string[],
|
|
688
680
|
limit: number,
|
|
689
681
|
queryAwarePrefilter?: QueryAwarePrefilter,
|
|
690
682
|
abortSignal?: AbortSignal,
|
|
691
683
|
): Promise<QmdSearchResult[]> {
|
|
692
684
|
throwIfRecallAborted(abortSignal);
|
|
693
685
|
const cappedLimit = Math.max(0, limit);
|
|
694
|
-
if (cappedLimit === 0) return [];
|
|
695
|
-
if (queryAwarePrefilter?.candidatePaths?.size === 0) return [];
|
|
686
|
+
if (cappedLimit === 0 || queryAwarePrefilter?.candidatePaths?.size === 0) return [];
|
|
696
687
|
|
|
697
688
|
const candidatePaths = queryAwarePrefilter?.candidatePaths;
|
|
698
689
|
const scopedSeedResults = (
|
|
@@ -700,57 +691,12 @@ export class RecallSearchPipelineCoordinator {
|
|
|
700
691
|
? await this.deps.searchScopedMemoryCandidates(
|
|
701
692
|
candidatePaths,
|
|
702
693
|
prompt,
|
|
703
|
-
// Overfetch the FULL scoped candidate set BEFORE the generic-recall
|
|
704
|
-
// exclusion below. Capping to `cappedLimit` first lets excluded paths
|
|
705
|
-
// (artifacts, activity digests, meeting records) consume the caller's
|
|
706
|
-
// budget and drop legitimate hits with no refill — e.g. a prefilter of
|
|
707
|
-
// [meetingRecord, facts/a.md] at limit 1 would return nothing. The
|
|
708
|
-
// candidate set is an already-bounded prefilter, so its size is the
|
|
709
|
-
// natural ceiling; the post-exclusion cap is applied below.
|
|
710
694
|
candidatePaths.size,
|
|
711
695
|
{ allowArchived: true },
|
|
712
696
|
)
|
|
713
697
|
: []
|
|
714
|
-
).filter((result) => !isGenericRecallExcludedPath(result.path, this.deps.config
|
|
715
|
-
|
|
716
|
-
// excluded above AFTER an unconstrained candidate fetch, so the cap here counts
|
|
717
|
-
// only recallable seeds — and EVERY early-return / direct-caller path below
|
|
718
|
-
// returns already-filtered, budget-honoring seeds.
|
|
719
|
-
if (scopedSeedResults.length >= cappedLimit) {
|
|
720
|
-
return scopedSeedResults.slice(0, cappedLimit);
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
const tokens = Array.from(new Set(tokenizeRecallQuery(prompt)));
|
|
724
|
-
if (tokens.length === 0) return scopedSeedResults;
|
|
725
|
-
|
|
726
|
-
throwIfRecallAborted(abortSignal);
|
|
727
|
-
const archivedMemories =
|
|
728
|
-
await this.deps.readArchivedMemoriesForNamespaces(recallNamespaces);
|
|
729
|
-
if (archivedMemories.length === 0) return scopedSeedResults;
|
|
730
|
-
|
|
731
|
-
// Issue #1674: off-load the CPU-bound archive-scoring loop to a
|
|
732
|
-
// worker_threads pool so concurrent recall requests run on separate
|
|
733
|
-
// cores instead of serializing on the main JS thread. The pure scoring
|
|
734
|
-
// function is identical to the old inline loop — only the execution
|
|
735
|
-
// context changed. Aborts are checked at the boundaries (before submit
|
|
736
|
-
// and after result); the worker's work is bounded by the file count.
|
|
737
|
-
throwIfRecallAborted(abortSignal);
|
|
738
|
-
const scoring = getDefaultArchiveScoring();
|
|
739
|
-
const scoredResults = await scoring.score(archivedMemories.map(memoryFileToScoreItem), tokens, abortSignal);
|
|
740
|
-
throwIfRecallAborted(abortSignal);
|
|
741
|
-
const scored: QmdSearchResult[] = scoredResults.map((r) => ({
|
|
742
|
-
docid: r.docid,
|
|
743
|
-
path: r.path,
|
|
744
|
-
score: r.score,
|
|
745
|
-
snippet: r.snippet,
|
|
746
|
-
}));
|
|
747
|
-
|
|
748
|
-
return dedupeResultsByNamespace(
|
|
749
|
-
[...scopedSeedResults, ...scored],
|
|
750
|
-
this.deps.namespaceFromPath,
|
|
751
|
-
cappedLimit,
|
|
752
|
-
{ filter: (result) => !isGenericRecallExcludedPath(result.path, this.deps.config.memoryDir) },
|
|
753
|
-
);
|
|
698
|
+
).filter((result) => !isGenericRecallExcludedPath(result.path, this.deps.config, "qmd"));
|
|
699
|
+
return scopedSeedResults.slice(0, cappedLimit);
|
|
754
700
|
}
|
|
755
701
|
|
|
756
702
|
async applyColdFallbackPipeline(options: {
|
|
@@ -935,17 +881,19 @@ export class RecallSearchPipelineCoordinator {
|
|
|
935
881
|
}
|
|
936
882
|
}
|
|
937
883
|
if (longTerm.length === 0) {
|
|
938
|
-
// The archive-scan scoring worker terminates when EITHER the caller
|
|
939
|
-
// aborts or this step's own deadline wins — both flow through the
|
|
940
|
-
// step signal injected by runColdStepWithinDeadline (#1674, #1907).
|
|
941
884
|
longTerm = await runColdStepWithinDeadline(
|
|
942
|
-
"
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
885
|
+
"query-aware fallback",
|
|
886
|
+
[],
|
|
887
|
+
(stepSignal) =>
|
|
888
|
+
this.deps.searchQueryAwareFallback(
|
|
889
|
+
options.prompt,
|
|
890
|
+
options.recallResultLimit,
|
|
891
|
+
options.queryAwarePrefilter,
|
|
892
|
+
stepSignal,
|
|
893
|
+
),
|
|
946
894
|
);
|
|
947
895
|
if (longTerm.length > 0) {
|
|
948
|
-
log.debug("cold-tier recall source=
|
|
896
|
+
log.debug("cold-tier recall source=query-aware-fallback");
|
|
949
897
|
}
|
|
950
898
|
}
|
|
951
899
|
if (longTerm.length === 0) return [];
|
|
@@ -997,9 +945,8 @@ export class RecallSearchPipelineCoordinator {
|
|
|
997
945
|
}
|
|
998
946
|
results = scopedResults;
|
|
999
947
|
}
|
|
1000
|
-
// Dedicated-surface isolation
|
|
1001
|
-
|
|
1002
|
-
results = results.filter((r) => !isGenericRecallExcludedPath(r.path, this.deps.config.memoryDir));
|
|
948
|
+
// Dedicated-surface isolation keeps generic recall out of artifacts, activity digests, and archives.
|
|
949
|
+
results = results.filter((r) => !isGenericRecallExcludedPath(r.path, this.deps.config, "qmd"));
|
|
1003
950
|
if (results.length === 0) return [];
|
|
1004
951
|
|
|
1005
952
|
const isFullModeGraphAssist =
|
|
@@ -1022,6 +969,9 @@ export class RecallSearchPipelineCoordinator {
|
|
|
1022
969
|
});
|
|
1023
970
|
results = merged;
|
|
1024
971
|
}
|
|
972
|
+
results = results.filter(
|
|
973
|
+
(result) => !isGenericRecallExcludedPath(result.path, this.deps.config, "qmd"),
|
|
974
|
+
);
|
|
1025
975
|
|
|
1026
976
|
const boostInput = await this.deps.filterSearchResultsForRecall(
|
|
1027
977
|
results,
|
package/src/orchestrator.ts
CHANGED
|
@@ -134,11 +134,11 @@ import {
|
|
|
134
134
|
computeSemanticDedupScope,
|
|
135
135
|
qmdCollectionNamespaceFromPrefix as computeQmdCollectionNamespaceFromPrefix,
|
|
136
136
|
} from "./orchestration/orchestrator-namespace-scope.js";
|
|
137
|
+
import { isGenericRecallExcludedPath } from "./orchestration/generic-recall-paths.js";
|
|
137
138
|
import {
|
|
138
139
|
abortRecallError,
|
|
139
140
|
buildCompressionGuidelinesMarkdown,
|
|
140
141
|
buildQmdIntentHint,
|
|
141
|
-
isGenericRecallExcludedPath,
|
|
142
142
|
mergeArtifactRecallCandidates,
|
|
143
143
|
tokenizeRecallQuery,
|
|
144
144
|
type BulkImportBatchIngestResult,
|
|
@@ -163,7 +163,6 @@ export {
|
|
|
163
163
|
computeQmdHybridFetchLimit,
|
|
164
164
|
defaultWorkspaceDir,
|
|
165
165
|
filterHourlySummaryMarkdownForLocalDay,
|
|
166
|
-
filterRecallCandidates,
|
|
167
166
|
formatDateInTimeZone,
|
|
168
167
|
isArtifactMemoryPath,
|
|
169
168
|
lifecycleRecallScoreAdjustment,
|
|
@@ -198,6 +197,7 @@ export {
|
|
|
198
197
|
type RecallInvocationOptions,
|
|
199
198
|
type RecallModeDecision,
|
|
200
199
|
} from "./orchestration/orchestrator-helpers.js";
|
|
200
|
+
export { filterRecallCandidates } from "./orchestration/generic-recall-paths.js";
|
|
201
201
|
|
|
202
202
|
export { hasIdentityRecoveryIntent, resolveEffectiveIdentityInjectionMode } from "./orchestration/recall-result-formatter.js";
|
|
203
203
|
import {
|
|
@@ -541,7 +541,6 @@ import type {
|
|
|
541
541
|
RecallSectionConfig,
|
|
542
542
|
RecallTierExplain,
|
|
543
543
|
} from "./types.js";
|
|
544
|
-
import { disposeDefaultArchiveScoring, getDefaultArchiveScoring, memoryFileToScoreItem } from "./recall/archive-scoring.js";
|
|
545
544
|
|
|
546
545
|
// Issue #1526 seam 15: ExtractionRunResult moved to orchestration/extraction-run.ts.
|
|
547
546
|
export type { ExtractionRunResult } from "./orchestration/extraction-run.js";
|
|
@@ -997,8 +996,6 @@ export class Orchestrator {
|
|
|
997
996
|
if (this.conversationQmd && this.conversationQmd !== this.qmd) {
|
|
998
997
|
await (this.conversationQmd as { dispose?: () => void | Promise<void> }).dispose?.();
|
|
999
998
|
}
|
|
1000
|
-
// Issue #1674: terminate archive-scoring worker threads on destroy.
|
|
1001
|
-
await disposeDefaultArchiveScoring();
|
|
1002
999
|
} finally {
|
|
1003
1000
|
if (bufferFlushError !== undefined) {
|
|
1004
1001
|
log.warn(
|
|
@@ -2483,7 +2480,7 @@ export class Orchestrator {
|
|
|
2483
2480
|
if (!paths) return null;
|
|
2484
2481
|
const scoped = new Set<string>();
|
|
2485
2482
|
for (const memoryPath of paths) {
|
|
2486
|
-
if (!memoryPath || isGenericRecallExcludedPath(memoryPath, this.config
|
|
2483
|
+
if (!memoryPath || isGenericRecallExcludedPath(memoryPath, this.config, "qmd")) continue;
|
|
2487
2484
|
if (
|
|
2488
2485
|
resolveNamespaceCapabilities(this.config).namespaces &&
|
|
2489
2486
|
!recallNamespaces.includes(this.namespaceFromPath(memoryPath))
|
|
@@ -3600,16 +3597,14 @@ export class Orchestrator {
|
|
|
3600
3597
|
);
|
|
3601
3598
|
}
|
|
3602
3599
|
|
|
3603
|
-
private async
|
|
3600
|
+
private async searchQueryAwareFallback(
|
|
3604
3601
|
prompt: string,
|
|
3605
|
-
recallNamespaces: string[],
|
|
3606
3602
|
limit: number,
|
|
3607
3603
|
queryAwarePrefilter?: QueryAwarePrefilter,
|
|
3608
3604
|
abortSignal?: AbortSignal,
|
|
3609
3605
|
): Promise<QmdSearchResult[]> {
|
|
3610
|
-
return this.recallSearchPipelineCoordinator.
|
|
3606
|
+
return this.recallSearchPipelineCoordinator.searchQueryAwareFallback(
|
|
3611
3607
|
prompt,
|
|
3612
|
-
recallNamespaces,
|
|
3613
3608
|
limit,
|
|
3614
3609
|
queryAwarePrefilter,
|
|
3615
3610
|
abortSignal,
|
|
@@ -3968,11 +3963,4 @@ export class Orchestrator {
|
|
|
3968
3963
|
namespaces,
|
|
3969
3964
|
);
|
|
3970
3965
|
}
|
|
3971
|
-
private async readArchivedMemoriesForNamespaces(
|
|
3972
|
-
namespaces: string[],
|
|
3973
|
-
): Promise<MemoryFile[]> {
|
|
3974
|
-
return this.namespaceReadFanoutCoordinator.readArchivedMemoriesForNamespaces(
|
|
3975
|
-
namespaces,
|
|
3976
|
-
);
|
|
3977
|
-
}
|
|
3978
3966
|
}
|