@remnic/core 9.3.664 → 9.3.666
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-audit.js +2 -2
- package/dist/access-cli.js +41 -40
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +3 -2
- package/dist/access-http.js +25 -25
- package/dist/access-mcp.d.ts +3 -2
- package/dist/access-mcp.js +22 -22
- package/dist/access-schema.js +3 -3
- package/dist/{access-service-D0SLB4MH.d.ts → access-service-DsS-TatL.d.ts} +1 -1
- package/dist/access-service.d.ts +3 -2
- package/dist/access-service.js +21 -21
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/bootstrap.d.ts +2 -1
- package/dist/briefing.js +4 -3
- package/dist/capabilities.d.ts +73 -0
- package/dist/capabilities.js +8 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/causal-behavior.js +2 -2
- package/dist/causal-chain.js +2 -2
- package/dist/causal-consolidation.js +7 -6
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/causal-retrieval.js +2 -2
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-ROHLEUTH.js → chunk-23EBQ27U.js} +5 -5
- package/dist/{chunk-YW52BQSU.js → chunk-2TCHDANJ.js} +2 -2
- package/dist/{chunk-IROWLAWG.js → chunk-46WUVFOD.js} +4 -4
- package/dist/{chunk-XB5P5P2L.js → chunk-4T7P2HLJ.js} +3 -3
- package/dist/{chunk-7XH7VJN4.js → chunk-6T4LTI2F.js} +4 -4
- package/dist/{chunk-TVVEYCNW.js → chunk-7K5Q6COX.js} +4 -4
- package/dist/{chunk-BZG2CWOQ.js → chunk-A5TEHAR4.js} +3 -3
- package/dist/{chunk-C7AF236A.js → chunk-AARDBQTA.js} +2 -2
- package/dist/{chunk-IHG6CC7T.js → chunk-BQJUPECT.js} +2 -2
- package/dist/{chunk-7OGJQP7T.js → chunk-CRO4LCQ6.js} +5 -5
- package/dist/{chunk-YNDLCWXS.js → chunk-EZ25VE3G.js} +4 -4
- package/dist/{chunk-LIERUFPO.js → chunk-GZ6QAYSH.js} +94 -74
- package/dist/chunk-GZ6QAYSH.js.map +1 -0
- package/dist/{chunk-UXA5L2DZ.js → chunk-HQCGRSRU.js} +2 -2
- package/dist/{chunk-RKNJBZ55.js → chunk-JBPKEARU.js} +4 -4
- package/dist/{chunk-XW3W4PV4.js → chunk-JTPXSXHC.js} +2 -2
- package/dist/{chunk-OHJFJ4HI.js → chunk-KOXGLQS7.js} +2 -2
- package/dist/{chunk-NLF54XMD.js → chunk-MPXYHC35.js} +26 -26
- package/dist/{chunk-6JBKHTQD.js → chunk-MR4PJ277.js} +2 -2
- package/dist/{chunk-EXXBA5OM.js → chunk-OI4BXFSB.js} +4 -4
- package/dist/{chunk-SQZ42MKH.js → chunk-OQH5XUH3.js} +6 -3
- package/dist/chunk-OQH5XUH3.js.map +1 -0
- package/dist/{chunk-2HEZXPYU.js → chunk-Q2LQZYQ7.js} +3 -3
- package/dist/{chunk-YKX63GBK.js → chunk-QHWJG5C5.js} +8 -8
- package/dist/{chunk-T2AN3BSP.js → chunk-QZ7ODIVL.js} +2 -2
- package/dist/chunk-RI5XBIZ6.js +23 -0
- package/dist/chunk-RI5XBIZ6.js.map +1 -0
- package/dist/{chunk-7ILWCUWH.js → chunk-TJ7HH5LB.js} +28 -3
- package/dist/chunk-TJ7HH5LB.js.map +1 -0
- package/dist/{chunk-V25ZAOSB.js → chunk-UOBLE67F.js} +4 -4
- package/dist/{chunk-JIX3ZL2J.js → chunk-UVUTV7CM.js} +15 -15
- package/dist/{chunk-VH6EIKVS.js → chunk-WKMCC4NQ.js} +35 -16
- package/dist/chunk-WKMCC4NQ.js.map +1 -0
- package/dist/{chunk-SSOMTUCA.js → chunk-WXGTC424.js} +1 -1
- package/dist/{chunk-KHGE6PMF.js → chunk-WXXLSZHA.js} +2 -2
- package/dist/{chunk-DSLUOQDY.js → chunk-XMWF6AU3.js} +2 -2
- package/dist/{chunk-DQY7NJ5L.js → chunk-XS2CWEHZ.js} +2 -2
- package/dist/{cli-BQRqR9N-.d.ts → cli-BypxcNqq.d.ts} +2 -2
- package/dist/cli.d.ts +4 -3
- package/dist/cli.js +42 -42
- package/dist/compounding/engine.js +4 -3
- package/dist/connectors/codex-materialize-runner.js +4 -3
- package/dist/connectors/index.js +4 -3
- package/dist/consolidation-provenance-check.js +2 -2
- package/dist/conversation-index/backend.js +2 -2
- package/dist/dashboard-runtime.js +2 -2
- package/dist/direct-answer-wiring.d.ts +13 -3
- package/dist/direct-answer-wiring.js +1 -1
- package/dist/entity-retrieval.js +4 -3
- package/dist/explicit-capture.d.ts +2 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +66 -65
- package/dist/index.js.map +1 -1
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.js +4 -4
- package/dist/maintenance/memory-governance.js +4 -4
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -3
- package/dist/maintenance/rebuild-memory-projection.js +5 -5
- package/dist/mcp-memory-inspector-app.d.ts +3 -2
- package/dist/namespaces/migrate.js +11 -11
- package/dist/namespaces/search.js +7 -7
- package/dist/namespaces/storage.d.ts +13 -0
- package/dist/namespaces/storage.js +4 -3
- package/dist/operator-toolkit.js +15 -15
- package/dist/{orchestrator-Cg1UkvmO.d.ts → orchestrator-DZqPVoMI.d.ts} +8 -0
- package/dist/orchestrator.d.ts +2 -1
- package/dist/orchestrator.js +32 -31
- package/dist/recall-planner-llm.d.ts +2 -1
- package/dist/recall-planner-llm.js +3 -2
- package/dist/recall-planner-llm.js.map +1 -1
- package/dist/search/factory.js +6 -6
- package/dist/search/index.js +10 -10
- package/dist/search/lancedb-backend.js +1 -1
- package/dist/search/meilisearch-backend.js +1 -1
- package/dist/search/orama-backend.js +1 -1
- package/dist/semantic-consolidation.js +5 -4
- package/dist/semantic-rule-promotion.js +4 -3
- package/dist/semantic-rule-verifier.js +4 -3
- package/dist/storage.js +3 -2
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/capsule-import.js +1 -1
- package/dist/verified-recall.js +4 -3
- package/package.json +1 -1
- package/src/capabilities.test.ts +97 -0
- package/src/capabilities.ts +86 -0
- package/src/direct-answer-wiring.test.ts +53 -2
- package/src/direct-answer-wiring.ts +18 -5
- package/src/namespaces/catalog.test.ts +12 -12
- package/src/namespaces/storage.ts +28 -1
- package/src/orchestrator.ts +69 -19
- package/src/recall-planner-llm.test.ts +12 -11
- package/src/recall-planner-llm.ts +7 -1
- package/src/storage-fallback-category-dirs.test.ts +150 -1
- package/src/storage.ts +51 -14
- package/dist/chunk-7ILWCUWH.js.map +0 -1
- package/dist/chunk-LIERUFPO.js.map +0 -1
- package/dist/chunk-SQZ42MKH.js.map +0 -1
- package/dist/chunk-VH6EIKVS.js.map +0 -1
- /package/dist/{chunk-ROHLEUTH.js.map → chunk-23EBQ27U.js.map} +0 -0
- /package/dist/{chunk-YW52BQSU.js.map → chunk-2TCHDANJ.js.map} +0 -0
- /package/dist/{chunk-IROWLAWG.js.map → chunk-46WUVFOD.js.map} +0 -0
- /package/dist/{chunk-XB5P5P2L.js.map → chunk-4T7P2HLJ.js.map} +0 -0
- /package/dist/{chunk-7XH7VJN4.js.map → chunk-6T4LTI2F.js.map} +0 -0
- /package/dist/{chunk-TVVEYCNW.js.map → chunk-7K5Q6COX.js.map} +0 -0
- /package/dist/{chunk-BZG2CWOQ.js.map → chunk-A5TEHAR4.js.map} +0 -0
- /package/dist/{chunk-C7AF236A.js.map → chunk-AARDBQTA.js.map} +0 -0
- /package/dist/{chunk-IHG6CC7T.js.map → chunk-BQJUPECT.js.map} +0 -0
- /package/dist/{chunk-7OGJQP7T.js.map → chunk-CRO4LCQ6.js.map} +0 -0
- /package/dist/{chunk-YNDLCWXS.js.map → chunk-EZ25VE3G.js.map} +0 -0
- /package/dist/{chunk-UXA5L2DZ.js.map → chunk-HQCGRSRU.js.map} +0 -0
- /package/dist/{chunk-RKNJBZ55.js.map → chunk-JBPKEARU.js.map} +0 -0
- /package/dist/{chunk-XW3W4PV4.js.map → chunk-JTPXSXHC.js.map} +0 -0
- /package/dist/{chunk-OHJFJ4HI.js.map → chunk-KOXGLQS7.js.map} +0 -0
- /package/dist/{chunk-NLF54XMD.js.map → chunk-MPXYHC35.js.map} +0 -0
- /package/dist/{chunk-6JBKHTQD.js.map → chunk-MR4PJ277.js.map} +0 -0
- /package/dist/{chunk-EXXBA5OM.js.map → chunk-OI4BXFSB.js.map} +0 -0
- /package/dist/{chunk-2HEZXPYU.js.map → chunk-Q2LQZYQ7.js.map} +0 -0
- /package/dist/{chunk-YKX63GBK.js.map → chunk-QHWJG5C5.js.map} +0 -0
- /package/dist/{chunk-T2AN3BSP.js.map → chunk-QZ7ODIVL.js.map} +0 -0
- /package/dist/{chunk-V25ZAOSB.js.map → chunk-UOBLE67F.js.map} +0 -0
- /package/dist/{chunk-JIX3ZL2J.js.map → chunk-UVUTV7CM.js.map} +0 -0
- /package/dist/{chunk-SSOMTUCA.js.map → chunk-WXGTC424.js.map} +0 -0
- /package/dist/{chunk-KHGE6PMF.js.map → chunk-WXXLSZHA.js.map} +0 -0
- /package/dist/{chunk-DSLUOQDY.js.map → chunk-XMWF6AU3.js.map} +0 -0
- /package/dist/{chunk-DQY7NJ5L.js.map → chunk-XS2CWEHZ.js.map} +0 -0
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
type DirectAnswerWiringInput,
|
|
8
8
|
} from "./direct-answer-wiring.js";
|
|
9
9
|
import { DEFAULT_TAXONOMY } from "./taxonomy/default-taxonomy.js";
|
|
10
|
-
import type { MemoryFile
|
|
10
|
+
import type { MemoryFile } from "./types.js";
|
|
11
11
|
import type { TrustZoneName } from "./trust-zones.js";
|
|
12
12
|
|
|
13
13
|
type WiringConfig = DirectAnswerWiringInput["config"];
|
|
@@ -94,7 +94,8 @@ test("tryDirectAnswer disabled-path does not call any source accessor", async ()
|
|
|
94
94
|
const result = await tryDirectAnswer({
|
|
95
95
|
query: "does not matter",
|
|
96
96
|
namespace: "default",
|
|
97
|
-
config:
|
|
97
|
+
config: BASE_CONFIG,
|
|
98
|
+
enabled: false,
|
|
98
99
|
sources,
|
|
99
100
|
});
|
|
100
101
|
assert.equal(result.eligible, false);
|
|
@@ -104,6 +105,42 @@ test("tryDirectAnswer disabled-path does not call any source accessor", async ()
|
|
|
104
105
|
assert.deepEqual(sources.calls.importance, []);
|
|
105
106
|
});
|
|
106
107
|
|
|
108
|
+
// ── Backward-compat (#1523): omit top-level `enabled`, fall back to config ──
|
|
109
|
+
|
|
110
|
+
test("tryDirectAnswer falls back to config.recallDirectAnswerEnabled when `enabled` is omitted (disabled)", async () => {
|
|
111
|
+
// Old input shape: config carries recallDirectAnswerEnabled: false and no
|
|
112
|
+
// top-level `enabled`. Must short-circuit as "disabled" (identical to the
|
|
113
|
+
// pre-#1523 behavior) rather than treating undefined as enabled.
|
|
114
|
+
const sources = makeMockSources({ memories: [makeMemory()] });
|
|
115
|
+
const result = await tryDirectAnswer({
|
|
116
|
+
query: "does not matter",
|
|
117
|
+
namespace: "default",
|
|
118
|
+
config: { ...BASE_CONFIG, recallDirectAnswerEnabled: false },
|
|
119
|
+
sources,
|
|
120
|
+
});
|
|
121
|
+
assert.equal(result.eligible, false);
|
|
122
|
+
assert.equal(result.reason, "disabled");
|
|
123
|
+
assert.equal(sources.calls.listCandidates, 0);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("tryDirectAnswer falls back to config.recallDirectAnswerEnabled when `enabled` is omitted (enabled)", async () => {
|
|
127
|
+
// Old input shape with recallDirectAnswerEnabled: true and no `enabled` must
|
|
128
|
+
// NOT short-circuit — it proceeds to materialize candidates.
|
|
129
|
+
const sources = makeMockSources({
|
|
130
|
+
memories: [makeMemory({ tags: ["pnpm"], content: "remnic uses pnpm" })],
|
|
131
|
+
trustZones: { m1: "trusted" },
|
|
132
|
+
importance: { m1: 0.9 },
|
|
133
|
+
});
|
|
134
|
+
const result = await tryDirectAnswer({
|
|
135
|
+
query: "package manager remnic",
|
|
136
|
+
namespace: "default",
|
|
137
|
+
config: BASE_CONFIG, // recallDirectAnswerEnabled: true
|
|
138
|
+
sources,
|
|
139
|
+
});
|
|
140
|
+
assert.notEqual(result.reason, "disabled");
|
|
141
|
+
assert.equal(sources.calls.listCandidates, 1);
|
|
142
|
+
});
|
|
143
|
+
|
|
107
144
|
// ── Empty-query short-circuit: no I/O ───────────────────────────────────────
|
|
108
145
|
|
|
109
146
|
test("tryDirectAnswer skips all I/O when query normalizes to zero searchable tokens", async () => {
|
|
@@ -119,6 +156,7 @@ test("tryDirectAnswer skips all I/O when query normalizes to zero searchable tok
|
|
|
119
156
|
query: "? !!! ",
|
|
120
157
|
namespace: "default",
|
|
121
158
|
config: BASE_CONFIG,
|
|
159
|
+
enabled: true,
|
|
122
160
|
sources,
|
|
123
161
|
});
|
|
124
162
|
assert.equal(result.reason, "empty-query");
|
|
@@ -135,6 +173,7 @@ test("tryDirectAnswer with empty memory list returns no-candidates", async () =>
|
|
|
135
173
|
query: "package manager remnic",
|
|
136
174
|
namespace: "default",
|
|
137
175
|
config: BASE_CONFIG,
|
|
176
|
+
enabled: true,
|
|
138
177
|
sources,
|
|
139
178
|
});
|
|
140
179
|
assert.equal(result.reason, "no-candidates");
|
|
@@ -158,6 +197,7 @@ test("tryDirectAnswer skips importance resolution for non-trusted memories", asy
|
|
|
158
197
|
query: "package manager remnic",
|
|
159
198
|
namespace: "default",
|
|
160
199
|
config: BASE_CONFIG,
|
|
200
|
+
enabled: true,
|
|
161
201
|
sources,
|
|
162
202
|
});
|
|
163
203
|
assert.equal(result.eligible, false);
|
|
@@ -182,6 +222,7 @@ test("tryDirectAnswer skips importance for quarantine-zone memories", async () =
|
|
|
182
222
|
query: "package manager remnic",
|
|
183
223
|
namespace: "default",
|
|
184
224
|
config: BASE_CONFIG,
|
|
225
|
+
enabled: true,
|
|
185
226
|
sources,
|
|
186
227
|
});
|
|
187
228
|
assert.equal(result.eligible, false);
|
|
@@ -203,6 +244,7 @@ test("tryDirectAnswer skips importance when trust zone is missing (null)", async
|
|
|
203
244
|
query: "package manager remnic",
|
|
204
245
|
namespace: "default",
|
|
205
246
|
config: BASE_CONFIG,
|
|
247
|
+
enabled: true,
|
|
206
248
|
sources,
|
|
207
249
|
});
|
|
208
250
|
assert.equal(result.eligible, false);
|
|
@@ -229,6 +271,7 @@ test("tryDirectAnswer skips importance when taxonomy bucket is not eligible", as
|
|
|
229
271
|
query: "package manager remnic",
|
|
230
272
|
namespace: "default",
|
|
231
273
|
config: BASE_CONFIG,
|
|
274
|
+
enabled: true,
|
|
232
275
|
sources,
|
|
233
276
|
});
|
|
234
277
|
assert.equal(result.eligible, false);
|
|
@@ -254,6 +297,7 @@ test("tryDirectAnswer returns eligible for a single trusted user-confirmed decis
|
|
|
254
297
|
query: "package manager remnic",
|
|
255
298
|
namespace: "default",
|
|
256
299
|
config: BASE_CONFIG,
|
|
300
|
+
enabled: true,
|
|
257
301
|
sources,
|
|
258
302
|
});
|
|
259
303
|
assert.equal(result.eligible, true);
|
|
@@ -284,6 +328,7 @@ test("tryDirectAnswer defers to hybrid when two trusted candidates are within am
|
|
|
284
328
|
query: "package manager remnic",
|
|
285
329
|
namespace: "default",
|
|
286
330
|
config: BASE_CONFIG,
|
|
331
|
+
enabled: true,
|
|
287
332
|
sources,
|
|
288
333
|
});
|
|
289
334
|
assert.equal(result.eligible, false);
|
|
@@ -327,6 +372,7 @@ test("tryDirectAnswer throws AbortError when signal aborts mid-loop", async () =
|
|
|
327
372
|
query: "package manager remnic",
|
|
328
373
|
namespace: "default",
|
|
329
374
|
config: BASE_CONFIG,
|
|
375
|
+
enabled: true,
|
|
330
376
|
sources,
|
|
331
377
|
abortSignal: controller.signal,
|
|
332
378
|
}),
|
|
@@ -363,6 +409,7 @@ test("tryDirectAnswer throws when abort lands during trustZoneFor on the only me
|
|
|
363
409
|
query: "package manager remnic",
|
|
364
410
|
namespace: "default",
|
|
365
411
|
config: BASE_CONFIG,
|
|
412
|
+
enabled: true,
|
|
366
413
|
sources,
|
|
367
414
|
abortSignal: controller.signal,
|
|
368
415
|
}),
|
|
@@ -391,6 +438,7 @@ test("tryDirectAnswer throws when abort lands during trustZoneFor on the last of
|
|
|
391
438
|
query: "package manager remnic",
|
|
392
439
|
namespace: "default",
|
|
393
440
|
config: BASE_CONFIG,
|
|
441
|
+
enabled: true,
|
|
394
442
|
sources,
|
|
395
443
|
abortSignal: controller.signal,
|
|
396
444
|
}),
|
|
@@ -408,6 +456,7 @@ test("tryDirectAnswer throws when signal is already aborted before I/O", async (
|
|
|
408
456
|
query: "anything",
|
|
409
457
|
namespace: "default",
|
|
410
458
|
config: BASE_CONFIG,
|
|
459
|
+
enabled: true,
|
|
411
460
|
sources,
|
|
412
461
|
abortSignal: controller.signal,
|
|
413
462
|
}),
|
|
@@ -434,6 +483,7 @@ test("tryDirectAnswer passes the requested namespace to listCandidateMemories",
|
|
|
434
483
|
query: "anything",
|
|
435
484
|
namespace: "project-x",
|
|
436
485
|
config: BASE_CONFIG,
|
|
486
|
+
enabled: true,
|
|
437
487
|
sources,
|
|
438
488
|
});
|
|
439
489
|
assert.equal(observedNamespace, "project-x");
|
|
@@ -465,6 +515,7 @@ test("tryDirectAnswer forwards queryEntityRefs to the eligibility gate", async (
|
|
|
465
515
|
query: "package manager remnic",
|
|
466
516
|
namespace: "default",
|
|
467
517
|
config: BASE_CONFIG,
|
|
518
|
+
enabled: true,
|
|
468
519
|
sources,
|
|
469
520
|
queryEntityRefs: ["remnic"],
|
|
470
521
|
});
|
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Short-circuit contract:
|
|
16
16
|
*
|
|
17
|
-
* - When `
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* - When the resolved gate (`input.enabled` if supplied, else
|
|
18
|
+
* `config.recallDirectAnswerEnabled`) is `false`, the function returns the
|
|
19
|
+
* eligibility verdict with reason `"disabled"` without touching any source
|
|
20
|
+
* accessor. This is the documented default.
|
|
20
21
|
* - When enabled, the wiring cheaply drops non-trusted-zone memories
|
|
21
22
|
* and ineligible taxonomy buckets before computing importance, so
|
|
22
23
|
* the eligibility module sees a pre-filtered candidate set. The
|
|
@@ -79,6 +80,15 @@ export interface DirectAnswerWiringInput {
|
|
|
79
80
|
| "recallDirectAnswerAmbiguityMargin"
|
|
80
81
|
| "recallDirectAnswerEligibleTaxonomyBuckets"
|
|
81
82
|
>;
|
|
83
|
+
/**
|
|
84
|
+
* Direct-answer capability gate, resolved once at the recall-operation entry
|
|
85
|
+
* (issue #1523: `caps.recallDirectAnswer`). OPTIONAL and additive: when the
|
|
86
|
+
* caller supplies it, this module and the orchestrator agree on a single
|
|
87
|
+
* resolved gate value for the whole operation. When omitted, we fall back to
|
|
88
|
+
* `config.recallDirectAnswerEnabled` so existing callers on the old input
|
|
89
|
+
* shape keep identical behavior.
|
|
90
|
+
*/
|
|
91
|
+
enabled?: boolean;
|
|
82
92
|
sources: DirectAnswerSources;
|
|
83
93
|
queryEntityRefs?: string[];
|
|
84
94
|
abortSignal?: AbortSignal;
|
|
@@ -92,10 +102,13 @@ export interface DirectAnswerWiringInput {
|
|
|
92
102
|
export async function tryDirectAnswer(
|
|
93
103
|
input: DirectAnswerWiringInput,
|
|
94
104
|
): Promise<DirectAnswerResult> {
|
|
95
|
-
const { query, namespace, config, sources, queryEntityRefs, abortSignal } = input;
|
|
105
|
+
const { query, namespace, config, enabled, sources, queryEntityRefs, abortSignal } = input;
|
|
96
106
|
|
|
97
107
|
const eligibilityConfig: DirectAnswerConfig = {
|
|
98
|
-
|
|
108
|
+
// Prefer the resolved capability when supplied; fall back to the config
|
|
109
|
+
// flag so callers on the old input shape (config-only, no `enabled`) get
|
|
110
|
+
// identical gating (issue #1523 backward-compat).
|
|
111
|
+
enabled: enabled ?? config.recallDirectAnswerEnabled,
|
|
99
112
|
tokenOverlapFloor: config.recallDirectAnswerTokenOverlapFloor,
|
|
100
113
|
importanceFloor: config.recallDirectAnswerImportanceFloor,
|
|
101
114
|
ambiguityMargin: config.recallDirectAnswerAmbiguityMargin,
|
|
@@ -487,13 +487,13 @@ test("StorageRouter integration: catalog registers namespace on storageFor", asy
|
|
|
487
487
|
const config = makeConfig(memoryDir);
|
|
488
488
|
const catalog = new NamespaceCatalog(config);
|
|
489
489
|
const router = new NamespaceStorageRouter(config, {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
490
|
+
// Return the registration promise so the router tracks it as in-flight and
|
|
491
|
+
// `whenResolveHooksSettled()` can await it deterministically (no timer race).
|
|
492
|
+
onResolve: (namespace, storageDir) => catalog.registerResolved(namespace, storageDir),
|
|
493
493
|
});
|
|
494
494
|
await router.storageFor("project-origin-abc123");
|
|
495
|
-
//
|
|
496
|
-
await
|
|
495
|
+
// Deterministically await the fire-and-forget registration instead of sleeping.
|
|
496
|
+
await router.whenResolveHooksSettled();
|
|
497
497
|
|
|
498
498
|
const record = await catalog.getNamespaceRecord("project-origin-abc123");
|
|
499
499
|
assert.ok(record, "storageFor should have registered the namespace");
|
|
@@ -2523,8 +2523,8 @@ test("an async onResolve hook rejection does not crash storage resolution", asyn
|
|
|
2523
2523
|
// Must not throw or produce an unhandled rejection that fails the test.
|
|
2524
2524
|
const sm = await router.storageFor("default");
|
|
2525
2525
|
assert.ok(sm, "storage resolution succeeds despite a rejecting async hook");
|
|
2526
|
-
//
|
|
2527
|
-
await
|
|
2526
|
+
// Deterministically await the swallowed rejection instead of sleeping.
|
|
2527
|
+
await router.whenResolveHooksSettled();
|
|
2528
2528
|
assert.ok(called >= 1, "the async hook was invoked");
|
|
2529
2529
|
} finally {
|
|
2530
2530
|
await rm(memoryDir, { recursive: true, force: true });
|
|
@@ -2564,7 +2564,7 @@ test("concurrent storageFor() for one namespace fires the resolve hook ONCE whil
|
|
|
2564
2564
|
// Let the in-flight registration settle, then a steady-state cache hit must
|
|
2565
2565
|
// still be a catalog no-op (now deduped via notifiedResolved).
|
|
2566
2566
|
release();
|
|
2567
|
-
await
|
|
2567
|
+
await router.whenResolveHooksSettled();
|
|
2568
2568
|
await router.storageFor("project-origin-inflight");
|
|
2569
2569
|
assert.equal(calls, 1, "a steady-state cache hit after settle must not re-fire the hook");
|
|
2570
2570
|
} finally {
|
|
@@ -2589,20 +2589,20 @@ test("a dropped resolve registration (hook returns false) is retried on a later
|
|
|
2589
2589
|
});
|
|
2590
2590
|
|
|
2591
2591
|
await router.storageFor("project-origin-retry");
|
|
2592
|
-
//
|
|
2593
|
-
await
|
|
2592
|
+
// Deterministically await the async hook so the in-flight marker is cleared.
|
|
2593
|
+
await router.whenResolveHooksSettled();
|
|
2594
2594
|
assert.equal(calls, 1, "the hook fired once for the dropped registration");
|
|
2595
2595
|
|
|
2596
2596
|
// Now the registration will succeed; a later resolve must RETRY (not be
|
|
2597
2597
|
// suppressed by a stale in-flight/notified marker from the dropped attempt).
|
|
2598
2598
|
result = undefined; // success (legacy void)
|
|
2599
2599
|
await router.storageFor("project-origin-retry");
|
|
2600
|
-
await
|
|
2600
|
+
await router.whenResolveHooksSettled();
|
|
2601
2601
|
assert.equal(calls, 2, "a dropped registration must be retried on the next storageFor()");
|
|
2602
2602
|
|
|
2603
2603
|
// After a SUCCESSFUL registration, further cache hits are deduped (no retry).
|
|
2604
2604
|
await router.storageFor("project-origin-retry");
|
|
2605
|
-
await
|
|
2605
|
+
await router.whenResolveHooksSettled();
|
|
2606
2606
|
assert.equal(calls, 2, "a successful registration is not re-fired on subsequent cache hits");
|
|
2607
2607
|
} finally {
|
|
2608
2608
|
await rm(memoryDir, { recursive: true, force: true });
|
|
@@ -223,6 +223,11 @@ export class NamespaceStorageRouter {
|
|
|
223
223
|
// entry is always removed when the promise settles, so the map cannot grow
|
|
224
224
|
// unbounded (one transient entry per concurrently-resolving namespace).
|
|
225
225
|
private readonly inFlightResolved = new Map<string, string>();
|
|
226
|
+
// Tracks every in-flight resolve-hook promise so callers can deterministically
|
|
227
|
+
// await the fire-and-forget registrations that `storageFor()` kicks off (see
|
|
228
|
+
// `whenResolveHooksSettled`). Entries are removed as each hook settles, so the
|
|
229
|
+
// set holds at most one promise per concurrently-resolving namespace.
|
|
230
|
+
private readonly pendingResolveHooks = new Set<Promise<unknown>>();
|
|
226
231
|
|
|
227
232
|
// Normalized (trimmed) default namespace identity (NH-FH). `storageFor`
|
|
228
233
|
// normalizes its input, so default-namespace branches must compare against the
|
|
@@ -325,7 +330,10 @@ export class NamespaceStorageRouter {
|
|
|
325
330
|
// the map holds at most one transient entry per concurrently-resolving
|
|
326
331
|
// namespace and cannot grow unbounded.
|
|
327
332
|
this.inFlightResolved.set(namespace, storageDir);
|
|
328
|
-
Promise.resolve(hook(namespace, storageDir))
|
|
333
|
+
const hookResult = Promise.resolve(hook(namespace, storageDir));
|
|
334
|
+
// Track the in-flight promise so `whenResolveHooksSettled()` can await it.
|
|
335
|
+
this.pendingResolveHooks.add(hookResult);
|
|
336
|
+
hookResult.then(
|
|
329
337
|
(persisted) => {
|
|
330
338
|
// Clear the in-flight marker ONLY if it is still ours (a newer resolve
|
|
331
339
|
// for a different dir may have replaced it).
|
|
@@ -338,6 +346,7 @@ export class NamespaceStorageRouter {
|
|
|
338
346
|
// On `false` (dropped touch) we intentionally do NOT mark notified, so
|
|
339
347
|
// a later `storageFor()` retries the registration. Clearing the
|
|
340
348
|
// in-flight marker above is what re-enables that retry.
|
|
349
|
+
this.pendingResolveHooks.delete(hookResult);
|
|
341
350
|
},
|
|
342
351
|
() => {
|
|
343
352
|
// Registration failed — clear in-flight AND do NOT mark as notified, so
|
|
@@ -348,6 +357,7 @@ export class NamespaceStorageRouter {
|
|
|
348
357
|
if (this.notifiedResolved.get(namespace) === storageDir) {
|
|
349
358
|
this.notifiedResolved.delete(namespace);
|
|
350
359
|
}
|
|
360
|
+
this.pendingResolveHooks.delete(hookResult);
|
|
351
361
|
},
|
|
352
362
|
);
|
|
353
363
|
} catch {
|
|
@@ -358,4 +368,21 @@ export class NamespaceStorageRouter {
|
|
|
358
368
|
}
|
|
359
369
|
}
|
|
360
370
|
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Resolve once every in-flight `onResolve` registration has settled.
|
|
374
|
+
*
|
|
375
|
+
* `storageFor()` fires the resolve hook fire-and-forget, so its catalog side
|
|
376
|
+
* effect (e.g. `registerResolved(...)`) is not observable the moment
|
|
377
|
+
* `storageFor()` returns. Callers that must act on that side effect — notably
|
|
378
|
+
* tests asserting the catalog was updated — should await this instead of
|
|
379
|
+
* racing a timer. Resolves immediately when no hook is registered or nothing
|
|
380
|
+
* is in flight. The loop re-checks because a settling hook could, in
|
|
381
|
+
* principle, trigger a follow-on resolution.
|
|
382
|
+
*/
|
|
383
|
+
async whenResolveHooksSettled(): Promise<void> {
|
|
384
|
+
while (this.pendingResolveHooks.size > 0) {
|
|
385
|
+
await Promise.allSettled([...this.pendingResolveHooks]);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
361
388
|
}
|