@0xmaxma/claude-gateway 1.6.8 → 1.7.0
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/README.md +149 -20
- package/config.template.json +59 -1
- package/dist/agent/dreaming/accept.d.ts +43 -0
- package/dist/agent/dreaming/accept.d.ts.map +1 -0
- package/dist/agent/dreaming/accept.js +230 -0
- package/dist/agent/dreaming/accept.js.map +1 -0
- package/dist/agent/dreaming/applier.d.ts +77 -0
- package/dist/agent/dreaming/applier.d.ts.map +1 -0
- package/dist/agent/dreaming/applier.js +406 -0
- package/dist/agent/dreaming/applier.js.map +1 -0
- package/dist/agent/dreaming/audit.d.ts +27 -0
- package/dist/agent/dreaming/audit.d.ts.map +1 -0
- package/dist/agent/dreaming/audit.js +100 -0
- package/dist/agent/dreaming/audit.js.map +1 -0
- package/dist/agent/dreaming/compactor.d.ts +66 -0
- package/dist/agent/dreaming/compactor.d.ts.map +1 -0
- package/dist/agent/dreaming/compactor.js +306 -0
- package/dist/agent/dreaming/compactor.js.map +1 -0
- package/dist/agent/dreaming/config.d.ts +19 -0
- package/dist/agent/dreaming/config.d.ts.map +1 -0
- package/dist/agent/dreaming/config.js +107 -0
- package/dist/agent/dreaming/config.js.map +1 -0
- package/dist/agent/dreaming/episodic.d.ts +42 -0
- package/dist/agent/dreaming/episodic.d.ts.map +1 -0
- package/dist/agent/dreaming/episodic.js +116 -0
- package/dist/agent/dreaming/episodic.js.map +1 -0
- package/dist/agent/dreaming/gather.d.ts +31 -0
- package/dist/agent/dreaming/gather.d.ts.map +1 -0
- package/dist/agent/dreaming/gather.js +55 -0
- package/dist/agent/dreaming/gather.js.map +1 -0
- package/dist/agent/dreaming/index.d.ts +87 -0
- package/dist/agent/dreaming/index.d.ts.map +1 -0
- package/dist/agent/dreaming/index.js +377 -0
- package/dist/agent/dreaming/index.js.map +1 -0
- package/dist/agent/dreaming/migrate-cli.d.ts +17 -0
- package/dist/agent/dreaming/migrate-cli.d.ts.map +1 -0
- package/dist/agent/dreaming/migrate-cli.js +42 -0
- package/dist/agent/dreaming/migrate-cli.js.map +1 -0
- package/dist/agent/dreaming/migrate.d.ts +72 -0
- package/dist/agent/dreaming/migrate.d.ts.map +1 -0
- package/dist/agent/dreaming/migrate.js +374 -0
- package/dist/agent/dreaming/migrate.js.map +1 -0
- package/dist/agent/dreaming/report.d.ts +39 -0
- package/dist/agent/dreaming/report.d.ts.map +1 -0
- package/dist/agent/dreaming/report.js +121 -0
- package/dist/agent/dreaming/report.js.map +1 -0
- package/dist/agent/dreaming/reviewer.d.ts +43 -0
- package/dist/agent/dreaming/reviewer.d.ts.map +1 -0
- package/dist/agent/dreaming/reviewer.js +249 -0
- package/dist/agent/dreaming/reviewer.js.map +1 -0
- package/dist/agent/dreaming/staleness.d.ts +68 -0
- package/dist/agent/dreaming/staleness.d.ts.map +1 -0
- package/dist/agent/dreaming/staleness.js +281 -0
- package/dist/agent/dreaming/staleness.js.map +1 -0
- package/dist/agent/dreaming/types.d.ts +130 -0
- package/dist/agent/dreaming/types.d.ts.map +1 -0
- package/dist/agent/dreaming/types.js +10 -0
- package/dist/agent/dreaming/types.js.map +1 -0
- package/dist/agent/knowledge/archive-db.d.ts +114 -0
- package/dist/agent/knowledge/archive-db.d.ts.map +1 -0
- package/dist/agent/knowledge/archive-db.js +474 -0
- package/dist/agent/knowledge/archive-db.js.map +1 -0
- package/dist/agent/knowledge/chunk.d.ts +21 -0
- package/dist/agent/knowledge/chunk.d.ts.map +1 -0
- package/dist/agent/knowledge/chunk.js +67 -0
- package/dist/agent/knowledge/chunk.js.map +1 -0
- package/dist/agent/knowledge/config.d.ts +20 -0
- package/dist/agent/knowledge/config.d.ts.map +1 -0
- package/dist/agent/knowledge/config.js +143 -0
- package/dist/agent/knowledge/config.js.map +1 -0
- package/dist/agent/knowledge/graph-demo.d.ts +25 -0
- package/dist/agent/knowledge/graph-demo.d.ts.map +1 -0
- package/dist/agent/knowledge/graph-demo.js +132 -0
- package/dist/agent/knowledge/graph-demo.js.map +1 -0
- package/dist/agent/knowledge/index.d.ts +24 -0
- package/dist/agent/knowledge/index.d.ts.map +1 -0
- package/dist/agent/knowledge/index.js +50 -0
- package/dist/agent/knowledge/index.js.map +1 -0
- package/dist/agent/knowledge/indexer.d.ts +34 -0
- package/dist/agent/knowledge/indexer.d.ts.map +1 -0
- package/dist/agent/knowledge/indexer.js +271 -0
- package/dist/agent/knowledge/indexer.js.map +1 -0
- package/dist/agent/knowledge/lifecycle.d.ts +91 -0
- package/dist/agent/knowledge/lifecycle.d.ts.map +1 -0
- package/dist/agent/knowledge/lifecycle.js +205 -0
- package/dist/agent/knowledge/lifecycle.js.map +1 -0
- package/dist/agent/knowledge/provenance.d.ts +15 -0
- package/dist/agent/knowledge/provenance.d.ts.map +1 -0
- package/dist/agent/knowledge/provenance.js +37 -0
- package/dist/agent/knowledge/provenance.js.map +1 -0
- package/dist/agent/knowledge/reindex-cli.d.ts +13 -0
- package/dist/agent/knowledge/reindex-cli.d.ts.map +1 -0
- package/dist/agent/knowledge/reindex-cli.js +53 -0
- package/dist/agent/knowledge/reindex-cli.js.map +1 -0
- package/dist/agent/knowledge/reindex-spawn.d.ts +30 -0
- package/dist/agent/knowledge/reindex-spawn.d.ts.map +1 -0
- package/dist/agent/knowledge/reindex-spawn.js +97 -0
- package/dist/agent/knowledge/reindex-spawn.js.map +1 -0
- package/dist/agent/knowledge/shared-promote.d.ts +18 -0
- package/dist/agent/knowledge/shared-promote.d.ts.map +1 -0
- package/dist/agent/knowledge/shared-promote.js +29 -0
- package/dist/agent/knowledge/shared-promote.js.map +1 -0
- package/dist/agent/knowledge/shared-writer.d.ts +24 -0
- package/dist/agent/knowledge/shared-writer.d.ts.map +1 -0
- package/dist/agent/knowledge/shared-writer.js +97 -0
- package/dist/agent/knowledge/shared-writer.js.map +1 -0
- package/dist/agent/knowledge/types.d.ts +83 -0
- package/dist/agent/knowledge/types.d.ts.map +1 -0
- package/dist/agent/knowledge/types.js +12 -0
- package/dist/agent/knowledge/types.js.map +1 -0
- package/dist/agent/knowledge/wiki.d.ts +107 -0
- package/dist/agent/knowledge/wiki.d.ts.map +1 -0
- package/dist/agent/knowledge/wiki.js +415 -0
- package/dist/agent/knowledge/wiki.js.map +1 -0
- package/dist/agent/memory-metrics.d.ts +48 -0
- package/dist/agent/memory-metrics.d.ts.map +1 -0
- package/dist/agent/memory-metrics.js +156 -0
- package/dist/agent/memory-metrics.js.map +1 -0
- package/dist/agent/runner.d.ts +24 -1
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +82 -6
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/skill-learning/config.d.ts +18 -0
- package/dist/agent/skill-learning/config.d.ts.map +1 -0
- package/dist/agent/skill-learning/config.js +71 -0
- package/dist/agent/skill-learning/config.js.map +1 -0
- package/dist/agent/skill-learning/curator.d.ts +45 -0
- package/dist/agent/skill-learning/curator.d.ts.map +1 -0
- package/dist/agent/skill-learning/curator.js +171 -0
- package/dist/agent/skill-learning/curator.js.map +1 -0
- package/dist/agent/skill-learning/index.d.ts +79 -0
- package/dist/agent/skill-learning/index.d.ts.map +1 -0
- package/dist/agent/skill-learning/index.js +308 -0
- package/dist/agent/skill-learning/index.js.map +1 -0
- package/dist/agent/skill-learning/metrics.d.ts +13 -0
- package/dist/agent/skill-learning/metrics.d.ts.map +1 -0
- package/dist/agent/skill-learning/metrics.js +109 -0
- package/dist/agent/skill-learning/metrics.js.map +1 -0
- package/dist/agent/skill-learning/notifier.d.ts +111 -0
- package/dist/agent/skill-learning/notifier.d.ts.map +1 -0
- package/dist/agent/skill-learning/notifier.js +398 -0
- package/dist/agent/skill-learning/notifier.js.map +1 -0
- package/dist/agent/skill-learning/reviewer.d.ts +61 -0
- package/dist/agent/skill-learning/reviewer.d.ts.map +1 -0
- package/dist/agent/skill-learning/reviewer.js +271 -0
- package/dist/agent/skill-learning/reviewer.js.map +1 -0
- package/dist/agent/skill-learning/telemetry.d.ts +22 -0
- package/dist/agent/skill-learning/telemetry.d.ts.map +1 -0
- package/dist/agent/skill-learning/telemetry.js +90 -0
- package/dist/agent/skill-learning/telemetry.js.map +1 -0
- package/dist/agent/skill-learning/trigger.d.ts +23 -0
- package/dist/agent/skill-learning/trigger.d.ts.map +1 -0
- package/dist/agent/skill-learning/trigger.js +44 -0
- package/dist/agent/skill-learning/trigger.js.map +1 -0
- package/dist/agent/skill-learning/types.d.ts +165 -0
- package/dist/agent/skill-learning/types.d.ts.map +1 -0
- package/dist/agent/skill-learning/types.js +11 -0
- package/dist/agent/skill-learning/types.js.map +1 -0
- package/dist/agent/skill-learning/writer.d.ts +37 -0
- package/dist/agent/skill-learning/writer.d.ts.map +1 -0
- package/dist/agent/skill-learning/writer.js +223 -0
- package/dist/agent/skill-learning/writer.js.map +1 -0
- package/dist/agent/workspace-loader.d.ts +83 -6
- package/dist/agent/workspace-loader.d.ts.map +1 -1
- package/dist/agent/workspace-loader.js +207 -14
- package/dist/agent/workspace-loader.js.map +1 -1
- package/dist/api/gateway-router.d.ts +2 -0
- package/dist/api/gateway-router.d.ts.map +1 -1
- package/dist/api/gateway-router.js +293 -6
- package/dist/api/gateway-router.js.map +1 -1
- package/dist/api/share-router.d.ts.map +1 -1
- package/dist/api/share-router.js +27 -1
- package/dist/api/share-router.js.map +1 -1
- package/dist/api/skills-router.d.ts +2 -2
- package/dist/api/skills-router.d.ts.map +1 -1
- package/dist/api/skills-router.js +61 -1
- package/dist/api/skills-router.js.map +1 -1
- package/dist/apps/agent-manager.d.ts +7 -0
- package/dist/apps/agent-manager.d.ts.map +1 -1
- package/dist/apps/agent-manager.js +13 -1
- package/dist/apps/agent-manager.js.map +1 -1
- package/dist/config/migrator.d.ts +2 -0
- package/dist/config/migrator.d.ts.map +1 -1
- package/dist/config/migrator.js +72 -1
- package/dist/config/migrator.js.map +1 -1
- package/dist/history/cleanup.d.ts +7 -0
- package/dist/history/cleanup.d.ts.map +1 -1
- package/dist/history/cleanup.js +27 -0
- package/dist/history/cleanup.js.map +1 -1
- package/dist/history/db.d.ts +65 -0
- package/dist/history/db.d.ts.map +1 -1
- package/dist/history/db.js +261 -0
- package/dist/history/db.js.map +1 -1
- package/dist/index.js +122 -20
- package/dist/index.js.map +1 -1
- package/dist/session/process.d.ts +10 -0
- package/dist/session/process.d.ts.map +1 -1
- package/dist/session/process.js +86 -1
- package/dist/session/process.js.map +1 -1
- package/dist/share/share-store.d.ts +11 -0
- package/dist/share/share-store.d.ts.map +1 -1
- package/dist/share/share-store.js +3 -1
- package/dist/share/share-store.js.map +1 -1
- package/dist/skills/loader.d.ts +8 -0
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +29 -1
- package/dist/skills/loader.js.map +1 -1
- package/dist/types.d.ts +116 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/web-ui.d.ts.map +1 -1
- package/dist/ui/web-ui.js +1126 -3
- package/dist/ui/web-ui.js.map +1 -1
- package/mcp/package.json +2 -1
- package/mcp/server.ts +2 -0
- package/mcp/tools/image/module.ts +114 -9
- package/mcp/tools/memory/archive-reader.test.ts +212 -0
- package/mcp/tools/memory/archive-reader.ts +253 -0
- package/mcp/tools/memory/module.ts +152 -0
- package/mcp/tools/memory/retrieval-recorder.ts +66 -0
- package/mcp/tools/shared/share-client.ts +20 -1
- package/mcp/tools/skills/module.ts +28 -0
- package/package.json +1 -1
- package/resource/claude_gateway.svg +1038 -112
package/dist/ui/web-ui.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web-ui.js","sourceRoot":"","sources":["../../src/ui/web-ui.ts"],"names":[],"mappings":";;AAIA,
|
|
1
|
+
{"version":3,"file":"web-ui.js","sourceRoot":"","sources":["../../src/ui/web-ui.ts"],"names":[],"mappings":";;AAIA,sDAw+DC;AAQD,8CAiFC;AArkED;;;GAGG;AACH,SAAgB,qBAAqB;IACnC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAs+DD,CAAC;AACT,CAAC;AAED;;;;;GAKG;AACH,SAAgB,iBAAiB,CAAC,cAAc,GAAG,EAAE;IACnD,MAAM,UAAU,GAAG,cAAc;SAC9B,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IAC9F,+EAA+E;IAC/E,8EAA8E;IAC9E,MAAM,SAAS,GAAG,UAAU;QAC1B,CAAC,CAAC;uBACiB,UAAU,QAAQ;QACrC,CAAC,CAAC;;;;;;;YAOM,CAAC;IACX,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmCP,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA6BH,CAAC;AACT,CAAC"}
|
package/mcp/package.json
CHANGED
package/mcp/server.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { ImageModule } from './tools/image/module';
|
|
|
23
23
|
import { ShareImageModule } from './tools/share-image/module';
|
|
24
24
|
import { AppsModule } from './tools/apps/module';
|
|
25
25
|
import { ApiModule } from './tools/api/module';
|
|
26
|
+
import { MemoryModule } from './tools/memory/module';
|
|
26
27
|
import { buildChannelInstructions } from './instructions';
|
|
27
28
|
import type { ChannelModule, ToolModule, McpToolDefinition } from './types';
|
|
28
29
|
|
|
@@ -46,6 +47,7 @@ const modules: AnyModule[] = [
|
|
|
46
47
|
new ShareImageModule(),
|
|
47
48
|
new AppsModule(),
|
|
48
49
|
new ApiModule(),
|
|
50
|
+
new MemoryModule(),
|
|
49
51
|
];
|
|
50
52
|
|
|
51
53
|
// Build tool-to-module mapping for enabled modules
|
|
@@ -60,6 +60,20 @@ const ERROR_HINTS: Record<string, string> = {
|
|
|
60
60
|
type JobResponse = {
|
|
61
61
|
task_id?: string;
|
|
62
62
|
status?: 'queued' | 'running' | 'done' | 'failed';
|
|
63
|
+
// What actually generated (or is generating) the image — the image service now
|
|
64
|
+
// echoes these on every poll, so a status re-poll in a LATER turn (which no
|
|
65
|
+
// longer has the original generate call's "model" argument in scope) can
|
|
66
|
+
// still recover them instead of falling back to "unknown".
|
|
67
|
+
provider?: string;
|
|
68
|
+
model?: string;
|
|
69
|
+
// The upstream provider's OWN task id — distinct from this job's own
|
|
70
|
+
// task_id (the image service's own job id, used for polling). A live E2E test
|
|
71
|
+
// caught that these are two separate id spaces: the image provider's own
|
|
72
|
+
// Store is keyed by ITS OWN ids, so a later resume request naming the wrong
|
|
73
|
+
// (outer) id silently fails to resolve. THIS is the id that must be
|
|
74
|
+
// persisted for continue_from to ever work. Absent for providers with no
|
|
75
|
+
// resume concept.
|
|
76
|
+
provider_task_id?: string;
|
|
63
77
|
byok?: boolean;
|
|
64
78
|
cost?: number;
|
|
65
79
|
images?: string[];
|
|
@@ -279,11 +293,44 @@ export class ImageModule implements ToolModule {
|
|
|
279
293
|
if (rawImage && rawImages) {
|
|
280
294
|
return { content: [{ type: 'text', text: 'generate_image: pass either "image" or "images", not both.' }], isError: true };
|
|
281
295
|
}
|
|
282
|
-
const
|
|
296
|
+
const refsList = rawImage ? [rawImage] : rawImages!;
|
|
297
|
+
const normalized = await this.normalizeRefs(refsList);
|
|
283
298
|
if ('error' in normalized) return normalized.error;
|
|
284
299
|
mintedShareIds = normalized.mintedShareIds;
|
|
285
300
|
if (rawImage) reqBody.image = normalized.urls[0];
|
|
286
301
|
else reqBody.images = normalized.urls;
|
|
302
|
+
|
|
303
|
+
// continue_from is the LLM's sole judgment call on
|
|
304
|
+
// whether this is a true continuation of a prior own-generation, not
|
|
305
|
+
// just a visual reference. Resolve it to that ref's provider task id —
|
|
306
|
+
// found ONLY by locating a matching artifact: entry among the refs just
|
|
307
|
+
// normalized above (never invented, never taken on faith). No match, or
|
|
308
|
+
// a match with no recorded task id (path ref, or an artifact with none)
|
|
309
|
+
// → silently omitted, exactly like any other unresolved resume signal
|
|
310
|
+
// downstream; never an error.
|
|
311
|
+
// Defensively strip a leading "artifact:" — the schema asks for the bare
|
|
312
|
+
// id, but tolerate the prefixed form rather than silently failing to match.
|
|
313
|
+
const continueFrom = (typeof args.continue_from === 'string' ? args.continue_from.trim() : '').replace(/^artifact:/i, '');
|
|
314
|
+
if (continueFrom) {
|
|
315
|
+
const idx = refsList.findIndex((r) => r.trim() === `artifact:${continueFrom}`);
|
|
316
|
+
const resumeTaskId = idx !== -1 ? normalized.taskIds[idx] : undefined;
|
|
317
|
+
if (resumeTaskId) {
|
|
318
|
+
reqBody.resume_task_id = resumeTaskId;
|
|
319
|
+
} else {
|
|
320
|
+
// Handoff-on-model-switch : resume isn't possible
|
|
321
|
+
// (no provider task id resolved — most commonly because a
|
|
322
|
+
// different model generated something since), but continue_from
|
|
323
|
+
// still signals a genuine continuation. Deterministically prepend
|
|
324
|
+
// the prompt that produced the referenced artifact — reused from
|
|
325
|
+
// stored data, not re-derived from the LLM's own memory, so it
|
|
326
|
+
// can't drift or balloon across a long edit chain (only ever ONE
|
|
327
|
+
// prior step is pulled in, never accumulated).
|
|
328
|
+
const priorPrompt = idx !== -1 ? normalized.priorPrompts[idx] : undefined;
|
|
329
|
+
if (priorPrompt) {
|
|
330
|
+
reqBody.prompt = `(Continuing from a prior edit: "${priorPrompt}") ${prompt}`;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
287
334
|
} else {
|
|
288
335
|
if (rawImage) reqBody.image = rawImage;
|
|
289
336
|
if (rawImages) reqBody.images = rawImages;
|
|
@@ -385,7 +432,15 @@ export class ImageModule implements ToolModule {
|
|
|
385
432
|
*/
|
|
386
433
|
private async normalizeRefs(
|
|
387
434
|
refs: string[],
|
|
388
|
-
): Promise<
|
|
435
|
+
): Promise<
|
|
436
|
+
| {
|
|
437
|
+
urls: string[];
|
|
438
|
+
mintedShareIds: string[];
|
|
439
|
+
taskIds: Array<string | undefined>;
|
|
440
|
+
priorPrompts: Array<string | undefined>;
|
|
441
|
+
}
|
|
442
|
+
| { error: McpToolResult }
|
|
443
|
+
> {
|
|
389
444
|
const fail = (text: string): { error: McpToolResult } => ({
|
|
390
445
|
error: { content: [{ type: 'text', text }], isError: true },
|
|
391
446
|
});
|
|
@@ -445,10 +500,27 @@ export class ImageModule implements ToolModule {
|
|
|
445
500
|
return fail('generate_image: image reference sharing requires gateway.publicUrl to be configured.');
|
|
446
501
|
}
|
|
447
502
|
}
|
|
448
|
-
// Merge back preserving the caller's order.
|
|
503
|
+
// Merge back preserving the caller's order. taskIds/priorPrompts are
|
|
504
|
+
// index-aligned with refs/urls — undefined for
|
|
505
|
+
// raw-URL entries and any local ref whose resolved artifact carries
|
|
506
|
+
// neither.
|
|
449
507
|
let next = 0;
|
|
450
|
-
const urls =
|
|
451
|
-
|
|
508
|
+
const urls: string[] = [];
|
|
509
|
+
const taskIds: Array<string | undefined> = [];
|
|
510
|
+
const priorPrompts: Array<string | undefined> = [];
|
|
511
|
+
for (const e of entries) {
|
|
512
|
+
if (e.kind === 'url') {
|
|
513
|
+
urls.push(e.url);
|
|
514
|
+
taskIds.push(undefined);
|
|
515
|
+
priorPrompts.push(undefined);
|
|
516
|
+
} else {
|
|
517
|
+
const m = minted[next++]!;
|
|
518
|
+
urls.push(m.url!);
|
|
519
|
+
taskIds.push(m.task_id);
|
|
520
|
+
priorPrompts.push(m.prior_prompt);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return { urls, mintedShareIds: minted.map((m) => m.share_id), taskIds, priorPrompts };
|
|
452
524
|
}
|
|
453
525
|
|
|
454
526
|
private pollTimeoutMs(): number {
|
|
@@ -552,10 +624,19 @@ export class ImageModule implements ToolModule {
|
|
|
552
624
|
// artifacts — file delivery is unaffected.
|
|
553
625
|
let artifacts: Array<Record<string, unknown>> | undefined;
|
|
554
626
|
if (shareBridgeEnabled() && files.length) {
|
|
627
|
+
// `model` (the caller's "provider/model" arg) is only known on the
|
|
628
|
+
// generate path (same-turn poll) — same limitation as `prompt` above.
|
|
629
|
+
// On an action="status" re-entry, fall back to job.provider/job.model,
|
|
630
|
+
// which the image service now echoes on every poll response for exactly
|
|
631
|
+
// this reason, instead of silently mislabeling the artifact "unknown".
|
|
555
632
|
const slash = (model ?? '').indexOf('/');
|
|
556
|
-
const provider = slash > 0 ? (model as string).slice(0, slash) : 'unknown';
|
|
557
|
-
const modelName = slash > 0 ? (model as string).slice(slash + 1) : (model || 'unknown');
|
|
558
|
-
|
|
633
|
+
const provider = slash > 0 ? (model as string).slice(0, slash) : (job.provider || 'unknown');
|
|
634
|
+
const modelName = slash > 0 ? (model as string).slice(slash + 1) : (job.model || model || 'unknown');
|
|
635
|
+
// taskId (this job's own id, used for polling above) is NOT what
|
|
636
|
+
// gets persisted here — a resumable artifact must be keyed by the
|
|
637
|
+
// PROVIDER's own task id (job.provider_task_id), the id its Store
|
|
638
|
+
// actually recognizes. See the JobResponse.provider_task_id comment.
|
|
639
|
+
const registered = await registerArtifacts(files, { provider, model: modelName, taskId: job.provider_task_id, prompt });
|
|
559
640
|
if (registered) {
|
|
560
641
|
artifacts = registered.map((item, index) => ({
|
|
561
642
|
artifact_id: item.artifact_id,
|
|
@@ -799,6 +880,14 @@ const imageToolDefs: McpToolDefinition[] = [
|
|
|
799
880
|
'attach it with your reply tool and answer in one or two short sentences. Every extra step risks pushing ' +
|
|
800
881
|
'the request past its timeout. ' +
|
|
801
882
|
'action="status" polls a previously returned task_id. ' +
|
|
883
|
+
'PATIENCE: image generation can legitimately take several minutes — a "running" status (including the ' +
|
|
884
|
+
'"still generating, call again with action=status" note you get back when the local poll budget runs out) ' +
|
|
885
|
+
'is normal, not stuck. Keep calling action="status" with the SAME task_id until it resolves to done/failed. ' +
|
|
886
|
+
'Do NOT submit a new action="generate" call (on the same or a different model) for the same request while ' +
|
|
887
|
+
'an earlier task_id for it is still running — the earlier job may finish moments later, and you will have ' +
|
|
888
|
+
'generated and charged for the image twice while delivering only one. If you are genuinely considering ' +
|
|
889
|
+
'giving up after a long wait, say so to the user first in a normal reply and let them decide, instead of ' +
|
|
890
|
+
'silently abandoning the task_id and starting over. ' +
|
|
802
891
|
'action="list" returns every available image model with its supported_qualities, supported_sizes, cost, and ' +
|
|
803
892
|
'the capability flags supports_image_ref (image-to-image / edit) and supports_style_ref. Call it FIRST when ' +
|
|
804
893
|
'choosing a model or when you need to know what a provider can do — you are NOT limited to the composer ' +
|
|
@@ -817,7 +906,15 @@ const imageToolDefs: McpToolDefinition[] = [
|
|
|
817
906
|
'been reset while the chat history still has them): never answer "no images attached" to such a ' +
|
|
818
907
|
'request before list_refs has returned an empty catalog. If the catalog makes the request ambiguous ' +
|
|
819
908
|
'(or the requested index does not exist), ask the user which image they mean instead of guessing. ' +
|
|
820
|
-
'When the user selected options in the composer (an <image-params .../> tag in the turn), honor those values.'
|
|
909
|
+
'When the user selected options in the composer (an <image-params .../> tag in the turn), honor those values. ' +
|
|
910
|
+
'CONTINUING AN EDIT: some providers can resume the exact generation session behind an image instead of ' +
|
|
911
|
+
'treating it as a fresh visual reference. Set "continue_from" to that image\'s artifact id ONLY when ALL of ' +
|
|
912
|
+
'these hold: (1) you generated that image yourself earlier in this session (not something the user uploaded ' +
|
|
913
|
+
'or an external URL), (2) that same artifact id is also included in this call\'s "image"/"images", (3) this ' +
|
|
914
|
+
'call uses the SAME "model" that produced it, and (4) no OTHER model has generated any image in this session ' +
|
|
915
|
+
'since that image was made — even if you are back on the original model now, a model switch in between ' +
|
|
916
|
+
'breaks the continuation. If any of these do not hold, omit "continue_from" and rely on the plain image ' +
|
|
917
|
+
'reference instead — this is safe by default; the resume is a bonus when it applies, not a requirement.',
|
|
821
918
|
inputSchema: {
|
|
822
919
|
type: 'object',
|
|
823
920
|
properties: {
|
|
@@ -838,6 +935,14 @@ const imageToolDefs: McpToolDefinition[] = [
|
|
|
838
935
|
image: { type: 'string', description: 'Optional reference image for image-to-image/edit: a media path (e.g. "media/xxx.png"), an "artifact:<id>" ref from a previous generate_image result, or an https URL. Local/artifact refs are converted to short-lived URLs automatically. ONLY pass this to a model whose supports_image_ref is true (check action="list"); for any other model, describe the reference image in the prompt instead of sending it here. Do not pass both "image" and "images".' },
|
|
839
936
|
images: { type: 'array', items: { type: 'string' }, description: 'Optional multiple reference images (max 5; media paths, artifact:<id> refs, or https URLs — same supports_image_ref rule as "image"). Order is preserved.' },
|
|
840
937
|
style: { type: 'string', description: 'Optional native style parameter (e.g. "vivid") — only for models whose supports_style_ref is true.' },
|
|
938
|
+
continue_from: {
|
|
939
|
+
type: 'string',
|
|
940
|
+
description:
|
|
941
|
+
'Optional artifact id (no "artifact:" prefix) of an image YOU generated earlier in THIS session, ' +
|
|
942
|
+
'also present in this call\'s "image"/"images", made with the SAME model as this call, with no OTHER ' +
|
|
943
|
+
'model generating any image since — see "CONTINUING AN EDIT" above for the full rule. Omit unless all ' +
|
|
944
|
+
'conditions clearly hold.',
|
|
945
|
+
},
|
|
841
946
|
task_id: { type: 'string', description: 'Job id to poll (required for action="status").' },
|
|
842
947
|
},
|
|
843
948
|
required: [],
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bun tests for the knowledge-archive read path (planning-64 K1).
|
|
3
|
+
*
|
|
4
|
+
* Runs under Bun (`bun test`) because the reader uses `bun:sqlite` — the Node
|
|
5
|
+
* jest suite cannot load this module. The fixture DB is built with bun:sqlite
|
|
6
|
+
* using the SAME schema src/ writes under node:sqlite (external-content FTS5 +
|
|
7
|
+
* provenance/recall), so this exercises the real query path end to end.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { test, expect } from 'bun:test';
|
|
11
|
+
import { Database } from 'bun:sqlite';
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as os from 'os';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
|
|
16
|
+
import { searchArchive, getExcerpt, isMemoryScopedPath, archiveDbPath, sharedDbPathFromEnv, mergeHits } from './archive-reader';
|
|
17
|
+
|
|
18
|
+
// Build a temp agent dir with a kb.sqlite carrying the given chunks.
|
|
19
|
+
function mkAgentWithArchive(
|
|
20
|
+
chunks: Array<{ id: string; path: string; start: number; end: number; text: string; origin?: string; importance?: number }>,
|
|
21
|
+
): { agentDir: string; workspaceDir: string; dbPath: string } {
|
|
22
|
+
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kb1-'));
|
|
23
|
+
const workspaceDir = path.join(agentDir, 'workspace');
|
|
24
|
+
fs.mkdirSync(workspaceDir, { recursive: true });
|
|
25
|
+
const dbPath = archiveDbPath(workspaceDir); // agentDir/kb.sqlite
|
|
26
|
+
|
|
27
|
+
const db = new Database(dbPath);
|
|
28
|
+
db.exec('PRAGMA foreign_keys=ON');
|
|
29
|
+
db.exec(`
|
|
30
|
+
CREATE TABLE kb_chunks (rowid_id INTEGER PRIMARY KEY AUTOINCREMENT, id TEXT UNIQUE, path TEXT,
|
|
31
|
+
start_line INTEGER, end_line INTEGER, text TEXT, updated_at INTEGER);
|
|
32
|
+
CREATE TABLE kb_chunk_recall (chunk_id TEXT PRIMARY KEY, importance INTEGER, triggers TEXT, project_key TEXT,
|
|
33
|
+
FOREIGN KEY (chunk_id) REFERENCES kb_chunks(id) ON DELETE CASCADE);
|
|
34
|
+
CREATE TABLE kb_chunk_provenance (chunk_id TEXT PRIMARY KEY, origin_class TEXT, session_kind TEXT,
|
|
35
|
+
observed_at INTEGER, supersedes_key TEXT, FOREIGN KEY (chunk_id) REFERENCES kb_chunks(id) ON DELETE CASCADE);
|
|
36
|
+
CREATE VIRTUAL TABLE kb_chunks_fts USING fts5(text, content='kb_chunks', content_rowid='rowid_id');
|
|
37
|
+
CREATE TRIGGER kb_chunks_ai AFTER INSERT ON kb_chunks BEGIN
|
|
38
|
+
INSERT INTO kb_chunks_fts(rowid, text) VALUES (new.rowid_id, new.text);
|
|
39
|
+
END;
|
|
40
|
+
`);
|
|
41
|
+
const ins = db.prepare('INSERT INTO kb_chunks(id,path,start_line,end_line,text,updated_at) VALUES(?,?,?,?,?,?)');
|
|
42
|
+
const insR = db.prepare('INSERT INTO kb_chunk_recall(chunk_id,importance) VALUES(?,?)');
|
|
43
|
+
const insP = db.prepare('INSERT INTO kb_chunk_provenance(chunk_id,origin_class,session_kind,observed_at) VALUES(?,?,?,?)');
|
|
44
|
+
for (const c of chunks) {
|
|
45
|
+
ins.run(c.id, c.path, c.start, c.end, c.text, 1);
|
|
46
|
+
insR.run(c.id, c.importance ?? null);
|
|
47
|
+
insP.run(c.id, c.origin ?? 'agent', 'unknown', 1);
|
|
48
|
+
}
|
|
49
|
+
db.close();
|
|
50
|
+
return { agentDir, workspaceDir, dbPath };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
test('isMemoryScopedPath: accepts memory-scoped, rejects traversal/abs/out-of-scope', () => {
|
|
54
|
+
expect(isMemoryScopedPath('MEMORY.md')).toBe(true);
|
|
55
|
+
expect(isMemoryScopedPath('USER.md')).toBe(true);
|
|
56
|
+
expect(isMemoryScopedPath('memory/foo.md')).toBe(true);
|
|
57
|
+
expect(isMemoryScopedPath('memory/sub/bar.md')).toBe(true);
|
|
58
|
+
expect(isMemoryScopedPath('../secret.md')).toBe(false);
|
|
59
|
+
// The discriminating case: ends in .md AND starts with memory/, so ONLY the
|
|
60
|
+
// ".." guard rejects it — proves that guard is load-bearing.
|
|
61
|
+
expect(isMemoryScopedPath('memory/../secret.md')).toBe(false);
|
|
62
|
+
expect(isMemoryScopedPath('memory/../../etc/passwd')).toBe(false);
|
|
63
|
+
expect(isMemoryScopedPath('/etc/passwd')).toBe(false);
|
|
64
|
+
expect(isMemoryScopedPath('notes/random.md')).toBe(false);
|
|
65
|
+
expect(isMemoryScopedPath('AGENTS.md')).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('searchArchive: FTS match returns ranked hit with provenance + importance', () => {
|
|
69
|
+
const { workspaceDir, agentDir } = mkAgentWithArchive([
|
|
70
|
+
{ id: 'memory/getpod.md#1-1', path: 'memory/getpod.md', start: 1, end: 1, text: 'Getpod ingress returns 401 before the gateway', origin: 'agent', importance: 7 },
|
|
71
|
+
{ id: 'memory/apps.md#1-1', path: 'memory/apps.md', start: 1, end: 1, text: 'apps router mounts under gateway-router', origin: 'agent' },
|
|
72
|
+
]);
|
|
73
|
+
try {
|
|
74
|
+
const hits = searchArchive(archiveDbPath(workspaceDir), 'ingress 401', 6);
|
|
75
|
+
expect(hits.length).toBe(1);
|
|
76
|
+
expect(hits[0].path).toBe('memory/getpod.md');
|
|
77
|
+
expect(hits[0].originClass).toBe('agent');
|
|
78
|
+
expect(hits[0].importance).toBe(7);
|
|
79
|
+
expect(hits[0].startLine).toBe(1);
|
|
80
|
+
expect(typeof hits[0].score).toBe('number');
|
|
81
|
+
} finally {
|
|
82
|
+
fs.rmSync(agentDir, { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Regression (#351): a kb.sqlite built before the planning-66 `entry_hash`
|
|
87
|
+
// column (exactly what `mkAgentWithArchive` produces) must not crash the read
|
|
88
|
+
// path with `no such column: entry_hash`. searchArchive heals the column in
|
|
89
|
+
// place and returns hits with entryHash=null. Proven to throw on pre-fix code.
|
|
90
|
+
test('searchArchive: DB missing entry_hash column does not throw and heals it', () => {
|
|
91
|
+
const { workspaceDir, agentDir } = mkAgentWithArchive([
|
|
92
|
+
{ id: 'memory/x.md#1-1', path: 'memory/x.md', start: 1, end: 1, text: 'ingress 401 token mismatch', origin: 'agent' },
|
|
93
|
+
]);
|
|
94
|
+
const dbPath = archiveDbPath(workspaceDir);
|
|
95
|
+
const cols = (dbPath: string): string[] => {
|
|
96
|
+
const d = new Database(dbPath, { readonly: true });
|
|
97
|
+
try {
|
|
98
|
+
return (d.query(`PRAGMA table_info(kb_chunks)`).all() as Array<{ name: string }>).map((c) => c.name);
|
|
99
|
+
} finally {
|
|
100
|
+
d.close();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
try {
|
|
104
|
+
// Precondition: the pre-#346 fixture genuinely lacks the column.
|
|
105
|
+
expect(cols(dbPath)).not.toContain('entry_hash');
|
|
106
|
+
|
|
107
|
+
let hits: ReturnType<typeof searchArchive> = [];
|
|
108
|
+
expect(() => {
|
|
109
|
+
hits = searchArchive(dbPath, 'ingress 401', 6);
|
|
110
|
+
}).not.toThrow();
|
|
111
|
+
expect(hits.length).toBe(1);
|
|
112
|
+
expect(hits[0].path).toBe('memory/x.md');
|
|
113
|
+
expect(hits[0].entryHash).toBeNull();
|
|
114
|
+
|
|
115
|
+
// The read path healed the DB in place so subsequent writers/readers are safe.
|
|
116
|
+
expect(cols(dbPath)).toContain('entry_hash');
|
|
117
|
+
} finally {
|
|
118
|
+
fs.rmSync(agentDir, { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('searchArchive: missing DB → [], empty/punctuation query → []', () => {
|
|
123
|
+
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kb1-'));
|
|
124
|
+
const workspaceDir = path.join(agentDir, 'workspace');
|
|
125
|
+
fs.mkdirSync(workspaceDir, { recursive: true });
|
|
126
|
+
try {
|
|
127
|
+
expect(searchArchive(archiveDbPath(workspaceDir), 'anything', 6)).toEqual([]); // no kb.sqlite
|
|
128
|
+
const { workspaceDir: ws2, agentDir: dir2 } = mkAgentWithArchive([
|
|
129
|
+
{ id: 'm#1-1', path: 'memory/a.md', start: 1, end: 1, text: 'hello world' },
|
|
130
|
+
]);
|
|
131
|
+
expect(searchArchive(archiveDbPath(ws2), ' !!! ', 6)).toEqual([]); // no usable tokens
|
|
132
|
+
fs.rmSync(dir2, { recursive: true, force: true });
|
|
133
|
+
} finally {
|
|
134
|
+
fs.rmSync(agentDir, { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('getExcerpt: bounded line range; out-of-scope/missing → null', () => {
|
|
139
|
+
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kb1-'));
|
|
140
|
+
const workspaceDir = path.join(agentDir, 'workspace');
|
|
141
|
+
fs.mkdirSync(path.join(workspaceDir, 'memory'), { recursive: true });
|
|
142
|
+
fs.writeFileSync(path.join(workspaceDir, 'memory/note.md'), 'l1\nl2\nl3\nl4\nl5');
|
|
143
|
+
try {
|
|
144
|
+
const ex = getExcerpt(workspaceDir, 'memory/note.md', 2, 2);
|
|
145
|
+
expect(ex).not.toBeNull();
|
|
146
|
+
expect(ex!.text).toBe('l2\nl3');
|
|
147
|
+
expect(ex!.from).toBe(2);
|
|
148
|
+
expect(ex!.to).toBe(3);
|
|
149
|
+
expect(ex!.totalLines).toBe(5);
|
|
150
|
+
expect(ex!.truncated).toBe(true);
|
|
151
|
+
|
|
152
|
+
// Whole file → not truncated.
|
|
153
|
+
expect(getExcerpt(workspaceDir, 'memory/note.md', 1, 100)!.truncated).toBe(false);
|
|
154
|
+
// Out-of-scope path and missing file → null.
|
|
155
|
+
expect(getExcerpt(workspaceDir, '../secret.md', 1, 5)).toBeNull();
|
|
156
|
+
expect(getExcerpt(workspaceDir, 'memory/missing.md', 1, 5)).toBeNull();
|
|
157
|
+
} finally {
|
|
158
|
+
fs.rmSync(agentDir, { recursive: true, force: true });
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('sharedDbPathFromEnv: null when unset, <dir>/kb.sqlite when set (K3 corpus:shared)', () => {
|
|
163
|
+
const prev = process.env.GATEWAY_SHARED_KB_DIR;
|
|
164
|
+
try {
|
|
165
|
+
delete process.env.GATEWAY_SHARED_KB_DIR;
|
|
166
|
+
expect(sharedDbPathFromEnv()).toBeNull();
|
|
167
|
+
process.env.GATEWAY_SHARED_KB_DIR = ' ';
|
|
168
|
+
expect(sharedDbPathFromEnv()).toBeNull(); // blank → disabled
|
|
169
|
+
process.env.GATEWAY_SHARED_KB_DIR = '/tmp/shared/kb/global';
|
|
170
|
+
expect(sharedDbPathFromEnv()).toBe('/tmp/shared/kb/global/kb.sqlite');
|
|
171
|
+
} finally {
|
|
172
|
+
if (prev === undefined) delete process.env.GATEWAY_SHARED_KB_DIR;
|
|
173
|
+
else process.env.GATEWAY_SHARED_KB_DIR = prev;
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('mergeHits: round-robin interleave (bm25 not comparable across corpora), capped', () => {
|
|
178
|
+
const mk = (path: string, score: number) => ({
|
|
179
|
+
path, startLine: 1, endLine: 1, score, snippet: path, originClass: 'agent', importance: null,
|
|
180
|
+
});
|
|
181
|
+
// Each list is already best-first within its own corpus. A cross-corpus numeric
|
|
182
|
+
// sort would mix incomparable bm25 scales; interleave a,c,b instead (personal
|
|
183
|
+
// first each round), capped at the limit.
|
|
184
|
+
const merged = mergeHits([mk('a', -0.9), mk('b', -0.1)], [mk('c', -0.5), mk('d', -0.05)], 3);
|
|
185
|
+
expect(merged.map((h) => h.path)).toEqual(['a', 'c', 'b']);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('mergeHits: interleave preserves per-corpus order even when scales differ', () => {
|
|
189
|
+
const mk = (path: string, score: number) => ({
|
|
190
|
+
path, startLine: 1, endLine: 1, score, snippet: path, originClass: 'agent', importance: null,
|
|
191
|
+
});
|
|
192
|
+
// Shared scores numerically "better" (more negative) must NOT starve the personal
|
|
193
|
+
// corpus — round-robin gives p1, s1, p2, s2.
|
|
194
|
+
const merged = mergeHits([mk('p1', -0.2), mk('p2', -0.05)], [mk('s1', -9), mk('s2', -8)], 4);
|
|
195
|
+
expect(merged.map((h) => h.path)).toEqual(['p1', 's1', 'p2', 's2']);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('getExcerpt: a symlink inside memory/ escaping the workspace is refused', () => {
|
|
199
|
+
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kb1-'));
|
|
200
|
+
const workspaceDir = path.join(agentDir, 'workspace');
|
|
201
|
+
fs.mkdirSync(path.join(workspaceDir, 'memory'), { recursive: true });
|
|
202
|
+
// A secret target OUTSIDE the workspace, and a memory/ symlink pointing at it.
|
|
203
|
+
const secret = path.join(agentDir, 'outside-secret.md');
|
|
204
|
+
fs.writeFileSync(secret, 'TOPSECRET outside the workspace');
|
|
205
|
+
fs.symlinkSync(secret, path.join(workspaceDir, 'memory', 'evil.md'));
|
|
206
|
+
try {
|
|
207
|
+
// Passes the string scope check (memory/*.md) but realpath escapes the ws → null.
|
|
208
|
+
expect(getExcerpt(workspaceDir, 'memory/evil.md', 1, 5)).toBeNull();
|
|
209
|
+
} finally {
|
|
210
|
+
fs.rmSync(agentDir, { recursive: true, force: true });
|
|
211
|
+
}
|
|
212
|
+
});
|