@remnic/core 9.3.715 → 9.3.717
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-boundary.d.ts +2 -2
- package/dist/access-boundary.js +4 -3
- package/dist/access-cli.js +13 -12
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +2 -2
- package/dist/access-http.js +8 -7
- package/dist/access-mcp.d.ts +2 -2
- package/dist/access-mcp.js +7 -6
- package/dist/access-operations-batch.js +5 -4
- package/dist/access-operations.d.ts +2 -2
- package/dist/access-operations.js +6 -5
- package/dist/{access-service-GgnA--CE.d.ts → access-service-d4Tk-Dlr.d.ts} +2 -2
- package/dist/access-service.d.ts +2 -2
- package/dist/access-service.js +3 -2
- package/dist/access-surface-catalog.d.ts +2 -2
- package/dist/bootstrap.d.ts +1 -1
- package/dist/{chunk-MECU6WSH.js → chunk-2B43BYYZ.js} +6 -6
- package/dist/chunk-2KF4HDG2.js +24 -0
- package/dist/chunk-2KF4HDG2.js.map +1 -0
- package/dist/{chunk-HHLWSWWF.js → chunk-BF2426TN.js} +2 -2
- package/dist/{chunk-RBHANGVX.js → chunk-BZAK3RBN.js} +2 -2
- package/dist/{chunk-YUJL27ET.js → chunk-CTUWGJRI.js} +40 -63
- package/dist/chunk-CTUWGJRI.js.map +1 -0
- package/dist/{chunk-6QUPUTT7.js → chunk-DZYARLJ7.js} +5 -9
- package/dist/{chunk-6QUPUTT7.js.map → chunk-DZYARLJ7.js.map} +1 -1
- package/dist/{chunk-CTAHKIG6.js → chunk-EM22PCQR.js} +7 -7
- package/dist/{chunk-SHRRWOVY.js → chunk-J6BH2OBA.js} +64 -21
- package/dist/chunk-J6BH2OBA.js.map +1 -0
- package/dist/{chunk-GM3BMWKR.js → chunk-KHQ433G5.js} +2 -2
- package/dist/{chunk-SYDHFFAW.js → chunk-OUDAU3T5.js} +4 -4
- package/dist/{chunk-OQH5XUH3.js → chunk-Q6IP2ARR.js} +2 -5
- package/dist/chunk-Q6IP2ARR.js.map +1 -0
- package/dist/{chunk-O62P2LFL.js → chunk-VZQXJNBD.js} +2 -2
- package/dist/{chunk-BNOAB5HH.js → chunk-YAINYEM3.js} +57 -48
- package/dist/chunk-YAINYEM3.js.map +1 -0
- package/dist/{chunk-3VUPWDSE.js → chunk-ZAVTEQFM.js} +4 -4
- package/dist/{chunk-IHMT6XTY.js → chunk-ZXLSNG3T.js} +3 -3
- package/dist/{cli-Do1YtfnH.d.ts → cli-insNLYs6.d.ts} +2 -2
- package/dist/cli.d.ts +3 -3
- package/dist/cli.js +12 -11
- package/dist/direct-answer-wiring.d.ts +9 -9
- package/dist/direct-answer-wiring.js +1 -1
- package/dist/explicit-capture.d.ts +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +23 -22
- package/dist/index.js.map +1 -1
- package/dist/mcp-memory-inspector-app.d.ts +2 -2
- package/dist/operator-toolkit.js +5 -3
- package/dist/orchestration/maintenance.js +5 -2
- package/dist/{orchestrator-BOaVstFG.d.ts → orchestrator-XPKhY2OV.d.ts} +6 -7
- package/dist/orchestrator.d.ts +1 -1
- package/dist/orchestrator.js +7 -6
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-planner-llm.js +2 -2
- package/dist/recall-planner-llm.js.map +1 -1
- package/dist/schemas.d.ts +22 -22
- package/dist/transfer/types.d.ts +12 -12
- package/package.json +2 -2
- package/src/access-service.ts +56 -41
- package/src/capabilities.test.ts +28 -0
- package/src/direct-answer-wiring.test.ts +20 -13
- package/src/direct-answer-wiring.ts +9 -13
- package/src/maintenance/namespace-planner.ts +3 -10
- package/src/orchestrator.ts +42 -71
- package/src/recall-planner-llm.test.ts +30 -11
- package/src/recall-planner-llm.ts +4 -5
- package/src/scopes/scope-plan.test.ts +168 -1
- package/src/scopes/scope-plan.ts +163 -0
- package/dist/chunk-BNOAB5HH.js.map +0 -1
- package/dist/chunk-OQH5XUH3.js.map +0 -1
- package/dist/chunk-SHRRWOVY.js.map +0 -1
- package/dist/chunk-YUJL27ET.js.map +0 -1
- /package/dist/{chunk-MECU6WSH.js.map → chunk-2B43BYYZ.js.map} +0 -0
- /package/dist/{chunk-HHLWSWWF.js.map → chunk-BF2426TN.js.map} +0 -0
- /package/dist/{chunk-RBHANGVX.js.map → chunk-BZAK3RBN.js.map} +0 -0
- /package/dist/{chunk-CTAHKIG6.js.map → chunk-EM22PCQR.js.map} +0 -0
- /package/dist/{chunk-GM3BMWKR.js.map → chunk-KHQ433G5.js.map} +0 -0
- /package/dist/{chunk-SYDHFFAW.js.map → chunk-OUDAU3T5.js.map} +0 -0
- /package/dist/{chunk-O62P2LFL.js.map → chunk-VZQXJNBD.js.map} +0 -0
- /package/dist/{chunk-3VUPWDSE.js.map → chunk-ZAVTEQFM.js.map} +0 -0
- /package/dist/{chunk-IHMT6XTY.js.map → chunk-ZXLSNG3T.js.map} +0 -0
package/src/orchestrator.ts
CHANGED
|
@@ -342,7 +342,11 @@ import {
|
|
|
342
342
|
recallNamespacesForPrincipal,
|
|
343
343
|
resolvePrincipal,
|
|
344
344
|
} from "./namespaces/principal.js";
|
|
345
|
-
import {
|
|
345
|
+
import {
|
|
346
|
+
getConfiguredNamespaces,
|
|
347
|
+
resolveNamespaceFromStorageDir,
|
|
348
|
+
resolveScopePlan,
|
|
349
|
+
} from "./scopes/scope-plan.js";
|
|
346
350
|
import {
|
|
347
351
|
expandScopeProfileReadNamespaces,
|
|
348
352
|
resolveScopeProfilePlan,
|
|
@@ -1396,12 +1400,11 @@ export async function resolveRecallModeDecisionAsync(
|
|
|
1396
1400
|
options: RecallModeGraphOptions & {
|
|
1397
1401
|
config: PluginConfig;
|
|
1398
1402
|
/**
|
|
1399
|
-
* Recall-operation capability gates (issue #1523).
|
|
1400
|
-
*
|
|
1401
|
-
*
|
|
1402
|
-
* planner gate falls back to `config.recallPlannerLlmEnabled` when omitted.
|
|
1403
|
+
* Recall-operation capability gates (issue #1523). REQUIRED: the recall
|
|
1404
|
+
* orchestrator always passes a resolved set — the LLM planner gate reads
|
|
1405
|
+
* `caps.recallPlannerLlm`, never re-derives from config.
|
|
1403
1406
|
*/
|
|
1404
|
-
caps
|
|
1407
|
+
caps: CapabilitySet;
|
|
1405
1408
|
hints?: string[];
|
|
1406
1409
|
llm?: FallbackLlmClient;
|
|
1407
1410
|
signal?: AbortSignal;
|
|
@@ -1410,10 +1413,8 @@ export async function resolveRecallModeDecisionAsync(
|
|
|
1410
1413
|
const heuristicDecision = resolveRecallModeDecision(options);
|
|
1411
1414
|
|
|
1412
1415
|
// Planner globally off, or LLM planning not opted into → heuristic only.
|
|
1413
|
-
//
|
|
1414
|
-
|
|
1415
|
-
const plannerLlmEnabled =
|
|
1416
|
-
options.caps?.recallPlannerLlm ?? options.config.recallPlannerLlmEnabled;
|
|
1416
|
+
// Read the resolved capability (issue #1523) — never re-derive from config.
|
|
1417
|
+
const plannerLlmEnabled = options.caps.recallPlannerLlm;
|
|
1417
1418
|
if (!options.plannerEnabled || !plannerLlmEnabled) {
|
|
1418
1419
|
return heuristicDecision;
|
|
1419
1420
|
}
|
|
@@ -1423,9 +1424,9 @@ export async function resolveRecallModeDecisionAsync(
|
|
|
1423
1424
|
options.prompt,
|
|
1424
1425
|
options.hints,
|
|
1425
1426
|
options.config,
|
|
1427
|
+
options.caps,
|
|
1426
1428
|
options.llm,
|
|
1427
1429
|
options.signal,
|
|
1428
|
-
options.caps,
|
|
1429
1430
|
);
|
|
1430
1431
|
|
|
1431
1432
|
// Shadow mode: record what the LLM would have chosen but keep the heuristic
|
|
@@ -2320,18 +2321,11 @@ export class Orchestrator {
|
|
|
2320
2321
|
return this.storageRouter.storageFor(ns);
|
|
2321
2322
|
}
|
|
2322
2323
|
|
|
2323
|
-
private
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
this.config.sharedNamespace,
|
|
2329
|
-
...this.config.namespacePolicies.map((policy) => policy.name),
|
|
2330
|
-
]
|
|
2331
|
-
.map((value) => value.trim())
|
|
2332
|
-
.filter(Boolean),
|
|
2333
|
-
),
|
|
2334
|
-
);
|
|
2324
|
+
private configuredNamespaceList(): string[] {
|
|
2325
|
+
// #1521: delegates to the scope-module resolver. The inline derivation is
|
|
2326
|
+
// retired so the adHocNamespaceResolutions ratchet no longer counts this
|
|
2327
|
+
// site.
|
|
2328
|
+
return getConfiguredNamespaces(this.config);
|
|
2335
2329
|
}
|
|
2336
2330
|
|
|
2337
2331
|
private rememberNamespaceStorageDirHint(namespace: string, storageDir?: string): void {
|
|
@@ -2459,7 +2453,7 @@ export class Orchestrator {
|
|
|
2459
2453
|
}
|
|
2460
2454
|
|
|
2461
2455
|
const configured = new Set(
|
|
2462
|
-
this.
|
|
2456
|
+
this.configuredNamespaceList().map((namespace) => normalizeNamespaceIdentity(namespace)),
|
|
2463
2457
|
);
|
|
2464
2458
|
const preferredByStorageDir = new Map<
|
|
2465
2459
|
string,
|
|
@@ -2573,7 +2567,7 @@ export class Orchestrator {
|
|
|
2573
2567
|
new Set(
|
|
2574
2568
|
(options.namespaces?.length
|
|
2575
2569
|
? options.namespaces
|
|
2576
|
-
: this.
|
|
2570
|
+
: this.configuredNamespaceList()
|
|
2577
2571
|
)
|
|
2578
2572
|
.map((value) => value.trim())
|
|
2579
2573
|
.filter(Boolean),
|
|
@@ -4141,7 +4135,7 @@ export class Orchestrator {
|
|
|
4141
4135
|
if (options?.dryRun !== true) {
|
|
4142
4136
|
try {
|
|
4143
4137
|
await this.processEntitySynthesisQueue(
|
|
4144
|
-
this.
|
|
4138
|
+
this.storageDirNamespace(targetStorage.dir),
|
|
4145
4139
|
5,
|
|
4146
4140
|
);
|
|
4147
4141
|
} catch (error) {
|
|
@@ -13974,7 +13968,7 @@ export class Orchestrator {
|
|
|
13974
13968
|
)
|
|
13975
13969
|
return false;
|
|
13976
13970
|
if (
|
|
13977
|
-
this.
|
|
13971
|
+
this.storageDirNamespace(targetStorage.dir) ===
|
|
13978
13972
|
this.config.sharedNamespace
|
|
13979
13973
|
)
|
|
13980
13974
|
return false;
|
|
@@ -14800,7 +14794,7 @@ export class Orchestrator {
|
|
|
14800
14794
|
let targetNamespaceName =
|
|
14801
14795
|
baseNamespace && baseNamespace.length > 0
|
|
14802
14796
|
? baseNamespace
|
|
14803
|
-
: this.
|
|
14797
|
+
: this.storageDirNamespace(targetStorage.dir);
|
|
14804
14798
|
let routedRuleId: string | undefined;
|
|
14805
14799
|
let routedNamespaceExplicit = false;
|
|
14806
14800
|
if (routeRules.length > 0) {
|
|
@@ -14841,7 +14835,7 @@ export class Orchestrator {
|
|
|
14841
14835
|
fact.scope === "global" &&
|
|
14842
14836
|
!routedNamespaceExplicit
|
|
14843
14837
|
) {
|
|
14844
|
-
const currentNs = this.
|
|
14838
|
+
const currentNs = this.storageDirNamespace(targetStorage.dir);
|
|
14845
14839
|
if (currentNs !== this.config.sharedNamespace && profileAllowsSharedWrites) {
|
|
14846
14840
|
try {
|
|
14847
14841
|
targetStorage = await this.storageRouter.storageFor(
|
|
@@ -15109,7 +15103,7 @@ export class Orchestrator {
|
|
|
15109
15103
|
this.qmd.isAvailable() &&
|
|
15110
15104
|
faithfulnessEnforceStatus !== "pending_review"
|
|
15111
15105
|
) {
|
|
15112
|
-
const targetNamespace = this.
|
|
15106
|
+
const targetNamespace = this.storageDirNamespace(targetStorage.dir);
|
|
15113
15107
|
const contradiction = await this.checkForContradiction(
|
|
15114
15108
|
fact.content,
|
|
15115
15109
|
writeCategory,
|
|
@@ -15528,7 +15522,7 @@ export class Orchestrator {
|
|
|
15528
15522
|
memoryId: parentId,
|
|
15529
15523
|
category: writeCategory,
|
|
15530
15524
|
content: fact.content,
|
|
15531
|
-
namespace: this.
|
|
15525
|
+
namespace: this.storageDirNamespace(targetStorage.dir),
|
|
15532
15526
|
confidence: fact.confidence,
|
|
15533
15527
|
source: "extraction",
|
|
15534
15528
|
}),
|
|
@@ -15539,7 +15533,7 @@ export class Orchestrator {
|
|
|
15539
15533
|
|
|
15540
15534
|
// Suggest links for this memory (Phase 3A)
|
|
15541
15535
|
if (this.config.memoryLinkingEnabled && this.qmd.isAvailable()) {
|
|
15542
|
-
const targetNamespace = this.
|
|
15536
|
+
const targetNamespace = this.storageDirNamespace(targetStorage.dir);
|
|
15543
15537
|
const suggestedLinks = await this.suggestLinksForMemory(
|
|
15544
15538
|
fact.content,
|
|
15545
15539
|
writeCategory,
|
|
@@ -15645,7 +15639,7 @@ export class Orchestrator {
|
|
|
15645
15639
|
memoryId,
|
|
15646
15640
|
category: writeCategory,
|
|
15647
15641
|
content: fact.content,
|
|
15648
|
-
namespace: this.
|
|
15642
|
+
namespace: this.storageDirNamespace(targetStorage.dir),
|
|
15649
15643
|
confidence: fact.confidence,
|
|
15650
15644
|
source: "extraction",
|
|
15651
15645
|
}),
|
|
@@ -15791,7 +15785,7 @@ export class Orchestrator {
|
|
|
15791
15785
|
const baseTouchNamespace =
|
|
15792
15786
|
baseNamespace && baseNamespace.length > 0
|
|
15793
15787
|
? baseNamespace
|
|
15794
|
-
: this.
|
|
15788
|
+
: this.storageDirNamespace(storage.dir);
|
|
15795
15789
|
};
|
|
15796
15790
|
const recordDurableNonFactWrite = () => {
|
|
15797
15791
|
durableNonFactWritten = true;
|
|
@@ -17982,7 +17976,7 @@ export class Orchestrator {
|
|
|
17982
17976
|
|
|
17983
17977
|
const fallbackNamespace =
|
|
17984
17978
|
fallbackStorageDir !== null
|
|
17985
|
-
? this.
|
|
17979
|
+
? this.storageDirNamespace(fallbackStorageDir)
|
|
17986
17980
|
: this.config.defaultNamespace;
|
|
17987
17981
|
if (
|
|
17988
17982
|
recallNamespaces.length === 0 ||
|
|
@@ -18172,7 +18166,7 @@ export class Orchestrator {
|
|
|
18172
18166
|
|
|
18173
18167
|
const fallbackNamespace =
|
|
18174
18168
|
fallbackStorageDir !== null
|
|
18175
|
-
? this.
|
|
18169
|
+
? this.storageDirNamespace(fallbackStorageDir)
|
|
18176
18170
|
: this.config.defaultNamespace;
|
|
18177
18171
|
maybeAddStorage(fallbackStorage, fallbackNamespace);
|
|
18178
18172
|
|
|
@@ -20037,41 +20031,18 @@ export class Orchestrator {
|
|
|
20037
20031
|
return namespaceIdentityFromToken(m[1]) ?? m[1];
|
|
20038
20032
|
}
|
|
20039
20033
|
|
|
20040
|
-
private
|
|
20041
|
-
|
|
20042
|
-
|
|
20043
|
-
|
|
20044
|
-
|
|
20045
|
-
|
|
20046
|
-
|
|
20047
|
-
|
|
20048
|
-
|
|
20049
|
-
|
|
20050
|
-
|
|
20051
|
-
|
|
20052
|
-
// (`namespaceIdentityToken(decoded) === dirName`) is TAUTOLOGICAL for a
|
|
20053
|
-
// canonical token string, so it cannot tell a tokenized dir for `alpha` apart
|
|
20054
|
-
// from the legacy raw root of a namespace literally named `ns-616c706861`
|
|
20055
|
-
// (codex NRCve). A dir name that is itself a KNOWN namespace (configured or
|
|
20056
|
-
// catalog-owned at this exact storage root) is therefore preserved as the
|
|
20057
|
-
// literal namespace BEFORE attempting to decode it.
|
|
20058
|
-
if (this.configuredNamespaces().includes(dirName)) {
|
|
20059
|
-
return dirName;
|
|
20060
|
-
}
|
|
20061
|
-
this.loadNamespaceStorageDirHintsFromCatalog();
|
|
20062
|
-
const hintedNamespaces = this.namespaceStorageDirHints.get(resolvedStorageDir);
|
|
20063
|
-
if (hintedNamespaces?.has(dirName)) {
|
|
20064
|
-
return dirName;
|
|
20065
|
-
}
|
|
20066
|
-
if (hintedNamespaces?.size === 1) {
|
|
20067
|
-
const [hintedNamespace] = hintedNamespaces;
|
|
20068
|
-
if (hintedNamespace) return hintedNamespace;
|
|
20069
|
-
}
|
|
20070
|
-
const decoded = namespaceIdentityFromToken(dirName);
|
|
20071
|
-
if (decoded && namespaceIdentityToken(decoded) === dirName) {
|
|
20072
|
-
return decoded;
|
|
20073
|
-
}
|
|
20074
|
-
return dirName;
|
|
20034
|
+
private storageDirNamespace(storageDir: string): string {
|
|
20035
|
+
// #1521: delegates to the scope-module resolver. The inline dir→namespace
|
|
20036
|
+
// derivation (token round-trip guard, catalog hints) is retired so the
|
|
20037
|
+
// adHocNamespaceResolutions ratchet no longer counts this site. Hints are
|
|
20038
|
+
// loaded lazily via the callback (only after early returns, matching the
|
|
20039
|
+
// original behavior — codex P2).
|
|
20040
|
+
return resolveNamespaceFromStorageDir(storageDir, {
|
|
20041
|
+
config: this.config,
|
|
20042
|
+
configuredNamespaces: this.configuredNamespaceList(),
|
|
20043
|
+
hints: this.namespaceStorageDirHints,
|
|
20044
|
+
loadHints: () => this.loadNamespaceStorageDirHintsFromCatalog(),
|
|
20045
|
+
});
|
|
20075
20046
|
}
|
|
20076
20047
|
|
|
20077
20048
|
// #1522: catalog touch methods removed — touches now happen at the storage chokepoint.
|
|
@@ -41,7 +41,7 @@ test("returns heuristic without calling the LLM when recallPlannerLlmEnabled is
|
|
|
41
41
|
const captured: Array<Record<string, unknown>> = [];
|
|
42
42
|
const llm = stubLlm({ capturedOptions: captured, result: { mode: "no_recall" } });
|
|
43
43
|
|
|
44
|
-
const result = await planRecallModeLLM("what did we decide about auth?", undefined, config, llm, undefined
|
|
44
|
+
const result = await planRecallModeLLM("what did we decide about auth?", undefined, config, resolveCapabilities(config), llm, undefined);
|
|
45
45
|
|
|
46
46
|
assert.equal(captured.length, 0, "LLM must not be contacted when disabled");
|
|
47
47
|
assert.equal(result.source, "heuristic");
|
|
@@ -55,7 +55,7 @@ test("uses the LLM classification when enabled", async () => {
|
|
|
55
55
|
const config = parseConfig({ recallPlannerLlmEnabled: true });
|
|
56
56
|
const llm = stubLlm({ result: { mode: "graph_mode", reason: "asks for root cause" }, modelUsed: "anthropic/claude" });
|
|
57
57
|
|
|
58
|
-
const result = await planRecallModeLLM("restart the gateway", undefined, config, llm, undefined
|
|
58
|
+
const result = await planRecallModeLLM("restart the gateway", undefined, config, resolveCapabilities(config), llm, undefined);
|
|
59
59
|
|
|
60
60
|
assert.equal(result.source, "llm");
|
|
61
61
|
assert.equal(result.mode, "graph_mode");
|
|
@@ -75,7 +75,7 @@ test("forwards taskModelChain AND recallPlannerModel in gateway mode (provider-a
|
|
|
75
75
|
const captured: Array<Record<string, unknown>> = [];
|
|
76
76
|
const llm = stubLlm({ capturedOptions: captured, result: { mode: "minimal" } });
|
|
77
77
|
|
|
78
|
-
await planRecallModeLLM("check status", undefined, config, llm, undefined
|
|
78
|
+
await planRecallModeLLM("check status", undefined, config, resolveCapabilities(config), llm, undefined);
|
|
79
79
|
|
|
80
80
|
assert.equal(captured.length, 1);
|
|
81
81
|
// recallPlannerModel is tried first (prepended), taskModelChain is the fallback chain.
|
|
@@ -98,7 +98,7 @@ test("plugin mode passes only the explicit model, no gateway chain", async () =>
|
|
|
98
98
|
const captured: Array<Record<string, unknown>> = [];
|
|
99
99
|
const llm = stubLlm({ capturedOptions: captured, result: { mode: "full" } });
|
|
100
100
|
|
|
101
|
-
await planRecallModeLLM("summarize the project", undefined, config, llm, undefined
|
|
101
|
+
await planRecallModeLLM("summarize the project", undefined, config, resolveCapabilities(config), llm, undefined);
|
|
102
102
|
|
|
103
103
|
assert.equal(captured.length, 1);
|
|
104
104
|
assert.equal(captured[0]?.model, "openai/gpt-5.5");
|
|
@@ -110,7 +110,7 @@ test("falls back to heuristic when the LLM throws", async () => {
|
|
|
110
110
|
const config = parseConfig({ recallPlannerLlmEnabled: true });
|
|
111
111
|
const llm = stubLlm({ throwError: "boom" });
|
|
112
112
|
|
|
113
|
-
const result = await planRecallModeLLM("what happened during the outage?", undefined, config, llm, undefined
|
|
113
|
+
const result = await planRecallModeLLM("what happened during the outage?", undefined, config, resolveCapabilities(config), llm, undefined);
|
|
114
114
|
|
|
115
115
|
assert.equal(result.source, "heuristic-fallback");
|
|
116
116
|
assert.equal(result.fallbackUsed, true);
|
|
@@ -124,7 +124,7 @@ test("falls back to heuristic when the LLM returns no parseable result", async (
|
|
|
124
124
|
const config = parseConfig({ recallPlannerLlmEnabled: true });
|
|
125
125
|
const llm = stubLlm({ result: null });
|
|
126
126
|
|
|
127
|
-
const result = await planRecallModeLLM("how did we get here?", undefined, config, llm, undefined
|
|
127
|
+
const result = await planRecallModeLLM("how did we get here?", undefined, config, resolveCapabilities(config), llm, undefined);
|
|
128
128
|
|
|
129
129
|
assert.equal(result.source, "heuristic-fallback");
|
|
130
130
|
assert.equal(result.fallbackUsed, true);
|
|
@@ -140,7 +140,7 @@ test("falls back without a network attempt when the chain is empty and the model
|
|
|
140
140
|
const captured: Array<Record<string, unknown>> = [];
|
|
141
141
|
const llm = stubLlm({ available: false, capturedOptions: captured, result: { mode: "full" } });
|
|
142
142
|
|
|
143
|
-
const result = await planRecallModeLLM("anything", undefined, config, llm, undefined
|
|
143
|
+
const result = await planRecallModeLLM("anything", undefined, config, resolveCapabilities(config), llm, undefined);
|
|
144
144
|
|
|
145
145
|
assert.equal(captured.length, 0, "no network attempt when nothing is routable");
|
|
146
146
|
assert.equal(result.source, "heuristic-fallback");
|
|
@@ -156,7 +156,7 @@ test("attempts the call (and falls back) when a provider-qualified model overrid
|
|
|
156
156
|
const captured: Array<Record<string, unknown>> = [];
|
|
157
157
|
const llm = stubLlm({ available: false, capturedOptions: captured, result: null });
|
|
158
158
|
|
|
159
|
-
const result = await planRecallModeLLM("anything", undefined, config, llm, undefined
|
|
159
|
+
const result = await planRecallModeLLM("anything", undefined, config, resolveCapabilities(config), llm, undefined);
|
|
160
160
|
|
|
161
161
|
assert.equal(captured.length, 1, "qualified model override → still attempt the call");
|
|
162
162
|
assert.equal(captured[0]?.model, "openai/gpt-5.5");
|
|
@@ -171,7 +171,7 @@ test("an already-aborted recall short-circuits to the heuristic without an LLM c
|
|
|
171
171
|
const ac = new AbortController();
|
|
172
172
|
ac.abort();
|
|
173
173
|
|
|
174
|
-
const result = await planRecallModeLLM("what did we decide?", undefined, config, llm, ac.signal
|
|
174
|
+
const result = await planRecallModeLLM("what did we decide?", undefined, config, resolveCapabilities(config), llm, ac.signal);
|
|
175
175
|
|
|
176
176
|
assert.equal(captured.length, 0, "no LLM call when the recall is already aborted");
|
|
177
177
|
assert.equal(result.source, "heuristic-fallback");
|
|
@@ -185,7 +185,7 @@ test("forwards the abort signal into the LLM call (cancellation contract)", asyn
|
|
|
185
185
|
const llm = stubLlm({ capturedOptions: captured, result: { mode: "minimal" } });
|
|
186
186
|
const ac = new AbortController();
|
|
187
187
|
|
|
188
|
-
await planRecallModeLLM("check status", undefined, config, llm, ac.signal
|
|
188
|
+
await planRecallModeLLM("check status", undefined, config, resolveCapabilities(config), llm, ac.signal);
|
|
189
189
|
|
|
190
190
|
assert.equal(captured.length, 1);
|
|
191
191
|
assert.equal(captured[0]?.signal, ac.signal, "recall abort signal must reach FallbackLlmClient");
|
|
@@ -196,7 +196,7 @@ test("empty prompts skip the LLM entirely", async () => {
|
|
|
196
196
|
const captured: Array<Record<string, unknown>> = [];
|
|
197
197
|
const llm = stubLlm({ capturedOptions: captured, result: { mode: "full" } });
|
|
198
198
|
|
|
199
|
-
const result = await planRecallModeLLM(" ", undefined, config, llm, undefined
|
|
199
|
+
const result = await planRecallModeLLM(" ", undefined, config, resolveCapabilities(config), llm, undefined);
|
|
200
200
|
|
|
201
201
|
assert.equal(captured.length, 0);
|
|
202
202
|
assert.equal(result.mode, "no_recall"); // heuristic returns no_recall for empty
|
|
@@ -223,3 +223,22 @@ test("resolveRecallPlannerLlmOptions drops bare model names but keeps provider-q
|
|
|
223
223
|
);
|
|
224
224
|
assert.equal(qualified.model, "anthropic/claude-haiku-4-5");
|
|
225
225
|
});
|
|
226
|
+
|
|
227
|
+
test("caps.recallPlannerLlm is authoritative — disabled caps wins over enabled config flag (one-resolution-per-op #1523)", async () => {
|
|
228
|
+
// Issue #1523 contract: the capability resolved at the operation entry is
|
|
229
|
+
// THE gate. Construct a deliberate mismatch: config says enabled, but the
|
|
230
|
+
// resolved CapabilitySet says disabled. The function must NOT call the LLM.
|
|
231
|
+
const config = parseConfig({ recallPlannerLlmEnabled: true });
|
|
232
|
+
const captured: Array<Record<string, unknown>> = [];
|
|
233
|
+
const llm = stubLlm({ capturedOptions: captured, result: { mode: "full" } });
|
|
234
|
+
|
|
235
|
+
// Manually construct caps with recallPlannerLlm: false (simulating a
|
|
236
|
+
// resolution that disagrees with the raw config — e.g. session toggle off).
|
|
237
|
+
const caps = { ...resolveCapabilities(config), recallPlannerLlm: false } as any;
|
|
238
|
+
|
|
239
|
+
const result = await planRecallModeLLM("what did we decide?", undefined, config, caps, llm, undefined);
|
|
240
|
+
|
|
241
|
+
assert.equal(captured.length, 0, "LLM must not be contacted when caps says disabled, even if config says enabled");
|
|
242
|
+
assert.equal(result.source, "heuristic");
|
|
243
|
+
assert.equal(result.reason, "llm-disabled");
|
|
244
|
+
});
|
|
@@ -188,16 +188,15 @@ export async function planRecallModeLLM(
|
|
|
188
188
|
prompt: string,
|
|
189
189
|
hints: string[] | undefined,
|
|
190
190
|
config: PluginConfig,
|
|
191
|
+
caps: CapabilitySet,
|
|
191
192
|
llm?: FallbackLlmClient,
|
|
192
193
|
signal?: AbortSignal,
|
|
193
|
-
caps?: CapabilitySet,
|
|
194
194
|
): Promise<RecallPlannerLlmResult> {
|
|
195
195
|
const heuristicMode = planRecallMode(prompt);
|
|
196
196
|
|
|
197
|
-
//
|
|
198
|
-
// capability
|
|
199
|
-
|
|
200
|
-
const plannerLlmEnabled = caps?.recallPlannerLlm ?? config.recallPlannerLlmEnabled;
|
|
197
|
+
// The recall-operation entry resolved this gate once (issue #1523); read the
|
|
198
|
+
// capability, never re-derive it from raw config here.
|
|
199
|
+
const plannerLlmEnabled = caps.recallPlannerLlm;
|
|
201
200
|
if (!plannerLlmEnabled) {
|
|
202
201
|
return heuristicResult(heuristicMode, "heuristic", "llm-disabled", 0, false);
|
|
203
202
|
}
|
|
@@ -21,7 +21,12 @@
|
|
|
21
21
|
import assert from "node:assert/strict";
|
|
22
22
|
import test from "node:test";
|
|
23
23
|
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
getConfiguredNamespaces,
|
|
26
|
+
resolveNamespaceFromStorageDir,
|
|
27
|
+
resolveScopePlan,
|
|
28
|
+
resolveWritableNamespaceValue,
|
|
29
|
+
} from "./scope-plan.js";
|
|
25
30
|
import {
|
|
26
31
|
combineNamespaces,
|
|
27
32
|
lcmSessionKeyForNamespace,
|
|
@@ -358,3 +363,165 @@ test("scope-plan: LCM session ids match lcmSessionKeyForNamespace encoding", ()
|
|
|
358
363
|
);
|
|
359
364
|
assert.deepEqual([...plan.lcmReadSessionIds], expected);
|
|
360
365
|
});
|
|
366
|
+
|
|
367
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
368
|
+
// Fitness tests: centralized namespace-resolution helpers (#1521 step 3–4)
|
|
369
|
+
//
|
|
370
|
+
// These pin the three pure helpers that replace the ad-hoc resolution scattered
|
|
371
|
+
// across orchestrator.ts and access-service.ts. They MUST match the pre-migration
|
|
372
|
+
// behavior byte-for-byte (the snapshots were derived from the original inline
|
|
373
|
+
// implementations).
|
|
374
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
375
|
+
|
|
376
|
+
test("getConfiguredNamespaces: default + shared + policies, trimmed and deduped", () => {
|
|
377
|
+
const config = baseConfig({
|
|
378
|
+
defaultNamespace: "default",
|
|
379
|
+
sharedNamespace: "shared",
|
|
380
|
+
namespacePolicies: [
|
|
381
|
+
{ name: "team-a", readPrincipals: ["*"], writePrincipals: ["*"] },
|
|
382
|
+
{ name: "team-b", readPrincipals: ["*"], writePrincipals: ["*"] },
|
|
383
|
+
],
|
|
384
|
+
});
|
|
385
|
+
const ns = getConfiguredNamespaces(config);
|
|
386
|
+
assert.deepEqual(ns, ["default", "shared", "team-a", "team-b"]);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test("getConfiguredNamespaces: trims whitespace and drops empty", () => {
|
|
390
|
+
const config = baseConfig({
|
|
391
|
+
defaultNamespace: " default ",
|
|
392
|
+
sharedNamespace: "",
|
|
393
|
+
namespacePolicies: [
|
|
394
|
+
{ name: "team-a ", readPrincipals: ["*"], writePrincipals: ["*"] },
|
|
395
|
+
],
|
|
396
|
+
});
|
|
397
|
+
const ns = getConfiguredNamespaces(config);
|
|
398
|
+
assert.deepEqual(ns, ["default", "team-a"]);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("getConfiguredNamespaces: deduplicates identical names", () => {
|
|
402
|
+
const config = baseConfig({
|
|
403
|
+
defaultNamespace: "default",
|
|
404
|
+
sharedNamespace: "default",
|
|
405
|
+
namespacePolicies: [
|
|
406
|
+
{ name: "default", readPrincipals: ["*"], writePrincipals: ["*"] },
|
|
407
|
+
],
|
|
408
|
+
});
|
|
409
|
+
const ns = getConfiguredNamespaces(config);
|
|
410
|
+
assert.deepEqual(ns, ["default"]);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// ── resolveNamespaceFromStorageDir ──────────────────────────────────────────
|
|
414
|
+
|
|
415
|
+
test("resolveNamespaceFromStorageDir: namespaces disabled → always default", () => {
|
|
416
|
+
const config = baseConfig({ namespacesEnabled: false });
|
|
417
|
+
const ns = resolveNamespaceFromStorageDir("/some/path/namespaces/team-a", {
|
|
418
|
+
config,
|
|
419
|
+
configuredNamespaces: getConfiguredNamespaces(config),
|
|
420
|
+
});
|
|
421
|
+
assert.equal(ns, "default");
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("resolveNamespaceFromStorageDir: memory root → default", () => {
|
|
425
|
+
const config = baseConfig({ memoryDir: "/data/memory" });
|
|
426
|
+
const ns = resolveNamespaceFromStorageDir("/data/memory", {
|
|
427
|
+
config,
|
|
428
|
+
configuredNamespaces: getConfiguredNamespaces(config),
|
|
429
|
+
});
|
|
430
|
+
assert.equal(ns, "default");
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("resolveNamespaceFromStorageDir: configured namespace dir → that namespace", () => {
|
|
434
|
+
const config = baseConfig({
|
|
435
|
+
memoryDir: "/data/memory",
|
|
436
|
+
namespacePolicies: [
|
|
437
|
+
{ name: "team-a", readPrincipals: ["*"], writePrincipals: ["*"] },
|
|
438
|
+
],
|
|
439
|
+
});
|
|
440
|
+
const ns = resolveNamespaceFromStorageDir("/data/memory/namespaces/team-a", {
|
|
441
|
+
config,
|
|
442
|
+
configuredNamespaces: getConfiguredNamespaces(config),
|
|
443
|
+
});
|
|
444
|
+
assert.equal(ns, "team-a");
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test("resolveNamespaceFromStorageDir: tokenized dir → decoded namespace", () => {
|
|
448
|
+
const config = baseConfig({ memoryDir: "/data/memory" });
|
|
449
|
+
// "alpha" tokenizes to ns-616c706861
|
|
450
|
+
const ns = resolveNamespaceFromStorageDir("/data/memory/namespaces/ns-616c706861", {
|
|
451
|
+
config,
|
|
452
|
+
configuredNamespaces: getConfiguredNamespaces(config),
|
|
453
|
+
});
|
|
454
|
+
assert.equal(ns, "alpha");
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test("resolveNamespaceFromStorageDir: unknown dir → literal dir name", () => {
|
|
458
|
+
const config = baseConfig({ memoryDir: "/data/memory" });
|
|
459
|
+
const ns = resolveNamespaceFromStorageDir("/data/memory/namespaces/custom-ns", {
|
|
460
|
+
config,
|
|
461
|
+
configuredNamespaces: getConfiguredNamespaces(config),
|
|
462
|
+
});
|
|
463
|
+
assert.equal(ns, "custom-ns");
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test("resolveNamespaceFromStorageDir: catalog hint resolves single-hint dir", () => {
|
|
467
|
+
const config = baseConfig({ memoryDir: "/data/memory" });
|
|
468
|
+
const resolvedDir = "/data/memory/namespaces/dynamic-xyz";
|
|
469
|
+
const hints = new Map([[resolvedDir, new Set(["my-dynamic-ns"])]]);
|
|
470
|
+
const ns = resolveNamespaceFromStorageDir(resolvedDir, {
|
|
471
|
+
config,
|
|
472
|
+
configuredNamespaces: getConfiguredNamespaces(config),
|
|
473
|
+
hints,
|
|
474
|
+
});
|
|
475
|
+
assert.equal(ns, "my-dynamic-ns");
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// ── resolveWritableNamespaceValue ───────────────────────────────────────────
|
|
479
|
+
|
|
480
|
+
test("resolveWritableNamespaceValue: undefined namespace → default, ok", () => {
|
|
481
|
+
const config = baseConfig();
|
|
482
|
+
const result = resolveWritableNamespaceValue(undefined, "sess-1", undefined, config);
|
|
483
|
+
assert.equal(result.ok, true);
|
|
484
|
+
if (result.ok) assert.equal(result.namespace, "default");
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test("resolveWritableNamespaceValue: explicit namespace, writable principal → ok", () => {
|
|
488
|
+
const config = baseConfig({
|
|
489
|
+
namespacePolicies: [
|
|
490
|
+
{ name: "team-a", readPrincipals: ["alice"], writePrincipals: ["alice"] },
|
|
491
|
+
],
|
|
492
|
+
});
|
|
493
|
+
const result = resolveWritableNamespaceValue("team-a", "alice:sess-1", "alice", config);
|
|
494
|
+
assert.equal(result.ok, true);
|
|
495
|
+
if (result.ok) assert.equal(result.namespace, "team-a");
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test("resolveWritableNamespaceValue: explicit namespace, non-writable principal → not_writable", () => {
|
|
499
|
+
const config = baseConfig({
|
|
500
|
+
namespacePolicies: [
|
|
501
|
+
{ name: "team-a", readPrincipals: ["alice"], writePrincipals: ["alice"] },
|
|
502
|
+
],
|
|
503
|
+
});
|
|
504
|
+
const result = resolveWritableNamespaceValue("team-a", "bob:sess-1", "bob", config);
|
|
505
|
+
assert.equal(result.ok, false);
|
|
506
|
+
if (!result.ok) {
|
|
507
|
+
assert.equal(result.reason, "not_writable");
|
|
508
|
+
assert.equal(result.namespace, "team-a");
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test("resolveWritableNamespaceValue: non-default namespace with namespaces disabled → unsupported", () => {
|
|
513
|
+
const config = baseConfig({ namespacesEnabled: false });
|
|
514
|
+
const result = resolveWritableNamespaceValue("custom-ns", "sess-1", undefined, config);
|
|
515
|
+
assert.equal(result.ok, false);
|
|
516
|
+
if (!result.ok) {
|
|
517
|
+
assert.equal(result.reason, "unsupported");
|
|
518
|
+
assert.equal(result.namespace, "custom-ns");
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test("resolveWritableNamespaceValue: namespaces disabled, default namespace → ok", () => {
|
|
523
|
+
const config = baseConfig({ namespacesEnabled: false });
|
|
524
|
+
const result = resolveWritableNamespaceValue("default", "sess-1", undefined, config);
|
|
525
|
+
assert.equal(result.ok, true);
|
|
526
|
+
if (result.ok) assert.equal(result.namespace, "default");
|
|
527
|
+
});
|