@remnic/core 9.46.0 → 9.46.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-admin-ops-surface.d.ts +2 -2
- package/dist/access-admin-ops-surface.js +3 -2
- package/dist/access-authorization-probe.d.ts +2 -2
- package/dist/access-authorization-probe.js +4 -3
- package/dist/access-boundary.d.ts +2 -2
- package/dist/access-boundary.js +3 -2
- package/dist/access-cli.js +16 -16
- package/dist/access-extraction-force-flush.d.ts +2 -2
- package/dist/access-extraction-force-flush.js +3 -2
- package/dist/access-http-lcm-compaction.d.ts +2 -2
- package/dist/access-http-lcm-compaction.js +1 -1
- package/dist/access-http-lifecycle-flush.d.ts +2 -2
- package/dist/access-http-lifecycle-flush.js +2 -2
- package/dist/access-http-offline-stream.d.ts +2 -2
- package/dist/access-http-query.js +3 -2
- package/dist/access-http.d.ts +8 -2
- package/dist/access-http.js +11 -10
- package/dist/access-identity-continuity-surface.d.ts +1 -1
- package/dist/access-identity-continuity-surface.js +3 -2
- package/dist/access-lcm-surface.d.ts +2 -2
- package/dist/access-lcm-surface.js +3 -2
- package/dist/access-mcp.d.ts +2 -2
- package/dist/access-mcp.js +7 -6
- package/dist/access-memory-search-fanout.d.ts +163 -2
- package/dist/access-memory-search-fanout.js +20 -3
- package/dist/access-namespace-preflight.js +3 -2
- package/dist/access-observe-write-surface.d.ts +2 -2
- package/dist/access-observe-write-surface.js +3 -2
- package/dist/access-offline-manifest.d.ts +1 -1
- package/dist/access-operations-batch.js +4 -3
- package/dist/access-operations.d.ts +3 -2
- package/dist/access-operations.js +6 -5
- package/dist/access-recall-concurrency.d.ts +2 -2
- package/dist/access-recall-concurrency.js +3 -2
- package/dist/access-recall-response.d.ts +2 -2
- package/dist/access-recall-response.js +3 -2
- package/dist/access-recall-surface.d.ts +2 -2
- package/dist/access-recall-surface.js +3 -2
- package/dist/{access-service-loJadYd7.d.ts → access-service-DNBYHK33.d.ts} +2 -1
- package/dist/access-service-helpers.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/access-surface-catalog.js +1 -0
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/active-recall.js +2 -2
- package/dist/bootstrap.d.ts +1 -1
- package/dist/{chunk-WHWDBTCJ.js → chunk-74HN5QUH.js} +2 -2
- package/dist/{chunk-2DSUT7DZ.js → chunk-FQU3HTQM.js} +40 -11
- package/dist/chunk-FQU3HTQM.js.map +1 -0
- package/dist/{chunk-5LRDM4R7.js → chunk-G2L5IZV4.js} +7 -7
- package/dist/{chunk-TQZ4DVBC.js → chunk-GZ3R6XOE.js} +5 -2
- package/dist/chunk-GZ3R6XOE.js.map +1 -0
- package/dist/{chunk-5546W5SI.js → chunk-HLKPRWF4.js} +2 -2
- package/dist/chunk-IXEBUYO4.js +201 -0
- package/dist/chunk-IXEBUYO4.js.map +1 -0
- package/dist/{chunk-RCU25Z66.js → chunk-IXH2U2QT.js} +169 -58
- package/dist/chunk-IXH2U2QT.js.map +1 -0
- package/dist/{chunk-HJ3SVHUW.js → chunk-IZQ5TDFT.js} +9 -4
- package/dist/chunk-IZQ5TDFT.js.map +1 -0
- package/dist/{chunk-RVPTS4U5.js → chunk-JCNJIXZN.js} +2 -2
- package/dist/{chunk-5R6S4DJZ.js → chunk-N5XULCU3.js} +2 -2
- package/dist/{chunk-6H744ZBH.js → chunk-QISYTKG4.js} +2 -2
- package/dist/{chunk-AUMWZSE7.js → chunk-RN53QBM4.js} +4 -4
- package/dist/{chunk-M32URQXV.js → chunk-SIP5DBDN.js} +275 -334
- package/dist/chunk-SIP5DBDN.js.map +1 -0
- package/dist/{chunk-44N6ESJ4.js → chunk-TIDEL77O.js} +2 -2
- package/dist/{chunk-XBMJ35IP.js → chunk-VFZS2HAA.js} +4 -3
- package/dist/chunk-VFZS2HAA.js.map +1 -0
- package/dist/{cli-D_RcqXfZ.d.ts → cli-BWXSPxB9.d.ts} +2 -2
- package/dist/cli.d.ts +3 -3
- package/dist/cli.js +15 -15
- package/dist/config.js +2 -2
- package/dist/explicit-capture.d.ts +1 -1
- package/dist/external-wiki-access.d.ts +2 -2
- package/dist/external-wiki-access.js +4 -3
- package/dist/external-wiki-mcp-tools.d.ts +2 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +16 -16
- package/dist/mcp-memory-inspector-app.d.ts +2 -2
- package/dist/operator-toolkit.js +3 -3
- package/dist/{orchestrator-LPGegii3.d.ts → orchestrator-CRR-6sVB.d.ts} +6 -0
- package/dist/orchestrator.d.ts +1 -1
- package/dist/orchestrator.js +17 -17
- package/dist/resume-bundles.js +3 -3
- package/package.json +2 -2
- package/src/access-http-lcm-compaction.ts +3 -0
- package/src/access-http.test.ts +645 -0
- package/src/access-http.ts +55 -4
- package/src/access-mcp.ts +1 -0
- package/src/access-memory-search-fanout.ts +305 -2
- package/src/access-operations.ts +6 -0
- package/src/access-service-namespace.test.ts +146 -0
- package/src/access-service.ts +39 -40
- package/src/access-surface-catalog.ts +1 -0
- package/src/orchestration/generic-recall-paths.ts +116 -5
- package/dist/chunk-2DSUT7DZ.js.map +0 -1
- package/dist/chunk-7HXCF4RE.js +0 -72
- package/dist/chunk-7HXCF4RE.js.map +0 -1
- package/dist/chunk-HJ3SVHUW.js.map +0 -1
- package/dist/chunk-M32URQXV.js.map +0 -1
- package/dist/chunk-RCU25Z66.js.map +0 -1
- package/dist/chunk-TQZ4DVBC.js.map +0 -1
- package/dist/chunk-XBMJ35IP.js.map +0 -1
- /package/dist/{chunk-WHWDBTCJ.js.map → chunk-74HN5QUH.js.map} +0 -0
- /package/dist/{chunk-5LRDM4R7.js.map → chunk-G2L5IZV4.js.map} +0 -0
- /package/dist/{chunk-5546W5SI.js.map → chunk-HLKPRWF4.js.map} +0 -0
- /package/dist/{chunk-RVPTS4U5.js.map → chunk-JCNJIXZN.js.map} +0 -0
- /package/dist/{chunk-5R6S4DJZ.js.map → chunk-N5XULCU3.js.map} +0 -0
- /package/dist/{chunk-6H744ZBH.js.map → chunk-QISYTKG4.js.map} +0 -0
- /package/dist/{chunk-AUMWZSE7.js.map → chunk-RN53QBM4.js.map} +0 -0
- /package/dist/{chunk-44N6ESJ4.js.map → chunk-TIDEL77O.js.map} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remnic/core",
|
|
3
|
-
"version": "9.46.
|
|
3
|
+
"version": "9.46.2",
|
|
4
4
|
"description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -3096,7 +3096,7 @@
|
|
|
3096
3096
|
"core"
|
|
3097
3097
|
],
|
|
3098
3098
|
"peerDependencies": {
|
|
3099
|
-
"@remnic/coding-graph": "^9.46.
|
|
3099
|
+
"@remnic/coding-graph": "^9.46.2"
|
|
3100
3100
|
},
|
|
3101
3101
|
"peerDependenciesMeta": {
|
|
3102
3102
|
"@remnic/coding-graph": {
|
|
@@ -28,6 +28,9 @@ export function respondAccessCapabilitiesHttp(response: ServerResponse): void {
|
|
|
28
28
|
response.end(JSON.stringify({
|
|
29
29
|
lcmCompactionFlushBatch: true,
|
|
30
30
|
offlineSyncConvergenceComplete: true,
|
|
31
|
+
// POST /engram|remnic/v1/memories/search — HTTP clients probe this before
|
|
32
|
+
// using the route so they can degrade against an older daemon.
|
|
33
|
+
memoriesSearch: true,
|
|
31
34
|
}, null, 2));
|
|
32
35
|
}
|
|
33
36
|
|
package/src/access-http.test.ts
CHANGED
|
@@ -3830,6 +3830,7 @@ test("HTTP capabilities explicitly advertise receiver convergence finalization",
|
|
|
3830
3830
|
assert.deepEqual(await response.json(), {
|
|
3831
3831
|
lcmCompactionFlushBatch: true,
|
|
3832
3832
|
offlineSyncConvergenceComplete: true,
|
|
3833
|
+
memoriesSearch: true,
|
|
3833
3834
|
});
|
|
3834
3835
|
} finally {
|
|
3835
3836
|
await server.stop();
|
|
@@ -4091,3 +4092,647 @@ test("HTTP external wiki search aliases return the same cited result", async ()
|
|
|
4091
4092
|
await rm(rootDir, { recursive: true, force: true });
|
|
4092
4093
|
}
|
|
4093
4094
|
});
|
|
4095
|
+
|
|
4096
|
+
test("HTTP memory search aliases dispatch the boundary operation identically", async () => {
|
|
4097
|
+
const calls: Array<Record<string, unknown>> = [];
|
|
4098
|
+
const service = {
|
|
4099
|
+
memorySearch: async (request: Record<string, unknown>) => {
|
|
4100
|
+
calls.push(request);
|
|
4101
|
+
return {
|
|
4102
|
+
query: request.query,
|
|
4103
|
+
results: [{ path: "facts/alice.md", score: 0.71, snippet: "alice prefers dark mode" }],
|
|
4104
|
+
count: 1,
|
|
4105
|
+
};
|
|
4106
|
+
},
|
|
4107
|
+
} as unknown as EngramAccessService;
|
|
4108
|
+
const server = new EngramAccessHttpServer({
|
|
4109
|
+
service,
|
|
4110
|
+
port: 0,
|
|
4111
|
+
authToken: "test-token",
|
|
4112
|
+
principal: "operator",
|
|
4113
|
+
adminConsoleEnabled: false,
|
|
4114
|
+
});
|
|
4115
|
+
const status = await server.start();
|
|
4116
|
+
try {
|
|
4117
|
+
let canonicalBody: unknown;
|
|
4118
|
+
for (const prefix of ["engram", "remnic"]) {
|
|
4119
|
+
const response = await fetch(`http://127.0.0.1:${status.port}/${prefix}/v1/memories/search`, {
|
|
4120
|
+
method: "POST",
|
|
4121
|
+
headers: {
|
|
4122
|
+
authorization: "Bearer test-token",
|
|
4123
|
+
"content-type": "application/json",
|
|
4124
|
+
},
|
|
4125
|
+
body: JSON.stringify({ query: "dark mode", maxResults: 3 }),
|
|
4126
|
+
});
|
|
4127
|
+
assert.equal(response.status, 200);
|
|
4128
|
+
const body: unknown = await response.json();
|
|
4129
|
+
if (canonicalBody === undefined) canonicalBody = body;
|
|
4130
|
+
else assert.deepEqual(body, canonicalBody, "both prefixes must return one shape");
|
|
4131
|
+
}
|
|
4132
|
+
assert.deepEqual(canonicalBody, {
|
|
4133
|
+
query: "dark mode",
|
|
4134
|
+
results: [{ path: "facts/alice.md", score: 0.71, snippet: "alice prefers dark mode" }],
|
|
4135
|
+
count: 1,
|
|
4136
|
+
});
|
|
4137
|
+
assert.equal(calls.length, 2);
|
|
4138
|
+
for (const call of calls) {
|
|
4139
|
+
assert.equal(call.query, "dark mode");
|
|
4140
|
+
assert.equal(call.maxResults, 3);
|
|
4141
|
+
assert.equal(
|
|
4142
|
+
call.principal,
|
|
4143
|
+
"operator",
|
|
4144
|
+
"the authenticated principal — not a client field — scopes the search",
|
|
4145
|
+
);
|
|
4146
|
+
}
|
|
4147
|
+
} finally {
|
|
4148
|
+
await server.stop();
|
|
4149
|
+
}
|
|
4150
|
+
});
|
|
4151
|
+
|
|
4152
|
+
test("HTTP memory search rejects an invalid body before service dispatch", async () => {
|
|
4153
|
+
const calls: unknown[] = [];
|
|
4154
|
+
const service = {
|
|
4155
|
+
memorySearch: async (request: unknown) => {
|
|
4156
|
+
calls.push(request);
|
|
4157
|
+
return { query: "", results: [], count: 0 };
|
|
4158
|
+
},
|
|
4159
|
+
} as unknown as EngramAccessService;
|
|
4160
|
+
const server = new EngramAccessHttpServer({
|
|
4161
|
+
service,
|
|
4162
|
+
port: 0,
|
|
4163
|
+
authToken: "test-token",
|
|
4164
|
+
adminConsoleEnabled: false,
|
|
4165
|
+
});
|
|
4166
|
+
const status = await server.start();
|
|
4167
|
+
const post = (body: unknown) =>
|
|
4168
|
+
fetch(`http://127.0.0.1:${status.port}/engram/v1/memories/search`, {
|
|
4169
|
+
method: "POST",
|
|
4170
|
+
headers: {
|
|
4171
|
+
authorization: "Bearer test-token",
|
|
4172
|
+
"content-type": "application/json",
|
|
4173
|
+
},
|
|
4174
|
+
body: JSON.stringify(body),
|
|
4175
|
+
});
|
|
4176
|
+
try {
|
|
4177
|
+
assert.equal((await post({ query: " " })).status, 400, "blank query is rejected");
|
|
4178
|
+
assert.equal((await post({})).status, 400, "a missing query is rejected");
|
|
4179
|
+
assert.equal((await post({ query: "ok", maxResults: 0 })).status, 400, "maxResults must be >= 1");
|
|
4180
|
+
assert.equal(
|
|
4181
|
+
(await post({ query: "ok", namespace: 123 })).status,
|
|
4182
|
+
400,
|
|
4183
|
+
"a non-string namespace is rejected, not silently defaulted to the principal's scope",
|
|
4184
|
+
);
|
|
4185
|
+
assert.equal((await post({ query: "ok", namespace: ["a"] })).status, 400);
|
|
4186
|
+
assert.equal((await post({ query: "ok", namespace: {} })).status, 400);
|
|
4187
|
+
assert.equal(calls.length, 0, "no invalid request may reach the service");
|
|
4188
|
+
assert.equal(
|
|
4189
|
+
(await post({ query: "ok", namespace: null })).status,
|
|
4190
|
+
200,
|
|
4191
|
+
"an explicit null keeps its documented no-namespace meaning",
|
|
4192
|
+
);
|
|
4193
|
+
assert.equal(calls.length, 1);
|
|
4194
|
+
} finally {
|
|
4195
|
+
await server.stop();
|
|
4196
|
+
}
|
|
4197
|
+
});
|
|
4198
|
+
|
|
4199
|
+
test("HTTP memory search enforces token op and namespace allow-lists", async () => {
|
|
4200
|
+
const calls: unknown[] = [];
|
|
4201
|
+
const service = {
|
|
4202
|
+
memorySearch: async (request: unknown) => {
|
|
4203
|
+
calls.push(request);
|
|
4204
|
+
return { query: "q", results: [], count: 0 };
|
|
4205
|
+
},
|
|
4206
|
+
} as unknown as EngramAccessService;
|
|
4207
|
+
const server = new EngramAccessHttpServer({
|
|
4208
|
+
service,
|
|
4209
|
+
port: 0,
|
|
4210
|
+
principal: "reader",
|
|
4211
|
+
authTokenEntriesGetter: () => [
|
|
4212
|
+
{ token: "wrong-op", capabilities: { version: 1, ops: ["recall"] } },
|
|
4213
|
+
{
|
|
4214
|
+
token: "wrong-namespace",
|
|
4215
|
+
capabilities: { version: 1, ops: ["memory_search"], namespaces: ["other"] },
|
|
4216
|
+
},
|
|
4217
|
+
{
|
|
4218
|
+
token: "reader",
|
|
4219
|
+
capabilities: { version: 1, ops: ["memory_search"], namespaces: ["team"] },
|
|
4220
|
+
},
|
|
4221
|
+
],
|
|
4222
|
+
adminConsoleEnabled: false,
|
|
4223
|
+
});
|
|
4224
|
+
const status = await server.start();
|
|
4225
|
+
const post = (token: string, body: unknown) =>
|
|
4226
|
+
fetch(`http://127.0.0.1:${status.port}/engram/v1/memories/search`, {
|
|
4227
|
+
method: "POST",
|
|
4228
|
+
headers: {
|
|
4229
|
+
authorization: `Bearer ${token}`,
|
|
4230
|
+
"content-type": "application/json",
|
|
4231
|
+
},
|
|
4232
|
+
body: JSON.stringify(body),
|
|
4233
|
+
});
|
|
4234
|
+
try {
|
|
4235
|
+
assert.equal((await post("wrong-op", { query: "q", namespace: "team" })).status, 403);
|
|
4236
|
+
assert.equal(
|
|
4237
|
+
(await post("wrong-namespace", { query: "q", namespace: "team" })).status,
|
|
4238
|
+
403,
|
|
4239
|
+
"a body namespace outside the token allow-list must fail closed",
|
|
4240
|
+
);
|
|
4241
|
+
assert.equal(calls.length, 0, "no denied request may reach the service");
|
|
4242
|
+
assert.equal((await post("reader", { query: "q", namespace: "team" })).status, 200);
|
|
4243
|
+
assert.equal(calls.length, 1);
|
|
4244
|
+
assert.equal(
|
|
4245
|
+
(calls[0] as { namespace?: unknown }).namespace,
|
|
4246
|
+
"team",
|
|
4247
|
+
"the gated namespace reaches the service",
|
|
4248
|
+
);
|
|
4249
|
+
} finally {
|
|
4250
|
+
await server.stop();
|
|
4251
|
+
}
|
|
4252
|
+
});
|
|
4253
|
+
|
|
4254
|
+
test("HTTP memory search binds a scoped token to one namespace instead of fanning out", async () => {
|
|
4255
|
+
const calls: Array<Record<string, unknown>> = [];
|
|
4256
|
+
const service = {
|
|
4257
|
+
configRef: { defaultNamespace: "generalist" },
|
|
4258
|
+
memorySearch: async (request: Record<string, unknown>) => {
|
|
4259
|
+
calls.push(request);
|
|
4260
|
+
return { query: "q", results: [], count: 0 };
|
|
4261
|
+
},
|
|
4262
|
+
} as unknown as EngramAccessService;
|
|
4263
|
+
const server = new EngramAccessHttpServer({
|
|
4264
|
+
service,
|
|
4265
|
+
port: 0,
|
|
4266
|
+
principal: "reader",
|
|
4267
|
+
authTokenEntriesGetter: () => [
|
|
4268
|
+
{
|
|
4269
|
+
token: "scoped",
|
|
4270
|
+
capabilities: { version: 1, ops: ["memory_search"], namespaces: ["generalist"] },
|
|
4271
|
+
},
|
|
4272
|
+
{ token: "unrestricted", capabilities: { version: 1, ops: ["memory_search"] } },
|
|
4273
|
+
],
|
|
4274
|
+
adminConsoleEnabled: false,
|
|
4275
|
+
});
|
|
4276
|
+
const status = await server.start();
|
|
4277
|
+
const post = (token: string) =>
|
|
4278
|
+
fetch(`http://127.0.0.1:${status.port}/engram/v1/memories/search`, {
|
|
4279
|
+
method: "POST",
|
|
4280
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
4281
|
+
body: JSON.stringify({ query: "q" }),
|
|
4282
|
+
});
|
|
4283
|
+
try {
|
|
4284
|
+
assert.equal((await post("scoped")).status, 200);
|
|
4285
|
+
assert.equal(
|
|
4286
|
+
calls[0]?.namespace,
|
|
4287
|
+
"generalist",
|
|
4288
|
+
"an omitted namespace on a scoped token binds to the allowed effective namespace",
|
|
4289
|
+
);
|
|
4290
|
+
assert.equal((await post("unrestricted")).status, 200);
|
|
4291
|
+
assert.equal(
|
|
4292
|
+
calls[1]?.namespace,
|
|
4293
|
+
undefined,
|
|
4294
|
+
"an unrestricted token keeps the principal-wide fan-out",
|
|
4295
|
+
);
|
|
4296
|
+
} finally {
|
|
4297
|
+
await server.stop();
|
|
4298
|
+
}
|
|
4299
|
+
});
|
|
4300
|
+
|
|
4301
|
+
test("HTTP memory search forwards a validated ranking mode and trims the namespace", async () => {
|
|
4302
|
+
const calls: Array<Record<string, unknown>> = [];
|
|
4303
|
+
const service = {
|
|
4304
|
+
configRef: { defaultNamespace: "generalist" },
|
|
4305
|
+
memorySearch: async (request: Record<string, unknown>) => {
|
|
4306
|
+
calls.push(request);
|
|
4307
|
+
return { query: "q", results: [], count: 0 };
|
|
4308
|
+
},
|
|
4309
|
+
} as unknown as EngramAccessService;
|
|
4310
|
+
const server = new EngramAccessHttpServer({
|
|
4311
|
+
service,
|
|
4312
|
+
port: 0,
|
|
4313
|
+
principal: "reader",
|
|
4314
|
+
authTokenEntriesGetter: () => [
|
|
4315
|
+
{ token: "scoped", capabilities: { version: 1, ops: ["memory_search"], namespaces: ["team"] } },
|
|
4316
|
+
],
|
|
4317
|
+
adminConsoleEnabled: false,
|
|
4318
|
+
});
|
|
4319
|
+
const status = await server.start();
|
|
4320
|
+
const post = (body: unknown) =>
|
|
4321
|
+
fetch(`http://127.0.0.1:${status.port}/engram/v1/memories/search`, {
|
|
4322
|
+
method: "POST",
|
|
4323
|
+
headers: { authorization: "Bearer scoped", "content-type": "application/json" },
|
|
4324
|
+
body: JSON.stringify(body),
|
|
4325
|
+
});
|
|
4326
|
+
try {
|
|
4327
|
+
assert.equal(
|
|
4328
|
+
(await post({ query: "q", namespace: " team ", mode: "vector" })).status,
|
|
4329
|
+
200,
|
|
4330
|
+
"surrounding whitespace must not 403 a namespace the token allows",
|
|
4331
|
+
);
|
|
4332
|
+
assert.equal(calls[0]?.namespace, "team");
|
|
4333
|
+
assert.equal(calls[0]?.mode, "vector", "the ranking mode reaches the service");
|
|
4334
|
+
assert.equal(
|
|
4335
|
+
(await post({ query: "q", namespace: "team", mode: "vsearch" })).status,
|
|
4336
|
+
400,
|
|
4337
|
+
"an unknown ranking mode is rejected, not silently ignored",
|
|
4338
|
+
);
|
|
4339
|
+
assert.equal(calls.length, 1);
|
|
4340
|
+
} finally {
|
|
4341
|
+
await server.stop();
|
|
4342
|
+
}
|
|
4343
|
+
});
|
|
4344
|
+
|
|
4345
|
+
test("HTTP memory search excludes artifacts and tops up to a full page", async () => {
|
|
4346
|
+
// Artifacts rank above real memories. Filtering after the cap would return a
|
|
4347
|
+
// thin page; the search must keep asking until the budget is met.
|
|
4348
|
+
const seen: number[] = [];
|
|
4349
|
+
const corpus = [
|
|
4350
|
+
{ path: "artifacts/a1.md", score: 0.99, snippet: "artifact" },
|
|
4351
|
+
{ path: "artifacts/a2.md", score: 0.98, snippet: "artifact" },
|
|
4352
|
+
{ path: "artifacts/a3.md", score: 0.97, snippet: "artifact" },
|
|
4353
|
+
{ path: "facts/one.md", score: 0.5, snippet: "one" },
|
|
4354
|
+
{ path: "facts/two.md", score: 0.4, snippet: "two" },
|
|
4355
|
+
];
|
|
4356
|
+
const { runScopedMemorySearch } = await import("./access-memory-search-fanout.js");
|
|
4357
|
+
const results = await runScopedMemorySearch({
|
|
4358
|
+
query: "q",
|
|
4359
|
+
budget: 2,
|
|
4360
|
+
sendInitialLimit: true,
|
|
4361
|
+
authorizeScope: () => {},
|
|
4362
|
+
namespacesEnabled: false,
|
|
4363
|
+
isExcluded: (memoryPath) => memoryPath.startsWith("artifacts/"),
|
|
4364
|
+
flatCorpus: async (limit) => {
|
|
4365
|
+
seen.push(limit ?? -1);
|
|
4366
|
+
return corpus.slice(0, limit);
|
|
4367
|
+
},
|
|
4368
|
+
namespaced: async () => [],
|
|
4369
|
+
});
|
|
4370
|
+
assert.deepEqual(
|
|
4371
|
+
results.map((hit) => hit.path),
|
|
4372
|
+
["facts/one.md", "facts/two.md"],
|
|
4373
|
+
"a full page of real memories",
|
|
4374
|
+
);
|
|
4375
|
+
assert.deepEqual(seen, [2, 4, 8], "doubles the candidate request until the budget is met");
|
|
4376
|
+
});
|
|
4377
|
+
|
|
4378
|
+
test("HTTP memory search stops topping up when the corpus is exhausted", async () => {
|
|
4379
|
+
const seen: number[] = [];
|
|
4380
|
+
const { runScopedMemorySearch } = await import("./access-memory-search-fanout.js");
|
|
4381
|
+
const results = await runScopedMemorySearch({
|
|
4382
|
+
query: "q",
|
|
4383
|
+
budget: 5,
|
|
4384
|
+
sendInitialLimit: true,
|
|
4385
|
+
authorizeScope: () => {},
|
|
4386
|
+
namespacesEnabled: false,
|
|
4387
|
+
isExcluded: (memoryPath) => memoryPath.startsWith("artifacts/"),
|
|
4388
|
+
flatCorpus: async (limit) => {
|
|
4389
|
+
seen.push(limit ?? -1);
|
|
4390
|
+
return [{ path: "facts/only.md", score: 0.5, snippet: "only" }];
|
|
4391
|
+
},
|
|
4392
|
+
namespaced: async () => [],
|
|
4393
|
+
});
|
|
4394
|
+
assert.equal(results.length, 1);
|
|
4395
|
+
assert.deepEqual(seen, [5], "a short page means there is nothing left to fetch");
|
|
4396
|
+
});
|
|
4397
|
+
|
|
4398
|
+
test("HTTP memory search omits maxResults when the caller named no budget", async () => {
|
|
4399
|
+
// The wire stays exactly as it was for the default request: the backend's
|
|
4400
|
+
// own page size, not a resolved number. Top-up rounds are explicit.
|
|
4401
|
+
const seen: Array<number | undefined> = [];
|
|
4402
|
+
const { runScopedMemorySearch } = await import("./access-memory-search-fanout.js");
|
|
4403
|
+
const results = await runScopedMemorySearch({
|
|
4404
|
+
query: "q",
|
|
4405
|
+
budget: 3,
|
|
4406
|
+
sendInitialLimit: false,
|
|
4407
|
+
authorizeScope: () => {},
|
|
4408
|
+
namespacesEnabled: false,
|
|
4409
|
+
isExcluded: (memoryPath) => memoryPath.startsWith("artifacts/"),
|
|
4410
|
+
flatCorpus: async (limit) => {
|
|
4411
|
+
seen.push(limit);
|
|
4412
|
+
const corpus = [
|
|
4413
|
+
{ path: "artifacts/a1.md", score: 0.9, snippet: "artifact" },
|
|
4414
|
+
{ path: "facts/one.md", score: 0.5, snippet: "one" },
|
|
4415
|
+
{ path: "facts/two.md", score: 0.4, snippet: "two" },
|
|
4416
|
+
{ path: "facts/three.md", score: 0.3, snippet: "three" },
|
|
4417
|
+
];
|
|
4418
|
+
return limit === undefined ? corpus.slice(0, 3) : corpus.slice(0, limit);
|
|
4419
|
+
},
|
|
4420
|
+
namespaced: async () => [],
|
|
4421
|
+
});
|
|
4422
|
+
assert.deepEqual(seen, [undefined, 6], "first request carries no cap, the top-up does");
|
|
4423
|
+
assert.deepEqual(results.map((hit) => hit.path), [
|
|
4424
|
+
"facts/one.md",
|
|
4425
|
+
"facts/two.md",
|
|
4426
|
+
"facts/three.md",
|
|
4427
|
+
]);
|
|
4428
|
+
});
|
|
4429
|
+
|
|
4430
|
+
test("HTTP memory search keeps topping up past four excluded pages", async () => {
|
|
4431
|
+
// Eight pages of artifacts ahead of the real memories: a fixed round count
|
|
4432
|
+
// would give up and report a thin page while valid hits sit right behind.
|
|
4433
|
+
const corpus = [
|
|
4434
|
+
...Array.from({ length: 40 }, (_, index) => ({
|
|
4435
|
+
path: `artifacts/a${index}.md`,
|
|
4436
|
+
score: 1 - index / 100,
|
|
4437
|
+
snippet: "artifact",
|
|
4438
|
+
})),
|
|
4439
|
+
{ path: "facts/one.md", score: 0.1, snippet: "one" },
|
|
4440
|
+
{ path: "facts/two.md", score: 0.09, snippet: "two" },
|
|
4441
|
+
];
|
|
4442
|
+
const seen: Array<number | undefined> = [];
|
|
4443
|
+
const { runScopedMemorySearch } = await import("./access-memory-search-fanout.js");
|
|
4444
|
+
const results = await runScopedMemorySearch({
|
|
4445
|
+
query: "q",
|
|
4446
|
+
budget: 2,
|
|
4447
|
+
sendInitialLimit: true,
|
|
4448
|
+
authorizeScope: () => {},
|
|
4449
|
+
namespacesEnabled: false,
|
|
4450
|
+
isExcluded: (memoryPath) => memoryPath.startsWith("artifacts/"),
|
|
4451
|
+
flatCorpus: async (limit) => {
|
|
4452
|
+
seen.push(limit);
|
|
4453
|
+
return corpus.slice(0, limit);
|
|
4454
|
+
},
|
|
4455
|
+
namespaced: async () => [],
|
|
4456
|
+
});
|
|
4457
|
+
assert.deepEqual(seen, [2, 4, 8, 16, 32, 64], "doubles past four rounds until the budget is met");
|
|
4458
|
+
assert.deepEqual(results.map((hit) => hit.path), ["facts/one.md", "facts/two.md"]);
|
|
4459
|
+
});
|
|
4460
|
+
|
|
4461
|
+
test("HTTP memory search stops at the candidate ceiling", async () => {
|
|
4462
|
+
// A backend that always returns a full page of excluded hits must not be
|
|
4463
|
+
// asked for an unbounded number of candidates.
|
|
4464
|
+
const seen: Array<number | undefined> = [];
|
|
4465
|
+
const { runScopedMemorySearch } = await import("./access-memory-search-fanout.js");
|
|
4466
|
+
const results = await runScopedMemorySearch({
|
|
4467
|
+
query: "q",
|
|
4468
|
+
budget: 1,
|
|
4469
|
+
sendInitialLimit: true,
|
|
4470
|
+
authorizeScope: () => {},
|
|
4471
|
+
namespacesEnabled: false,
|
|
4472
|
+
isExcluded: () => true,
|
|
4473
|
+
flatCorpus: async (limit) => {
|
|
4474
|
+
seen.push(limit);
|
|
4475
|
+
return Array.from({ length: limit ?? 0 }, (_, index) => ({
|
|
4476
|
+
path: `artifacts/a${index}.md`,
|
|
4477
|
+
score: 0.5,
|
|
4478
|
+
snippet: "artifact",
|
|
4479
|
+
}));
|
|
4480
|
+
},
|
|
4481
|
+
namespaced: async () => [],
|
|
4482
|
+
});
|
|
4483
|
+
assert.deepEqual(results, []);
|
|
4484
|
+
// The cap is ABSOLUTE, not a multiple of the budget: a small request whose
|
|
4485
|
+
// hits are all excluded still walks far enough to prove the corpus holds
|
|
4486
|
+
// nothing for it, then stops on the backend-safety bound.
|
|
4487
|
+
assert.equal(seen.at(-1), 25_000, "the last request lands exactly on the cap");
|
|
4488
|
+
assert.ok(seen.length < 20, "and the loop terminates");
|
|
4489
|
+
});
|
|
4490
|
+
|
|
4491
|
+
test("HTTP memory search scales its ceiling above a large requested budget", async () => {
|
|
4492
|
+
// A fixed 1000 ceiling would stop at or below a 2000-result request, so one
|
|
4493
|
+
// excluded hit in the first page could never be replaced.
|
|
4494
|
+
const seen: Array<number | undefined> = [];
|
|
4495
|
+
const { runScopedMemorySearch } = await import("./access-memory-search-fanout.js");
|
|
4496
|
+
const results = await runScopedMemorySearch({
|
|
4497
|
+
query: "q",
|
|
4498
|
+
budget: 2_000,
|
|
4499
|
+
sendInitialLimit: true,
|
|
4500
|
+
authorizeScope: () => {},
|
|
4501
|
+
namespacesEnabled: false,
|
|
4502
|
+
isExcluded: (memoryPath) => memoryPath === "artifacts/a.md",
|
|
4503
|
+
flatCorpus: async (limit) => {
|
|
4504
|
+
seen.push(limit);
|
|
4505
|
+
const size = limit ?? 0;
|
|
4506
|
+
return [
|
|
4507
|
+
{ path: "artifacts/a.md", score: 1, snippet: "artifact" },
|
|
4508
|
+
...Array.from({ length: size - 1 }, (_, index) => ({
|
|
4509
|
+
path: `facts/f${index}.md`,
|
|
4510
|
+
score: 0.5,
|
|
4511
|
+
snippet: "fact",
|
|
4512
|
+
})),
|
|
4513
|
+
];
|
|
4514
|
+
},
|
|
4515
|
+
namespaced: async () => [],
|
|
4516
|
+
});
|
|
4517
|
+
assert.deepEqual(seen, [2_000, 4_000], "tops up past the requested budget");
|
|
4518
|
+
assert.equal(results.length, 2_000, "the requested page is delivered in full");
|
|
4519
|
+
});
|
|
4520
|
+
|
|
4521
|
+
test("HTTP memory search keeps the backend page size when no limit is named", async () => {
|
|
4522
|
+
// The backend's default page (6) is smaller than the configured cap (8).
|
|
4523
|
+
// Treating the cap as the target made a FULL page look short and reissued
|
|
4524
|
+
// the query, returning 8 where the same request used to return 6.
|
|
4525
|
+
const seen: Array<number | undefined> = [];
|
|
4526
|
+
const { runScopedMemorySearch } = await import("./access-memory-search-fanout.js");
|
|
4527
|
+
const results = await runScopedMemorySearch({
|
|
4528
|
+
query: "q",
|
|
4529
|
+
budget: 8,
|
|
4530
|
+
sendInitialLimit: false,
|
|
4531
|
+
authorizeScope: () => {},
|
|
4532
|
+
namespacesEnabled: false,
|
|
4533
|
+
isExcluded: () => false,
|
|
4534
|
+
flatCorpus: async (limit) => {
|
|
4535
|
+
seen.push(limit);
|
|
4536
|
+
const size = limit ?? 6;
|
|
4537
|
+
return Array.from({ length: size }, (_, index) => ({
|
|
4538
|
+
path: `facts/f${index}.md`,
|
|
4539
|
+
score: 0.5,
|
|
4540
|
+
snippet: "fact",
|
|
4541
|
+
}));
|
|
4542
|
+
},
|
|
4543
|
+
namespaced: async () => [],
|
|
4544
|
+
});
|
|
4545
|
+
assert.deepEqual(seen, [undefined], "one request, no spurious top-up");
|
|
4546
|
+
assert.equal(results.length, 6, "the backend's own page is returned intact");
|
|
4547
|
+
});
|
|
4548
|
+
|
|
4549
|
+
test("HTTP memory search still tops up a thinned default page", async () => {
|
|
4550
|
+
// Same shape, but an excluded hit really does shorten the page: the target
|
|
4551
|
+
// is the backend page size, so it tops up to restore it.
|
|
4552
|
+
const seen: Array<number | undefined> = [];
|
|
4553
|
+
const { runScopedMemorySearch } = await import("./access-memory-search-fanout.js");
|
|
4554
|
+
const results = await runScopedMemorySearch({
|
|
4555
|
+
query: "q",
|
|
4556
|
+
budget: 8,
|
|
4557
|
+
sendInitialLimit: false,
|
|
4558
|
+
authorizeScope: () => {},
|
|
4559
|
+
namespacesEnabled: false,
|
|
4560
|
+
isExcluded: (memoryPath) => memoryPath.startsWith("artifacts/"),
|
|
4561
|
+
flatCorpus: async (limit) => {
|
|
4562
|
+
seen.push(limit);
|
|
4563
|
+
const size = limit ?? 6;
|
|
4564
|
+
return [
|
|
4565
|
+
{ path: "artifacts/a.md", score: 1, snippet: "artifact" },
|
|
4566
|
+
...Array.from({ length: size - 1 }, (_, index) => ({
|
|
4567
|
+
path: `facts/f${index}.md`,
|
|
4568
|
+
score: 0.5,
|
|
4569
|
+
snippet: "fact",
|
|
4570
|
+
})),
|
|
4571
|
+
];
|
|
4572
|
+
},
|
|
4573
|
+
namespaced: async () => [],
|
|
4574
|
+
});
|
|
4575
|
+
assert.deepEqual(seen, [undefined, 12], "tops up against the backend page size");
|
|
4576
|
+
assert.equal(results.length, 6);
|
|
4577
|
+
});
|
|
4578
|
+
|
|
4579
|
+
test("search keeps archived memories but still excludes the dedicated surfaces", async () => {
|
|
4580
|
+
// The lifecycle reserves archived memories for explicit read/search
|
|
4581
|
+
// surfaces, so a ranked search must return them - while artifacts, activity
|
|
4582
|
+
// digests, and meeting records stay on their own paths.
|
|
4583
|
+
const { isSearchExcludedPath, isGenericRecallExcludedPath } = await import(
|
|
4584
|
+
"./orchestration/generic-recall-paths.js"
|
|
4585
|
+
);
|
|
4586
|
+
assert.equal(
|
|
4587
|
+
isSearchExcludedPath("archive/2026-01/fact-1.md"),
|
|
4588
|
+
false,
|
|
4589
|
+
"archived memories remain findable through search",
|
|
4590
|
+
);
|
|
4591
|
+
assert.equal(
|
|
4592
|
+
isGenericRecallExcludedPath("archive/2026-01/fact-1.md"),
|
|
4593
|
+
true,
|
|
4594
|
+
"but recall injection still skips them",
|
|
4595
|
+
);
|
|
4596
|
+
// Artifacts flow only through the dedicated verbatim path, in BOTH modes.
|
|
4597
|
+
assert.equal(isSearchExcludedPath("artifacts/report.md"), true);
|
|
4598
|
+
assert.equal(isGenericRecallExcludedPath("artifacts/report.md"), true);
|
|
4599
|
+
// And the wiring uses the search predicate, so an archived hit survives.
|
|
4600
|
+
const { runScopedMemorySearch } = await import("./access-memory-search-fanout.js");
|
|
4601
|
+
const results = await runScopedMemorySearch({
|
|
4602
|
+
query: "q",
|
|
4603
|
+
budget: 5,
|
|
4604
|
+
sendInitialLimit: true,
|
|
4605
|
+
authorizeScope: () => {},
|
|
4606
|
+
namespacesEnabled: false,
|
|
4607
|
+
isExcluded: (memoryPath) => isSearchExcludedPath(memoryPath),
|
|
4608
|
+
flatCorpus: async () => [
|
|
4609
|
+
{ path: "archive/2026-01/fact-1.md", score: 0.9, snippet: "archived" },
|
|
4610
|
+
{ path: "artifacts/report.md", score: 0.8, snippet: "artifact" },
|
|
4611
|
+
],
|
|
4612
|
+
namespaced: async () => [],
|
|
4613
|
+
});
|
|
4614
|
+
assert.deepEqual(results.map((hit) => hit.path), ["archive/2026-01/fact-1.md"]);
|
|
4615
|
+
});
|
|
4616
|
+
|
|
4617
|
+
test("a collection-qualified QMD path still hits the dedicated-surface exclusions", async () => {
|
|
4618
|
+
const { isSearchExcludedPath } = await import("./orchestration/generic-recall-paths.js");
|
|
4619
|
+
// A flat-corpus QMD transport returns `qmd://<collection>/<path>` or
|
|
4620
|
+
// `<collection>/<path>`. The activity predicate is root-aware, so an
|
|
4621
|
+
// un-stripped prefix reads as a NESTED path and the digest leaks into
|
|
4622
|
+
// ranked search.
|
|
4623
|
+
const policy = { memoryDir: "/memory", qmdCollection: "memories" };
|
|
4624
|
+
|
|
4625
|
+
for (const excluded of [
|
|
4626
|
+
"qmd://memories/activity/2026-08-02.md",
|
|
4627
|
+
"memories/activity/2026-08-02.md",
|
|
4628
|
+
"qmd://memories/artifacts/report.md",
|
|
4629
|
+
"memories/meetings/2026-01-01/mtg-2026-01-01-abcdef12.md",
|
|
4630
|
+
]) {
|
|
4631
|
+
assert.equal(isSearchExcludedPath(excluded, policy, "qmd"), true, excluded);
|
|
4632
|
+
}
|
|
4633
|
+
// A namespaced search rewrites hits to absolute paths beneath the
|
|
4634
|
+
// namespace's own storage root; the digest there must be excluded too.
|
|
4635
|
+
assert.equal(
|
|
4636
|
+
isSearchExcludedPath("/memory/namespaces/team/activity/2026-08-02.md", policy, "qmd"),
|
|
4637
|
+
true,
|
|
4638
|
+
"a digest under a namespace root is still a digest",
|
|
4639
|
+
);
|
|
4640
|
+
assert.equal(
|
|
4641
|
+
isSearchExcludedPath("/memory/namespaces/team/facts/proj/activity/2026-08-02.md", policy, "qmd"),
|
|
4642
|
+
false,
|
|
4643
|
+
"and an ordinary nested memory under that root stays searchable",
|
|
4644
|
+
);
|
|
4645
|
+
// A collection whose NAME is also a memory category is disambiguated two
|
|
4646
|
+
// ways: a `qmd://` URI states it in the authority, and a bare path is
|
|
4647
|
+
// resolved against the collection the caller actually requested.
|
|
4648
|
+
assert.equal(
|
|
4649
|
+
isSearchExcludedPath("qmd://facts/activity/2026-08-02.md", policy, "qmd"),
|
|
4650
|
+
true,
|
|
4651
|
+
"the URI authority is unambiguously the collection",
|
|
4652
|
+
);
|
|
4653
|
+
assert.equal(
|
|
4654
|
+
isSearchExcludedPath(
|
|
4655
|
+
"facts/activity/2026-08-02.md",
|
|
4656
|
+
{ ...policy, requestedCollection: "facts" },
|
|
4657
|
+
"qmd",
|
|
4658
|
+
),
|
|
4659
|
+
true,
|
|
4660
|
+
"the caller named `facts` as its collection",
|
|
4661
|
+
);
|
|
4662
|
+
assert.equal(
|
|
4663
|
+
isSearchExcludedPath("facts/activity/2026-08-02.md", policy, "qmd"),
|
|
4664
|
+
false,
|
|
4665
|
+
"without that, `facts` stays a memory category and the memory is searchable",
|
|
4666
|
+
);
|
|
4667
|
+
// A collection this policy does NOT know — a caller-named custom one, or
|
|
4668
|
+
// any collection global search spans — is covered too.
|
|
4669
|
+
for (const excluded of [
|
|
4670
|
+
"custom/activity/2026-08-02.md",
|
|
4671
|
+
"qmd://custom/activity/2026-08-02.md",
|
|
4672
|
+
"custom/artifacts/report.md",
|
|
4673
|
+
]) {
|
|
4674
|
+
assert.equal(isSearchExcludedPath(excluded, policy, "qmd"), true, excluded);
|
|
4675
|
+
}
|
|
4676
|
+
// A nested ordinary memory that merely looks like a digest stays searchable,
|
|
4677
|
+
// including under a custom collection, and so does an archived one. A
|
|
4678
|
+
// leading segment naming a memory CATEGORY is never read as a collection.
|
|
4679
|
+
for (const kept of [
|
|
4680
|
+
"qmd://memories/facts/proj/activity/2026-08-02.md",
|
|
4681
|
+
"custom/facts/proj/activity/2026-08-02.md",
|
|
4682
|
+
// The configured collection is stripped ONCE. Removing `projects` too
|
|
4683
|
+
// would leave `activity/<date>.md` and hide an ordinary nested memory
|
|
4684
|
+
// from search and recall alike.
|
|
4685
|
+
"memories/projects/activity/2026-08-02.md",
|
|
4686
|
+
"qmd://memories/projects/activity/2026-08-02.md",
|
|
4687
|
+
"facts/activity/2026-08-02.md",
|
|
4688
|
+
"memories/archive/2026-01/fact-1.md",
|
|
4689
|
+
]) {
|
|
4690
|
+
assert.equal(isSearchExcludedPath(kept, policy, "qmd"), false, kept);
|
|
4691
|
+
}
|
|
4692
|
+
});
|
|
4693
|
+
|
|
4694
|
+
test("search keeps paging while the backend page is full of excluded hits", async () => {
|
|
4695
|
+
// A budget-proportional ceiling made "the excluded paths rank first"
|
|
4696
|
+
// indistinguishable from "there is nothing else": a 1,000-row request whose
|
|
4697
|
+
// first 4,000 hits are artifacts stopped at 4,000 and answered empty while
|
|
4698
|
+
// valid memories sat at rank 4,001.
|
|
4699
|
+
const { searchWithGenericExclusion } = await import("./access-memory-search-fanout.js");
|
|
4700
|
+
const corpus = [
|
|
4701
|
+
...Array.from({ length: 4_000 }, (_, index) => ({ path: `artifacts/a-${index}.md` })),
|
|
4702
|
+
...Array.from({ length: 50, }, (_, index) => ({ path: `facts/f-${index}.md` })),
|
|
4703
|
+
];
|
|
4704
|
+
const limits: Array<number | undefined> = [];
|
|
4705
|
+
const results = await searchWithGenericExclusion({
|
|
4706
|
+
budget: 1_000,
|
|
4707
|
+
sendInitialLimit: true,
|
|
4708
|
+
search: async (limit) => {
|
|
4709
|
+
limits.push(limit);
|
|
4710
|
+
return corpus.slice(0, limit ?? corpus.length);
|
|
4711
|
+
},
|
|
4712
|
+
isExcluded: (memoryPath) => memoryPath.startsWith("artifacts/"),
|
|
4713
|
+
});
|
|
4714
|
+
assert.equal(results.length, 50, "the memories behind the excluded block are returned");
|
|
4715
|
+
assert.ok(limits.length > 1, "and it took more than the first page to reach them");
|
|
4716
|
+
assert.ok(
|
|
4717
|
+
limits.every((limit) => (limit ?? 0) <= 25_000),
|
|
4718
|
+
"while still respecting the absolute backend cap",
|
|
4719
|
+
);
|
|
4720
|
+
});
|
|
4721
|
+
|
|
4722
|
+
test("a budget above the backend cap still gets the rows it asked for", async () => {
|
|
4723
|
+
// The cap protects the backend from an unbounded walk; it must never sit
|
|
4724
|
+
// BELOW an explicit request, or a large search returns a short page for a
|
|
4725
|
+
// count the operation deliberately supports.
|
|
4726
|
+
const { searchWithGenericExclusion } = await import("./access-memory-search-fanout.js");
|
|
4727
|
+
const corpus = [
|
|
4728
|
+
{ path: "artifacts/excluded.md" },
|
|
4729
|
+
...Array.from({ length: 30_000 }, (_, index) => ({ path: `facts/f-${index}.md` })),
|
|
4730
|
+
];
|
|
4731
|
+
const results = await searchWithGenericExclusion({
|
|
4732
|
+
budget: 30_000,
|
|
4733
|
+
sendInitialLimit: true,
|
|
4734
|
+
search: async (limit) => corpus.slice(0, limit ?? corpus.length),
|
|
4735
|
+
isExcluded: (memoryPath) => memoryPath.startsWith("artifacts/"),
|
|
4736
|
+
});
|
|
4737
|
+
assert.equal(results.length, 30_000, "the excluded hit was replaced, not subtracted");
|
|
4738
|
+
});
|