@exulu/backend 3.2.0 → 3.3.1
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/{chunk-CVQTDG37.js → chunk-5FTX543Z.js} +49 -20
- package/dist/{convert-exulu-tools-to-ai-sdk-tools-QG7E6UX5.js → convert-exulu-tools-to-ai-sdk-tools-WQWYMU7G.js} +1 -1
- package/dist/index.cjs +101 -21
- package/dist/index.d.cts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +46 -3
- package/ee/agentic-retrieval/pipeline/index.test.ts +4 -4
- package/ee/agentic-retrieval/pipeline/index.ts +6 -4
- package/ee/agentic-retrieval/pipeline/memory.test.ts +4 -2
- package/ee/agentic-retrieval/pipeline/memory.ts +20 -13
- package/ee/agentic-retrieval/pipeline/search.test.ts +19 -4
- package/ee/agentic-retrieval/pipeline/search.ts +8 -4
- package/ee/agentic-retrieval/pipeline/types.ts +1 -1
- package/package.json +1 -1
|
@@ -1733,7 +1733,7 @@ var ExuluTool = class _ExuluTool {
|
|
|
1733
1733
|
if (!agent) {
|
|
1734
1734
|
throw new Error("Agent not found.");
|
|
1735
1735
|
}
|
|
1736
|
-
const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-
|
|
1736
|
+
const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-WQWYMU7G.js");
|
|
1737
1737
|
const tools = await convertExuluToolsToAiSdkTools2(
|
|
1738
1738
|
[this],
|
|
1739
1739
|
[],
|
|
@@ -3008,7 +3008,7 @@ function neutralResult(question, keywords, importantKeyword, steps = []) {
|
|
|
3008
3008
|
return {
|
|
3009
3009
|
memoryChunksForAnswer: [],
|
|
3010
3010
|
memoryOverride: { active: false, chunks: [], reason: "" },
|
|
3011
|
-
|
|
3011
|
+
memoryPinnedItemIdsByContext: /* @__PURE__ */ new Map(),
|
|
3012
3012
|
updatedQuestion: question,
|
|
3013
3013
|
updatedKeywords: keywords,
|
|
3014
3014
|
updatedImportantKeyword: importantKeyword,
|
|
@@ -3109,7 +3109,7 @@ async function runMemoryPhase({
|
|
|
3109
3109
|
chunks: [],
|
|
3110
3110
|
reason: ""
|
|
3111
3111
|
};
|
|
3112
|
-
|
|
3112
|
+
const memoryPinnedItemIdsByContext = /* @__PURE__ */ new Map();
|
|
3113
3113
|
let updatedQuestion = question;
|
|
3114
3114
|
let updatedKeywords = keywords;
|
|
3115
3115
|
let updatedImportantKeyword = importantKeyword;
|
|
@@ -3272,25 +3272,30 @@ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
|
|
|
3272
3272
|
if (fileResult.output?.shouldPrioritizeFiles && fileResult.output?.fileNameHints?.length) {
|
|
3273
3273
|
const hints = fileResult.output.fileNameHints;
|
|
3274
3274
|
const pinResults = await Promise.all(
|
|
3275
|
-
documentContexts.map(
|
|
3276
|
-
|
|
3275
|
+
documentContexts.map(async (ctx) => ({
|
|
3276
|
+
ctxId: ctx.id,
|
|
3277
|
+
matches: await fuzzyPrefilter({
|
|
3277
3278
|
cacheKey: `memory-pin:${ctx.id}`,
|
|
3278
3279
|
relevantKeywords: hints,
|
|
3279
3280
|
context: ctx,
|
|
3280
3281
|
fields: ["name", "id", "external_id"],
|
|
3281
3282
|
normalize: (item) => item.external_id ? normalizeFileName(item.external_id) : item.name
|
|
3282
3283
|
}).catch(() => [])
|
|
3283
|
-
)
|
|
3284
|
+
}))
|
|
3284
3285
|
);
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3286
|
+
const pinnedNames = [];
|
|
3287
|
+
for (const { ctxId, matches } of pinResults) {
|
|
3288
|
+
if (!matches.length) continue;
|
|
3289
|
+
const set = memoryPinnedItemIdsByContext.get(ctxId) ?? /* @__PURE__ */ new Set();
|
|
3290
|
+
for (const m of matches) {
|
|
3291
|
+
set.add(m.id);
|
|
3292
|
+
pinnedNames.push(m.name);
|
|
3288
3293
|
}
|
|
3294
|
+
memoryPinnedItemIdsByContext.set(ctxId, set);
|
|
3289
3295
|
}
|
|
3290
|
-
if (
|
|
3291
|
-
const names = pinResults.flat().map((i) => i.name).join(", ");
|
|
3296
|
+
if (pinnedNames.length > 0) {
|
|
3292
3297
|
steps.push({
|
|
3293
|
-
text: `Memory prioritizes specific document(s); pinning ${
|
|
3298
|
+
text: `Memory prioritizes specific document(s); pinning ${pinnedNames.length} file(s) into the search: ${pinnedNames.join(", ")}`
|
|
3294
3299
|
});
|
|
3295
3300
|
}
|
|
3296
3301
|
}
|
|
@@ -3313,7 +3318,7 @@ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
|
|
|
3313
3318
|
return {
|
|
3314
3319
|
memoryChunksForAnswer,
|
|
3315
3320
|
memoryOverride,
|
|
3316
|
-
|
|
3321
|
+
memoryPinnedItemIdsByContext,
|
|
3317
3322
|
updatedQuestion,
|
|
3318
3323
|
updatedKeywords,
|
|
3319
3324
|
updatedImportantKeyword,
|
|
@@ -3420,7 +3425,7 @@ async function searchContexts(opts) {
|
|
|
3420
3425
|
preselectedItems,
|
|
3421
3426
|
scopedItemsByContext,
|
|
3422
3427
|
identifierPinsByContext,
|
|
3423
|
-
|
|
3428
|
+
memoryPinnedItemIdsByContext,
|
|
3424
3429
|
userPinnedItemIdsByContext,
|
|
3425
3430
|
rewrites,
|
|
3426
3431
|
styleHint,
|
|
@@ -3451,7 +3456,8 @@ async function searchContexts(opts) {
|
|
|
3451
3456
|
const identifierPins = identifierPinsByContext.get(ctxId) ?? /* @__PURE__ */ new Set();
|
|
3452
3457
|
let pins = new Set(identifierPins);
|
|
3453
3458
|
if (kind === "documents") {
|
|
3454
|
-
|
|
3459
|
+
const memPins = memoryPinnedItemIdsByContext.get(ctxId);
|
|
3460
|
+
if (memPins) for (const id of memPins) pins.add(id);
|
|
3455
3461
|
}
|
|
3456
3462
|
const userPins = userPinnedItemIdsByContext.get(ctxId);
|
|
3457
3463
|
if (userPins && userPins.size > 0) {
|
|
@@ -3963,7 +3969,7 @@ ${projectScope.customInstructions}` : ""
|
|
|
3963
3969
|
updatedQuestion,
|
|
3964
3970
|
updatedKeywords,
|
|
3965
3971
|
updatedImportantKeyword,
|
|
3966
|
-
|
|
3972
|
+
memoryPinnedItemIdsByContext,
|
|
3967
3973
|
memoryOverride
|
|
3968
3974
|
} = memResult;
|
|
3969
3975
|
const { pinsByContext: identifierPinsByContext, exactPinsByContext, steps: pinSteps } = await resolveIdentifierPins({
|
|
@@ -3996,7 +4002,7 @@ ${projectScope.customInstructions}` : ""
|
|
|
3996
4002
|
model: utilityModel,
|
|
3997
4003
|
preselectedItems,
|
|
3998
4004
|
identifierPinsByContext,
|
|
3999
|
-
|
|
4005
|
+
memoryPinnedItemIdsByContext,
|
|
4000
4006
|
userPinnedItemIdsByContext,
|
|
4001
4007
|
scopedItemsByContext: resolvedProject?.scopedItemsByContext,
|
|
4002
4008
|
rewrites: cfg.vocabulary.rewrites,
|
|
@@ -4016,7 +4022,7 @@ ${projectScope.customInstructions}` : ""
|
|
|
4016
4022
|
model: utilityModel,
|
|
4017
4023
|
preselectedItems,
|
|
4018
4024
|
identifierPinsByContext,
|
|
4019
|
-
|
|
4025
|
+
memoryPinnedItemIdsByContext,
|
|
4020
4026
|
userPinnedItemIdsByContext,
|
|
4021
4027
|
scopedItemsByContext: resolvedProject?.scopedItemsByContext,
|
|
4022
4028
|
rewrites: cfg.vocabulary.rewrites,
|
|
@@ -4026,7 +4032,9 @@ ${projectScope.customInstructions}` : ""
|
|
|
4026
4032
|
}) : Promise.resolve({ chunks: [] })
|
|
4027
4033
|
]);
|
|
4028
4034
|
const pinnedItemIds = /* @__PURE__ */ new Set([
|
|
4029
|
-
...
|
|
4035
|
+
...(function* () {
|
|
4036
|
+
for (const s of memoryPinnedItemIdsByContext.values()) yield* s;
|
|
4037
|
+
})(),
|
|
4030
4038
|
...(function* () {
|
|
4031
4039
|
for (const s of exactPinsByContext.values()) yield* s;
|
|
4032
4040
|
})(),
|
|
@@ -7483,6 +7491,7 @@ var buildToolCallEvent = async (ctx, opts) => {
|
|
|
7483
7491
|
toolCallId: ctx.toolCallId
|
|
7484
7492
|
},
|
|
7485
7493
|
target: { kind: "tool", id: ctx.tool.id, name: ctx.tool.name, category: ctx.tool.category, builtin: ctx.builtin },
|
|
7494
|
+
...ctx.client ? { client: ctx.client } : {},
|
|
7486
7495
|
...credential ? { credential } : {},
|
|
7487
7496
|
status,
|
|
7488
7497
|
...status === "error" ? { error: { name: err?.name, message: String(err?.message ?? err ?? "unknown error") } } : {},
|
|
@@ -7599,6 +7608,25 @@ var emitToolCallAudit = async (logger, ctx) => {
|
|
|
7599
7608
|
);
|
|
7600
7609
|
};
|
|
7601
7610
|
|
|
7611
|
+
// src/exulu/audit/client-info.ts
|
|
7612
|
+
var firstHeader = (v) => Array.isArray(v) ? v[0] : v ?? void 0;
|
|
7613
|
+
function extractClientInfo(req) {
|
|
7614
|
+
if (!req) return void 0;
|
|
7615
|
+
const headers = req.headers ?? {};
|
|
7616
|
+
const forwardedFor = firstHeader(headers["x-forwarded-for"]);
|
|
7617
|
+
const ip = (forwardedFor ? forwardedFor.split(",")[0]?.trim() : void 0) || req.ip || req.socket?.remoteAddress || void 0;
|
|
7618
|
+
const userAgent = firstHeader(headers["user-agent"]);
|
|
7619
|
+
const referer = firstHeader(headers["referer"]);
|
|
7620
|
+
const origin = firstHeader(headers["origin"]);
|
|
7621
|
+
const client = {};
|
|
7622
|
+
if (ip) client.ip = ip;
|
|
7623
|
+
if (userAgent) client.userAgent = userAgent;
|
|
7624
|
+
if (referer) client.referer = referer;
|
|
7625
|
+
if (origin) client.origin = origin;
|
|
7626
|
+
if (forwardedFor) client.forwardedFor = forwardedFor;
|
|
7627
|
+
return Object.keys(client).length > 0 ? client : void 0;
|
|
7628
|
+
}
|
|
7629
|
+
|
|
7602
7630
|
// src/templates/tools/convert-exulu-tools-to-ai-sdk-tools.ts
|
|
7603
7631
|
var OUTPUT_OFFLOAD_EXEMPT_TOOL_IDS = /* @__PURE__ */ new Set(["agentic_context_search"]);
|
|
7604
7632
|
var generateS3Key = (filename) => `${randomUUID5()}-${filename}`;
|
|
@@ -8080,7 +8108,8 @@ var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approved
|
|
|
8080
8108
|
input: inputs,
|
|
8081
8109
|
output: __auditOutput,
|
|
8082
8110
|
status: __auditStatus,
|
|
8083
|
-
error: __auditError
|
|
8111
|
+
error: __auditError,
|
|
8112
|
+
client: extractClientInfo(req)
|
|
8084
8113
|
});
|
|
8085
8114
|
if (__auditLogger.failClosed) {
|
|
8086
8115
|
await __emit;
|
package/dist/index.cjs
CHANGED
|
@@ -5699,6 +5699,7 @@ var init_tool_call = __esm({
|
|
|
5699
5699
|
toolCallId: ctx.toolCallId
|
|
5700
5700
|
},
|
|
5701
5701
|
target: { kind: "tool", id: ctx.tool.id, name: ctx.tool.name, category: ctx.tool.category, builtin: ctx.builtin },
|
|
5702
|
+
...ctx.client ? { client: ctx.client } : {},
|
|
5702
5703
|
...credential ? { credential } : {},
|
|
5703
5704
|
status,
|
|
5704
5705
|
...status === "error" ? { error: { name: err?.name, message: String(err?.message ?? err ?? "unknown error") } } : {},
|
|
@@ -5834,6 +5835,32 @@ var init_emit_tool_call = __esm({
|
|
|
5834
5835
|
}
|
|
5835
5836
|
});
|
|
5836
5837
|
|
|
5838
|
+
// src/exulu/audit/client-info.ts
|
|
5839
|
+
function extractClientInfo(req) {
|
|
5840
|
+
if (!req) return void 0;
|
|
5841
|
+
const headers = req.headers ?? {};
|
|
5842
|
+
const forwardedFor = firstHeader(headers["x-forwarded-for"]);
|
|
5843
|
+
const ip = (forwardedFor ? forwardedFor.split(",")[0]?.trim() : void 0) || req.ip || req.socket?.remoteAddress || void 0;
|
|
5844
|
+
const userAgent = firstHeader(headers["user-agent"]);
|
|
5845
|
+
const referer = firstHeader(headers["referer"]);
|
|
5846
|
+
const origin = firstHeader(headers["origin"]);
|
|
5847
|
+
const client2 = {};
|
|
5848
|
+
if (ip) client2.ip = ip;
|
|
5849
|
+
if (userAgent) client2.userAgent = userAgent;
|
|
5850
|
+
if (referer) client2.referer = referer;
|
|
5851
|
+
if (origin) client2.origin = origin;
|
|
5852
|
+
if (forwardedFor) client2.forwardedFor = forwardedFor;
|
|
5853
|
+
return Object.keys(client2).length > 0 ? client2 : void 0;
|
|
5854
|
+
}
|
|
5855
|
+
var firstHeader;
|
|
5856
|
+
var init_client_info = __esm({
|
|
5857
|
+
"src/exulu/audit/client-info.ts"() {
|
|
5858
|
+
"use strict";
|
|
5859
|
+
init_cjs_shims();
|
|
5860
|
+
firstHeader = (v) => Array.isArray(v) ? v[0] : v ?? void 0;
|
|
5861
|
+
}
|
|
5862
|
+
});
|
|
5863
|
+
|
|
5837
5864
|
// src/templates/tools/convert-exulu-tools-to-ai-sdk-tools.ts
|
|
5838
5865
|
var convert_exulu_tools_to_ai_sdk_tools_exports = {};
|
|
5839
5866
|
__export(convert_exulu_tools_to_ai_sdk_tools_exports, {
|
|
@@ -5869,6 +5896,7 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
|
|
|
5869
5896
|
init_context_budget();
|
|
5870
5897
|
init_logger();
|
|
5871
5898
|
init_emit_tool_call();
|
|
5899
|
+
init_client_info();
|
|
5872
5900
|
OUTPUT_OFFLOAD_EXEMPT_TOOL_IDS = /* @__PURE__ */ new Set(["agentic_context_search"]);
|
|
5873
5901
|
generateS3Key = (filename) => `${(0, import_node_crypto4.randomUUID)()}-${filename}`;
|
|
5874
5902
|
getMimeType = (type) => {
|
|
@@ -6348,7 +6376,8 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
|
|
|
6348
6376
|
input: inputs,
|
|
6349
6377
|
output: __auditOutput,
|
|
6350
6378
|
status: __auditStatus,
|
|
6351
|
-
error: __auditError
|
|
6379
|
+
error: __auditError,
|
|
6380
|
+
client: extractClientInfo(req)
|
|
6352
6381
|
});
|
|
6353
6382
|
if (__auditLogger.failClosed) {
|
|
6354
6383
|
await __emit;
|
|
@@ -7580,7 +7609,7 @@ function neutralResult(question, keywords, importantKeyword, steps = []) {
|
|
|
7580
7609
|
return {
|
|
7581
7610
|
memoryChunksForAnswer: [],
|
|
7582
7611
|
memoryOverride: { active: false, chunks: [], reason: "" },
|
|
7583
|
-
|
|
7612
|
+
memoryPinnedItemIdsByContext: /* @__PURE__ */ new Map(),
|
|
7584
7613
|
updatedQuestion: question,
|
|
7585
7614
|
updatedKeywords: keywords,
|
|
7586
7615
|
updatedImportantKeyword: importantKeyword,
|
|
@@ -7681,7 +7710,7 @@ async function runMemoryPhase({
|
|
|
7681
7710
|
chunks: [],
|
|
7682
7711
|
reason: ""
|
|
7683
7712
|
};
|
|
7684
|
-
|
|
7713
|
+
const memoryPinnedItemIdsByContext = /* @__PURE__ */ new Map();
|
|
7685
7714
|
let updatedQuestion = question;
|
|
7686
7715
|
let updatedKeywords = keywords;
|
|
7687
7716
|
let updatedImportantKeyword = importantKeyword;
|
|
@@ -7844,25 +7873,30 @@ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
|
|
|
7844
7873
|
if (fileResult.output?.shouldPrioritizeFiles && fileResult.output?.fileNameHints?.length) {
|
|
7845
7874
|
const hints = fileResult.output.fileNameHints;
|
|
7846
7875
|
const pinResults = await Promise.all(
|
|
7847
|
-
documentContexts.map(
|
|
7848
|
-
|
|
7876
|
+
documentContexts.map(async (ctx) => ({
|
|
7877
|
+
ctxId: ctx.id,
|
|
7878
|
+
matches: await fuzzyPrefilter({
|
|
7849
7879
|
cacheKey: `memory-pin:${ctx.id}`,
|
|
7850
7880
|
relevantKeywords: hints,
|
|
7851
7881
|
context: ctx,
|
|
7852
7882
|
fields: ["name", "id", "external_id"],
|
|
7853
7883
|
normalize: (item) => item.external_id ? normalizeFileName(item.external_id) : item.name
|
|
7854
7884
|
}).catch(() => [])
|
|
7855
|
-
)
|
|
7885
|
+
}))
|
|
7856
7886
|
);
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7887
|
+
const pinnedNames = [];
|
|
7888
|
+
for (const { ctxId, matches } of pinResults) {
|
|
7889
|
+
if (!matches.length) continue;
|
|
7890
|
+
const set = memoryPinnedItemIdsByContext.get(ctxId) ?? /* @__PURE__ */ new Set();
|
|
7891
|
+
for (const m of matches) {
|
|
7892
|
+
set.add(m.id);
|
|
7893
|
+
pinnedNames.push(m.name);
|
|
7860
7894
|
}
|
|
7895
|
+
memoryPinnedItemIdsByContext.set(ctxId, set);
|
|
7861
7896
|
}
|
|
7862
|
-
if (
|
|
7863
|
-
const names = pinResults.flat().map((i) => i.name).join(", ");
|
|
7897
|
+
if (pinnedNames.length > 0) {
|
|
7864
7898
|
steps.push({
|
|
7865
|
-
text: `Memory prioritizes specific document(s); pinning ${
|
|
7899
|
+
text: `Memory prioritizes specific document(s); pinning ${pinnedNames.length} file(s) into the search: ${pinnedNames.join(", ")}`
|
|
7866
7900
|
});
|
|
7867
7901
|
}
|
|
7868
7902
|
}
|
|
@@ -7885,7 +7919,7 @@ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
|
|
|
7885
7919
|
return {
|
|
7886
7920
|
memoryChunksForAnswer,
|
|
7887
7921
|
memoryOverride,
|
|
7888
|
-
|
|
7922
|
+
memoryPinnedItemIdsByContext,
|
|
7889
7923
|
updatedQuestion,
|
|
7890
7924
|
updatedKeywords,
|
|
7891
7925
|
updatedImportantKeyword,
|
|
@@ -8016,7 +8050,7 @@ async function searchContexts(opts) {
|
|
|
8016
8050
|
preselectedItems,
|
|
8017
8051
|
scopedItemsByContext,
|
|
8018
8052
|
identifierPinsByContext,
|
|
8019
|
-
|
|
8053
|
+
memoryPinnedItemIdsByContext,
|
|
8020
8054
|
userPinnedItemIdsByContext,
|
|
8021
8055
|
rewrites,
|
|
8022
8056
|
styleHint,
|
|
@@ -8047,7 +8081,8 @@ async function searchContexts(opts) {
|
|
|
8047
8081
|
const identifierPins = identifierPinsByContext.get(ctxId) ?? /* @__PURE__ */ new Set();
|
|
8048
8082
|
let pins = new Set(identifierPins);
|
|
8049
8083
|
if (kind === "documents") {
|
|
8050
|
-
|
|
8084
|
+
const memPins = memoryPinnedItemIdsByContext.get(ctxId);
|
|
8085
|
+
if (memPins) for (const id of memPins) pins.add(id);
|
|
8051
8086
|
}
|
|
8052
8087
|
const userPins = userPinnedItemIdsByContext.get(ctxId);
|
|
8053
8088
|
if (userPins && userPins.size > 0) {
|
|
@@ -8580,7 +8615,7 @@ ${projectScope.customInstructions}` : ""
|
|
|
8580
8615
|
updatedQuestion,
|
|
8581
8616
|
updatedKeywords,
|
|
8582
8617
|
updatedImportantKeyword,
|
|
8583
|
-
|
|
8618
|
+
memoryPinnedItemIdsByContext,
|
|
8584
8619
|
memoryOverride
|
|
8585
8620
|
} = memResult;
|
|
8586
8621
|
const { pinsByContext: identifierPinsByContext, exactPinsByContext, steps: pinSteps } = await resolveIdentifierPins({
|
|
@@ -8613,7 +8648,7 @@ ${projectScope.customInstructions}` : ""
|
|
|
8613
8648
|
model: utilityModel,
|
|
8614
8649
|
preselectedItems,
|
|
8615
8650
|
identifierPinsByContext,
|
|
8616
|
-
|
|
8651
|
+
memoryPinnedItemIdsByContext,
|
|
8617
8652
|
userPinnedItemIdsByContext,
|
|
8618
8653
|
scopedItemsByContext: resolvedProject?.scopedItemsByContext,
|
|
8619
8654
|
rewrites: cfg.vocabulary.rewrites,
|
|
@@ -8633,7 +8668,7 @@ ${projectScope.customInstructions}` : ""
|
|
|
8633
8668
|
model: utilityModel,
|
|
8634
8669
|
preselectedItems,
|
|
8635
8670
|
identifierPinsByContext,
|
|
8636
|
-
|
|
8671
|
+
memoryPinnedItemIdsByContext,
|
|
8637
8672
|
userPinnedItemIdsByContext,
|
|
8638
8673
|
scopedItemsByContext: resolvedProject?.scopedItemsByContext,
|
|
8639
8674
|
rewrites: cfg.vocabulary.rewrites,
|
|
@@ -8643,7 +8678,9 @@ ${projectScope.customInstructions}` : ""
|
|
|
8643
8678
|
}) : Promise.resolve({ chunks: [] })
|
|
8644
8679
|
]);
|
|
8645
8680
|
const pinnedItemIds = /* @__PURE__ */ new Set([
|
|
8646
|
-
...
|
|
8681
|
+
...(function* () {
|
|
8682
|
+
for (const s of memoryPinnedItemIdsByContext.values()) yield* s;
|
|
8683
|
+
})(),
|
|
8647
8684
|
...(function* () {
|
|
8648
8685
|
for (const s of exactPinsByContext.values()) yield* s;
|
|
8649
8686
|
})(),
|
|
@@ -14452,8 +14489,6 @@ var addProviderFields = async (args, requestedFields, result, tools, user, conte
|
|
|
14452
14489
|
let litellmEntry;
|
|
14453
14490
|
if (isLiteLLMEnabled() && result?.model) {
|
|
14454
14491
|
litellmEntry = await findLiteLLMModel(result.model);
|
|
14455
|
-
} else {
|
|
14456
|
-
throw new Error("Could not load modal for: " + result?.model);
|
|
14457
14492
|
}
|
|
14458
14493
|
if (requestedFields.includes("providerName")) {
|
|
14459
14494
|
result.providerName = isLiteLLMEnabled() ? "LiteLLM" : "";
|
|
@@ -16421,6 +16456,40 @@ function createMutations(table, contexts, tools, config) {
|
|
|
16421
16456
|
}
|
|
16422
16457
|
return await ctx.entityLayer.detachItem(args.item);
|
|
16423
16458
|
};
|
|
16459
|
+
if (table.RBAC) {
|
|
16460
|
+
mutations[`${tableNamePlural}BulkUpdateRBAC`] = async (_, args, context) => {
|
|
16461
|
+
const { db: db2 } = context;
|
|
16462
|
+
const { ids, rights_mode, RBAC } = args;
|
|
16463
|
+
if (!Array.isArray(ids) || ids.length === 0) {
|
|
16464
|
+
throw new Error("ids is required and must be a non-empty array.");
|
|
16465
|
+
}
|
|
16466
|
+
if (!VALID_RIGHTS_MODES.includes(rights_mode)) {
|
|
16467
|
+
throw new Error(
|
|
16468
|
+
`Invalid rights_mode "${rights_mode}" \u2014 expected one of: ${VALID_RIGHTS_MODES.join(", ")}`
|
|
16469
|
+
);
|
|
16470
|
+
}
|
|
16471
|
+
for (const id of ids) {
|
|
16472
|
+
await validateWriteAccess(id, context);
|
|
16473
|
+
}
|
|
16474
|
+
await db2.transaction(async (trx) => {
|
|
16475
|
+
await trx(tableNamePlural).whereIn("id", ids).update({ rights_mode, updatedAt: /* @__PURE__ */ new Date() });
|
|
16476
|
+
for (const id of ids) {
|
|
16477
|
+
const existingRbacRecords = await trx.from("rbac").where({ entity: table.name.singular, target_resource_id: id }).select("*");
|
|
16478
|
+
await handleRBACUpdate(
|
|
16479
|
+
trx,
|
|
16480
|
+
table.name.singular,
|
|
16481
|
+
id,
|
|
16482
|
+
RBAC ?? {},
|
|
16483
|
+
existingRbacRecords
|
|
16484
|
+
);
|
|
16485
|
+
}
|
|
16486
|
+
});
|
|
16487
|
+
return {
|
|
16488
|
+
message: `Access updated for ${ids.length} item${ids.length === 1 ? "" : "s"}.`,
|
|
16489
|
+
itemCount: ids.length
|
|
16490
|
+
};
|
|
16491
|
+
};
|
|
16492
|
+
}
|
|
16424
16493
|
}
|
|
16425
16494
|
return mutations;
|
|
16426
16495
|
}
|
|
@@ -21392,6 +21461,17 @@ function createSDL(tables, contexts, tools, config, evals) {
|
|
|
21392
21461
|
${tableNameSingular}ExtractEntities(item: ID!): ${tableNameSingular}EntityExtractPayload
|
|
21393
21462
|
${tableNameSingular}DetachEntities(item: ID!): ${tableNameSingular}EntityDetachPayload
|
|
21394
21463
|
`;
|
|
21464
|
+
if (table.RBAC) {
|
|
21465
|
+
mutationDefs += `
|
|
21466
|
+
${tableNamePlural}BulkUpdateRBAC(ids: [ID!]!, rights_mode: String!, RBAC: RBACInput): ${tableNameSingular}BulkUpdateRBACPayload
|
|
21467
|
+
`;
|
|
21468
|
+
modelDefs += `
|
|
21469
|
+
type ${tableNameSingular}BulkUpdateRBACPayload {
|
|
21470
|
+
message: String!
|
|
21471
|
+
itemCount: Int!
|
|
21472
|
+
}
|
|
21473
|
+
`;
|
|
21474
|
+
}
|
|
21395
21475
|
if (table.processor) {
|
|
21396
21476
|
mutationDefs += `
|
|
21397
21477
|
${tableNameSingular}ProcessItem(item: ID!): ${tableNameSingular}ProcessItemFieldReturnPayload
|
package/dist/index.d.cts
CHANGED
|
@@ -986,6 +986,13 @@ type AuditConfig = {
|
|
|
986
986
|
};
|
|
987
987
|
};
|
|
988
988
|
|
|
989
|
+
type AuditClient = {
|
|
990
|
+
ip?: string;
|
|
991
|
+
userAgent?: string;
|
|
992
|
+
referer?: string;
|
|
993
|
+
origin?: string;
|
|
994
|
+
forwardedFor?: string;
|
|
995
|
+
};
|
|
989
996
|
type AuditEvent = {
|
|
990
997
|
v: 1;
|
|
991
998
|
ts: string;
|
|
@@ -1025,6 +1032,7 @@ type AuditEvent = {
|
|
|
1025
1032
|
data?: Record<string, unknown>;
|
|
1026
1033
|
durationMs?: number;
|
|
1027
1034
|
truncated?: Record<string, boolean>;
|
|
1035
|
+
client?: AuditClient;
|
|
1028
1036
|
};
|
|
1029
1037
|
type AuditToolCallInput = {
|
|
1030
1038
|
durationMs: number;
|
|
@@ -1054,6 +1062,7 @@ type AuditToolCallInput = {
|
|
|
1054
1062
|
output: unknown;
|
|
1055
1063
|
status: "ok" | "error" | "auth_required";
|
|
1056
1064
|
error?: unknown;
|
|
1065
|
+
client?: AuditClient;
|
|
1057
1066
|
};
|
|
1058
1067
|
|
|
1059
1068
|
interface AuditLogger {
|
package/dist/index.d.ts
CHANGED
|
@@ -986,6 +986,13 @@ type AuditConfig = {
|
|
|
986
986
|
};
|
|
987
987
|
};
|
|
988
988
|
|
|
989
|
+
type AuditClient = {
|
|
990
|
+
ip?: string;
|
|
991
|
+
userAgent?: string;
|
|
992
|
+
referer?: string;
|
|
993
|
+
origin?: string;
|
|
994
|
+
forwardedFor?: string;
|
|
995
|
+
};
|
|
989
996
|
type AuditEvent = {
|
|
990
997
|
v: 1;
|
|
991
998
|
ts: string;
|
|
@@ -1025,6 +1032,7 @@ type AuditEvent = {
|
|
|
1025
1032
|
data?: Record<string, unknown>;
|
|
1026
1033
|
durationMs?: number;
|
|
1027
1034
|
truncated?: Record<string, boolean>;
|
|
1035
|
+
client?: AuditClient;
|
|
1028
1036
|
};
|
|
1029
1037
|
type AuditToolCallInput = {
|
|
1030
1038
|
durationMs: number;
|
|
@@ -1054,6 +1062,7 @@ type AuditToolCallInput = {
|
|
|
1054
1062
|
output: unknown;
|
|
1055
1063
|
status: "ok" | "error" | "auth_required";
|
|
1056
1064
|
error?: unknown;
|
|
1065
|
+
client?: AuditClient;
|
|
1057
1066
|
};
|
|
1058
1067
|
|
|
1059
1068
|
interface AuditLogger {
|
package/dist/index.js
CHANGED
|
@@ -88,7 +88,7 @@ import {
|
|
|
88
88
|
verifyCredentialNonce,
|
|
89
89
|
waitForLiteLLMReady,
|
|
90
90
|
withRetry
|
|
91
|
-
} from "./chunk-
|
|
91
|
+
} from "./chunk-5FTX543Z.js";
|
|
92
92
|
import {
|
|
93
93
|
findLiteLLMModel
|
|
94
94
|
} from "./chunk-7CCMW3IW.js";
|
|
@@ -5576,8 +5576,6 @@ var addProviderFields = async (args, requestedFields, result, tools, user, conte
|
|
|
5576
5576
|
let litellmEntry;
|
|
5577
5577
|
if (isLiteLLMEnabled() && result?.model) {
|
|
5578
5578
|
litellmEntry = await findLiteLLMModel(result.model);
|
|
5579
|
-
} else {
|
|
5580
|
-
throw new Error("Could not load modal for: " + result?.model);
|
|
5581
5579
|
}
|
|
5582
5580
|
if (requestedFields.includes("providerName")) {
|
|
5583
5581
|
result.providerName = isLiteLLMEnabled() ? "LiteLLM" : "";
|
|
@@ -7523,6 +7521,40 @@ function createMutations(table, contexts, tools, config) {
|
|
|
7523
7521
|
}
|
|
7524
7522
|
return await ctx.entityLayer.detachItem(args.item);
|
|
7525
7523
|
};
|
|
7524
|
+
if (table.RBAC) {
|
|
7525
|
+
mutations[`${tableNamePlural}BulkUpdateRBAC`] = async (_, args, context) => {
|
|
7526
|
+
const { db } = context;
|
|
7527
|
+
const { ids, rights_mode, RBAC } = args;
|
|
7528
|
+
if (!Array.isArray(ids) || ids.length === 0) {
|
|
7529
|
+
throw new Error("ids is required and must be a non-empty array.");
|
|
7530
|
+
}
|
|
7531
|
+
if (!VALID_RIGHTS_MODES.includes(rights_mode)) {
|
|
7532
|
+
throw new Error(
|
|
7533
|
+
`Invalid rights_mode "${rights_mode}" \u2014 expected one of: ${VALID_RIGHTS_MODES.join(", ")}`
|
|
7534
|
+
);
|
|
7535
|
+
}
|
|
7536
|
+
for (const id of ids) {
|
|
7537
|
+
await validateWriteAccess(id, context);
|
|
7538
|
+
}
|
|
7539
|
+
await db.transaction(async (trx) => {
|
|
7540
|
+
await trx(tableNamePlural).whereIn("id", ids).update({ rights_mode, updatedAt: /* @__PURE__ */ new Date() });
|
|
7541
|
+
for (const id of ids) {
|
|
7542
|
+
const existingRbacRecords = await trx.from("rbac").where({ entity: table.name.singular, target_resource_id: id }).select("*");
|
|
7543
|
+
await handleRBACUpdate(
|
|
7544
|
+
trx,
|
|
7545
|
+
table.name.singular,
|
|
7546
|
+
id,
|
|
7547
|
+
RBAC ?? {},
|
|
7548
|
+
existingRbacRecords
|
|
7549
|
+
);
|
|
7550
|
+
}
|
|
7551
|
+
});
|
|
7552
|
+
return {
|
|
7553
|
+
message: `Access updated for ${ids.length} item${ids.length === 1 ? "" : "s"}.`,
|
|
7554
|
+
itemCount: ids.length
|
|
7555
|
+
};
|
|
7556
|
+
};
|
|
7557
|
+
}
|
|
7526
7558
|
}
|
|
7527
7559
|
return mutations;
|
|
7528
7560
|
}
|
|
@@ -12412,6 +12444,17 @@ function createSDL(tables, contexts, tools, config, evals) {
|
|
|
12412
12444
|
${tableNameSingular}ExtractEntities(item: ID!): ${tableNameSingular}EntityExtractPayload
|
|
12413
12445
|
${tableNameSingular}DetachEntities(item: ID!): ${tableNameSingular}EntityDetachPayload
|
|
12414
12446
|
`;
|
|
12447
|
+
if (table.RBAC) {
|
|
12448
|
+
mutationDefs += `
|
|
12449
|
+
${tableNamePlural}BulkUpdateRBAC(ids: [ID!]!, rights_mode: String!, RBAC: RBACInput): ${tableNameSingular}BulkUpdateRBACPayload
|
|
12450
|
+
`;
|
|
12451
|
+
modelDefs += `
|
|
12452
|
+
type ${tableNameSingular}BulkUpdateRBACPayload {
|
|
12453
|
+
message: String!
|
|
12454
|
+
itemCount: Int!
|
|
12455
|
+
}
|
|
12456
|
+
`;
|
|
12457
|
+
}
|
|
12415
12458
|
if (table.processor) {
|
|
12416
12459
|
mutationDefs += `
|
|
12417
12460
|
${tableNameSingular}ProcessItem(item: ID!): ${tableNameSingular}ProcessItemFieldReturnPayload
|
|
@@ -4,13 +4,13 @@ import { createAgenticRetrievalTool, parsePreselectedItems } from "./index";
|
|
|
4
4
|
jest.mock("@EE/entitlements", () => ({ checkLicense: () => ({ "agentic-retrieval": true }) }));
|
|
5
5
|
jest.mock("@SRC/exulu/resolve-reranker", () => ({ resolveReranker: jest.fn(async () => ({ model: "m", rerank: async (_q: any, c: any) => c })) }));
|
|
6
6
|
jest.mock("@SRC/exulu/resolve-model", () => ({ resolveModel: jest.fn() }));
|
|
7
|
-
jest.mock("@SRC/exulu/app/singleton", () => ({ exuluApp: { get: () => () } }));
|
|
7
|
+
jest.mock("@SRC/exulu/app/singleton", () => ({ exuluApp: { get: () => ({}) } }));
|
|
8
8
|
jest.mock("./routing", () => ({ runRoutingPhase: jest.fn(async () => ({
|
|
9
9
|
mainContexts: ["docs"], fallbackContexts: [], userPinnedItemIdsByContext: new Map(),
|
|
10
10
|
userRequestedPage: null, hasExplicitDocAndPage: false, steps: [{ text: "routed" }] })) }));
|
|
11
11
|
jest.mock("./memory", () => ({ runMemoryPhase: jest.fn(async () => ({
|
|
12
12
|
memoryChunksForAnswer: [], memoryOverride: { active: false, chunks: [], reason: "" },
|
|
13
|
-
|
|
13
|
+
memoryPinnedItemIdsByContext: new Map(), updatedQuestion: "q", updatedKeywords: ["k"],
|
|
14
14
|
updatedImportantKeyword: "k", steps: [] })) }));
|
|
15
15
|
jest.mock("./prefilter", () => ({ resolveIdentifierPins: jest.fn(async () => ({
|
|
16
16
|
pinsByContext: new Map(), exactPinsByContext: new Map(), steps: [] })) }));
|
|
@@ -75,7 +75,7 @@ describe("createAgenticRetrievalTool", () => {
|
|
|
75
75
|
runMemoryPhase.mockResolvedValueOnce({
|
|
76
76
|
memoryChunksForAnswer: [{ chunk_id: "m1" }],
|
|
77
77
|
memoryOverride: { active: false, chunks: [], reason: "" },
|
|
78
|
-
|
|
78
|
+
memoryPinnedItemIdsByContext: new Map(),
|
|
79
79
|
updatedQuestion: "q",
|
|
80
80
|
updatedKeywords: ["k"],
|
|
81
81
|
updatedImportantKeyword: "k",
|
|
@@ -114,7 +114,7 @@ describe("payload deduplication", () => {
|
|
|
114
114
|
runMemoryPhase.mockResolvedValueOnce({
|
|
115
115
|
memoryChunksForAnswer: [memChunk],
|
|
116
116
|
memoryOverride: { active: false, chunks: [], reason: "" },
|
|
117
|
-
|
|
117
|
+
memoryPinnedItemIdsByContext: new Map(), updatedQuestion: "q", updatedKeywords: ["k"],
|
|
118
118
|
updatedImportantKeyword: "k",
|
|
119
119
|
steps: [{ text: "memory step", chunks: [memChunk] }],
|
|
120
120
|
});
|
|
@@ -425,7 +425,7 @@ export function createAgenticRetrievalTool(opts: {
|
|
|
425
425
|
updatedQuestion,
|
|
426
426
|
updatedKeywords,
|
|
427
427
|
updatedImportantKeyword,
|
|
428
|
-
|
|
428
|
+
memoryPinnedItemIdsByContext,
|
|
429
429
|
memoryOverride,
|
|
430
430
|
} = memResult;
|
|
431
431
|
|
|
@@ -464,7 +464,7 @@ export function createAgenticRetrievalTool(opts: {
|
|
|
464
464
|
model: utilityModel,
|
|
465
465
|
preselectedItems,
|
|
466
466
|
identifierPinsByContext,
|
|
467
|
-
|
|
467
|
+
memoryPinnedItemIdsByContext,
|
|
468
468
|
userPinnedItemIdsByContext,
|
|
469
469
|
scopedItemsByContext: resolvedProject?.scopedItemsByContext,
|
|
470
470
|
rewrites: cfg.vocabulary.rewrites,
|
|
@@ -485,7 +485,7 @@ export function createAgenticRetrievalTool(opts: {
|
|
|
485
485
|
model: utilityModel,
|
|
486
486
|
preselectedItems,
|
|
487
487
|
identifierPinsByContext,
|
|
488
|
-
|
|
488
|
+
memoryPinnedItemIdsByContext,
|
|
489
489
|
userPinnedItemIdsByContext,
|
|
490
490
|
scopedItemsByContext: resolvedProject?.scopedItemsByContext,
|
|
491
491
|
rewrites: cfg.vocabulary.rewrites,
|
|
@@ -499,7 +499,9 @@ export function createAgenticRetrievalTool(opts: {
|
|
|
499
499
|
// ── Build rerank state ────────────────────────────────────────────────
|
|
500
500
|
// pinnedItemIds = memory ∪ exact identifier pins ∪ user pins ∪ project pins
|
|
501
501
|
const pinnedItemIds = new Set<string>([
|
|
502
|
-
...
|
|
502
|
+
...(function* () {
|
|
503
|
+
for (const s of memoryPinnedItemIdsByContext.values()) yield* s;
|
|
504
|
+
})(),
|
|
503
505
|
...(function* () {
|
|
504
506
|
for (const s of exactPinsByContext.values()) yield* s;
|
|
505
507
|
})(),
|
|
@@ -74,7 +74,7 @@ describe("runMemoryPhase", () => {
|
|
|
74
74
|
expect(r.updatedImportantKeyword).toBe("FST-2XT");
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
-
it("resolves file-prioritization pins
|
|
77
|
+
it("resolves file-prioritization pins keyed by their document context", async () => {
|
|
78
78
|
(generateText as jest.Mock)
|
|
79
79
|
.mockResolvedValueOnce({ output: { relevantChunkIds: ["1"] } })
|
|
80
80
|
.mockResolvedValueOnce({ output: { shouldPrioritizeFiles: true, fileNameHints: ["PROJECT_NOTES"] } });
|
|
@@ -82,7 +82,9 @@ describe("runMemoryPhase", () => {
|
|
|
82
82
|
const r = await runMemoryPhase({ ...baseOpts, memoryChunks: [memChunk("1", "always check PROJECT_NOTES")],
|
|
83
83
|
memoryContext: undefined, documentContexts: [{ id: "docs" }],
|
|
84
84
|
memoryConfig: { enabled: true, override: false, filePrioritization: true, queryAugmentation: false } });
|
|
85
|
-
|
|
85
|
+
// Pins are keyed by the context they were resolved in, so a consumer can apply them
|
|
86
|
+
// only to that context (no cross-context leak). See search.ts rule 2b.
|
|
87
|
+
expect([...(r.memoryPinnedItemIdsByContext.get("docs") ?? [])]).toEqual(["d1"]);
|
|
86
88
|
});
|
|
87
89
|
|
|
88
90
|
it("never throws even when post-Promise.all processing encounters runtime errors", async () => {
|
|
@@ -129,7 +129,7 @@ function neutralResult(
|
|
|
129
129
|
return {
|
|
130
130
|
memoryChunksForAnswer: [],
|
|
131
131
|
memoryOverride: { active: false, chunks: [], reason: "" },
|
|
132
|
-
|
|
132
|
+
memoryPinnedItemIdsByContext: new Map(),
|
|
133
133
|
updatedQuestion: question,
|
|
134
134
|
updatedKeywords: keywords,
|
|
135
135
|
updatedImportantKeyword: importantKeyword,
|
|
@@ -272,7 +272,7 @@ export async function runMemoryPhase({
|
|
|
272
272
|
chunks: [],
|
|
273
273
|
reason: "",
|
|
274
274
|
};
|
|
275
|
-
|
|
275
|
+
const memoryPinnedItemIdsByContext = new Map<string, Set<string>>();
|
|
276
276
|
let updatedQuestion = question;
|
|
277
277
|
let updatedKeywords = keywords;
|
|
278
278
|
let updatedImportantKeyword = importantKeyword;
|
|
@@ -466,12 +466,15 @@ export async function runMemoryPhase({
|
|
|
466
466
|
};
|
|
467
467
|
}
|
|
468
468
|
|
|
469
|
-
// File prioritization: resolve hints via fuzzyPrefilter against EVERY documentContexts entry
|
|
469
|
+
// File prioritization: resolve hints via fuzzyPrefilter against EVERY documentContexts entry.
|
|
470
|
+
// Pins are kept keyed by the context they were resolved in, so the search phase can apply a
|
|
471
|
+
// pin only to its home context — a tech_doc file must never filter a software-docs search to 0.
|
|
470
472
|
if (fileResult.output?.shouldPrioritizeFiles && fileResult.output?.fileNameHints?.length) {
|
|
471
473
|
const hints = fileResult.output.fileNameHints;
|
|
472
474
|
const pinResults = await Promise.all(
|
|
473
|
-
documentContexts.map((ctx) =>
|
|
474
|
-
|
|
475
|
+
documentContexts.map(async (ctx) => ({
|
|
476
|
+
ctxId: ctx.id as string,
|
|
477
|
+
matches: await fuzzyPrefilter({
|
|
475
478
|
cacheKey: `memory-pin:${ctx.id}`,
|
|
476
479
|
relevantKeywords: hints,
|
|
477
480
|
context: ctx,
|
|
@@ -479,17 +482,21 @@ export async function runMemoryPhase({
|
|
|
479
482
|
normalize: (item: any) =>
|
|
480
483
|
item.external_id ? normalizeFileName(item.external_id) : item.name,
|
|
481
484
|
}).catch(() => []),
|
|
482
|
-
),
|
|
485
|
+
})),
|
|
483
486
|
);
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
+
const pinnedNames: string[] = [];
|
|
488
|
+
for (const { ctxId, matches } of pinResults) {
|
|
489
|
+
if (!matches.length) continue;
|
|
490
|
+
const set = memoryPinnedItemIdsByContext.get(ctxId) ?? new Set<string>();
|
|
491
|
+
for (const m of matches) {
|
|
492
|
+
set.add(m.id);
|
|
493
|
+
pinnedNames.push(m.name);
|
|
487
494
|
}
|
|
495
|
+
memoryPinnedItemIdsByContext.set(ctxId, set);
|
|
488
496
|
}
|
|
489
|
-
if (
|
|
490
|
-
const names = pinResults.flat().map((i) => i.name).join(", ");
|
|
497
|
+
if (pinnedNames.length > 0) {
|
|
491
498
|
steps.push({
|
|
492
|
-
text: `Memory prioritizes specific document(s); pinning ${
|
|
499
|
+
text: `Memory prioritizes specific document(s); pinning ${pinnedNames.length} file(s) into the search: ${pinnedNames.join(", ")}`,
|
|
493
500
|
});
|
|
494
501
|
}
|
|
495
502
|
}
|
|
@@ -520,7 +527,7 @@ export async function runMemoryPhase({
|
|
|
520
527
|
return {
|
|
521
528
|
memoryChunksForAnswer,
|
|
522
529
|
memoryOverride,
|
|
523
|
-
|
|
530
|
+
memoryPinnedItemIdsByContext,
|
|
524
531
|
updatedQuestion,
|
|
525
532
|
updatedKeywords,
|
|
526
533
|
updatedImportantKeyword,
|
|
@@ -20,7 +20,7 @@ const base = {
|
|
|
20
20
|
} as any,
|
|
21
21
|
question: "how to fix door error E42", keywords: ["door", "E42"], importantKeyword: "E42",
|
|
22
22
|
user: {}, role: "r", model: {},
|
|
23
|
-
preselectedItems: new Map(), identifierPinsByContext: new Map(),
|
|
23
|
+
preselectedItems: new Map(), identifierPinsByContext: new Map(), memoryPinnedItemIdsByContext: new Map<string, Set<string>>(),
|
|
24
24
|
userPinnedItemIdsByContext: new Map(), rewrites: [{ find: "fix", replace: "repair" }],
|
|
25
25
|
styleHint: "", maxQueries: 5, skipPrefilter: false,
|
|
26
26
|
};
|
|
@@ -55,7 +55,7 @@ describe("searchContexts", () => {
|
|
|
55
55
|
await searchContexts({
|
|
56
56
|
...base, contextIds: ["docs"],
|
|
57
57
|
identifierPinsByContext: new Map([["docs", new Set(["i1"])]]),
|
|
58
|
-
|
|
58
|
+
memoryPinnedItemIdsByContext: new Map([["docs", new Set(["m1"])]]),
|
|
59
59
|
});
|
|
60
60
|
expect(new Set((multiQuerySearch as jest.Mock).mock.calls[0][0].pinnedItemIds)).toEqual(new Set(["i1", "m1"]));
|
|
61
61
|
|
|
@@ -63,12 +63,27 @@ describe("searchContexts", () => {
|
|
|
63
63
|
await searchContexts({
|
|
64
64
|
...base, contextIds: ["docs"],
|
|
65
65
|
identifierPinsByContext: new Map([["docs", new Set(["i1"])]]),
|
|
66
|
-
|
|
66
|
+
memoryPinnedItemIdsByContext: new Map([["docs", new Set(["m1"])]]),
|
|
67
67
|
userPinnedItemIdsByContext: new Map([["docs", new Set(["u1"])]]),
|
|
68
68
|
});
|
|
69
69
|
expect((multiQuerySearch as jest.Mock).mock.calls[0][0].pinnedItemIds).toEqual(["u1"]);
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
+
it("memory pins apply ONLY to their own context (no cross-context leak)", async () => {
|
|
73
|
+
// Regression: a memory pin resolved in another context (e.g. tech_doc's FST-Miscel-Secrets)
|
|
74
|
+
// must not become a hard id-whitelist on THIS context, which would filter it down to 0 chunks.
|
|
75
|
+
await searchContexts({
|
|
76
|
+
...base, contextIds: ["docs"],
|
|
77
|
+
memoryPinnedItemIdsByContext: new Map([
|
|
78
|
+
["docs", new Set(["m_docs"])],
|
|
79
|
+
["other", new Set(["m_other"])],
|
|
80
|
+
]),
|
|
81
|
+
});
|
|
82
|
+
const pins = (multiQuerySearch as jest.Mock).mock.calls[0][0].pinnedItemIds;
|
|
83
|
+
expect(pins).toEqual(["m_docs"]); // own-context pin applied
|
|
84
|
+
expect(pins).not.toContain("m_other"); // foreign-context pin did NOT leak in
|
|
85
|
+
});
|
|
86
|
+
|
|
72
87
|
it("preselected items win over everything and skip prefilters", async () => {
|
|
73
88
|
await searchContexts({
|
|
74
89
|
...base, contextIds: ["docs"],
|
|
@@ -95,7 +110,7 @@ describe("searchContexts", () => {
|
|
|
95
110
|
await searchContexts({
|
|
96
111
|
...base, contextIds: ["docs"], skipPrefilter: true,
|
|
97
112
|
identifierPinsByContext: new Map([["docs", new Set(["i1"])]]),
|
|
98
|
-
|
|
113
|
+
memoryPinnedItemIdsByContext: new Map([["docs", new Set(["m1"])]]),
|
|
99
114
|
});
|
|
100
115
|
expect((multiQuerySearch as jest.Mock).mock.calls[0][0].pinnedItemIds).toEqual([]);
|
|
101
116
|
});
|
|
@@ -25,7 +25,7 @@ export async function searchContexts(opts: {
|
|
|
25
25
|
preselectedItems: Map<string, string[] | null>;
|
|
26
26
|
scopedItemsByContext?: Map<string, string[] | null>; // Project-added sources: hard item filter per context (null = whole context).
|
|
27
27
|
identifierPinsByContext: Map<string, Set<string>>; // from resolveIdentifierPins
|
|
28
|
-
|
|
28
|
+
memoryPinnedItemIdsByContext: Map<string, Set<string>>; // from memory phase (documents kind only), keyed by home context
|
|
29
29
|
userPinnedItemIdsByContext: Map<string, Set<string>>; // from routing phase
|
|
30
30
|
rewrites: { find: string; replace: string }[];
|
|
31
31
|
styleHint: string;
|
|
@@ -45,7 +45,7 @@ export async function searchContexts(opts: {
|
|
|
45
45
|
preselectedItems,
|
|
46
46
|
scopedItemsByContext,
|
|
47
47
|
identifierPinsByContext,
|
|
48
|
-
|
|
48
|
+
memoryPinnedItemIdsByContext,
|
|
49
49
|
userPinnedItemIdsByContext,
|
|
50
50
|
rewrites,
|
|
51
51
|
styleHint,
|
|
@@ -94,9 +94,13 @@ export async function searchContexts(opts: {
|
|
|
94
94
|
const identifierPins = identifierPinsByContext.get(ctxId) ?? new Set<string>();
|
|
95
95
|
let pins = new Set<string>(identifierPins);
|
|
96
96
|
|
|
97
|
-
// 2b: documents kind only — UNION with
|
|
97
|
+
// 2b: documents kind only — UNION with memory pins that belong to THIS context.
|
|
98
|
+
// Memory pins are keyed by their home context so a pin resolved elsewhere (e.g. a
|
|
99
|
+
// tech_doc file) never becomes a hard id-whitelist on a context that lacks it, which
|
|
100
|
+
// would prefilter that context down to 0 chunks.
|
|
98
101
|
if (kind === "documents") {
|
|
99
|
-
|
|
102
|
+
const memPins = memoryPinnedItemIdsByContext.get(ctxId);
|
|
103
|
+
if (memPins) for (const id of memPins) pins.add(id);
|
|
100
104
|
}
|
|
101
105
|
|
|
102
106
|
// 2c: user pins REPLACE everything (authoritative)
|
|
@@ -21,7 +21,7 @@ export type RoutingPhaseResult = {
|
|
|
21
21
|
export type MemoryPhaseResult = {
|
|
22
22
|
memoryChunksForAnswer: ChunkWithScore[];
|
|
23
23
|
memoryOverride: { active: boolean; chunks: ChunkWithScore[]; reason: string };
|
|
24
|
-
|
|
24
|
+
memoryPinnedItemIdsByContext: Map<string, Set<string>>;
|
|
25
25
|
updatedQuestion: string;
|
|
26
26
|
updatedKeywords: string[];
|
|
27
27
|
updatedImportantKeyword: string;
|