@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.
Files changed (223) hide show
  1. package/README.md +149 -20
  2. package/config.template.json +59 -1
  3. package/dist/agent/dreaming/accept.d.ts +43 -0
  4. package/dist/agent/dreaming/accept.d.ts.map +1 -0
  5. package/dist/agent/dreaming/accept.js +230 -0
  6. package/dist/agent/dreaming/accept.js.map +1 -0
  7. package/dist/agent/dreaming/applier.d.ts +77 -0
  8. package/dist/agent/dreaming/applier.d.ts.map +1 -0
  9. package/dist/agent/dreaming/applier.js +406 -0
  10. package/dist/agent/dreaming/applier.js.map +1 -0
  11. package/dist/agent/dreaming/audit.d.ts +27 -0
  12. package/dist/agent/dreaming/audit.d.ts.map +1 -0
  13. package/dist/agent/dreaming/audit.js +100 -0
  14. package/dist/agent/dreaming/audit.js.map +1 -0
  15. package/dist/agent/dreaming/compactor.d.ts +66 -0
  16. package/dist/agent/dreaming/compactor.d.ts.map +1 -0
  17. package/dist/agent/dreaming/compactor.js +306 -0
  18. package/dist/agent/dreaming/compactor.js.map +1 -0
  19. package/dist/agent/dreaming/config.d.ts +19 -0
  20. package/dist/agent/dreaming/config.d.ts.map +1 -0
  21. package/dist/agent/dreaming/config.js +107 -0
  22. package/dist/agent/dreaming/config.js.map +1 -0
  23. package/dist/agent/dreaming/episodic.d.ts +42 -0
  24. package/dist/agent/dreaming/episodic.d.ts.map +1 -0
  25. package/dist/agent/dreaming/episodic.js +116 -0
  26. package/dist/agent/dreaming/episodic.js.map +1 -0
  27. package/dist/agent/dreaming/gather.d.ts +31 -0
  28. package/dist/agent/dreaming/gather.d.ts.map +1 -0
  29. package/dist/agent/dreaming/gather.js +55 -0
  30. package/dist/agent/dreaming/gather.js.map +1 -0
  31. package/dist/agent/dreaming/index.d.ts +87 -0
  32. package/dist/agent/dreaming/index.d.ts.map +1 -0
  33. package/dist/agent/dreaming/index.js +377 -0
  34. package/dist/agent/dreaming/index.js.map +1 -0
  35. package/dist/agent/dreaming/migrate-cli.d.ts +17 -0
  36. package/dist/agent/dreaming/migrate-cli.d.ts.map +1 -0
  37. package/dist/agent/dreaming/migrate-cli.js +42 -0
  38. package/dist/agent/dreaming/migrate-cli.js.map +1 -0
  39. package/dist/agent/dreaming/migrate.d.ts +72 -0
  40. package/dist/agent/dreaming/migrate.d.ts.map +1 -0
  41. package/dist/agent/dreaming/migrate.js +374 -0
  42. package/dist/agent/dreaming/migrate.js.map +1 -0
  43. package/dist/agent/dreaming/report.d.ts +39 -0
  44. package/dist/agent/dreaming/report.d.ts.map +1 -0
  45. package/dist/agent/dreaming/report.js +121 -0
  46. package/dist/agent/dreaming/report.js.map +1 -0
  47. package/dist/agent/dreaming/reviewer.d.ts +43 -0
  48. package/dist/agent/dreaming/reviewer.d.ts.map +1 -0
  49. package/dist/agent/dreaming/reviewer.js +249 -0
  50. package/dist/agent/dreaming/reviewer.js.map +1 -0
  51. package/dist/agent/dreaming/staleness.d.ts +68 -0
  52. package/dist/agent/dreaming/staleness.d.ts.map +1 -0
  53. package/dist/agent/dreaming/staleness.js +281 -0
  54. package/dist/agent/dreaming/staleness.js.map +1 -0
  55. package/dist/agent/dreaming/types.d.ts +130 -0
  56. package/dist/agent/dreaming/types.d.ts.map +1 -0
  57. package/dist/agent/dreaming/types.js +10 -0
  58. package/dist/agent/dreaming/types.js.map +1 -0
  59. package/dist/agent/knowledge/archive-db.d.ts +114 -0
  60. package/dist/agent/knowledge/archive-db.d.ts.map +1 -0
  61. package/dist/agent/knowledge/archive-db.js +474 -0
  62. package/dist/agent/knowledge/archive-db.js.map +1 -0
  63. package/dist/agent/knowledge/chunk.d.ts +21 -0
  64. package/dist/agent/knowledge/chunk.d.ts.map +1 -0
  65. package/dist/agent/knowledge/chunk.js +67 -0
  66. package/dist/agent/knowledge/chunk.js.map +1 -0
  67. package/dist/agent/knowledge/config.d.ts +20 -0
  68. package/dist/agent/knowledge/config.d.ts.map +1 -0
  69. package/dist/agent/knowledge/config.js +143 -0
  70. package/dist/agent/knowledge/config.js.map +1 -0
  71. package/dist/agent/knowledge/graph-demo.d.ts +25 -0
  72. package/dist/agent/knowledge/graph-demo.d.ts.map +1 -0
  73. package/dist/agent/knowledge/graph-demo.js +132 -0
  74. package/dist/agent/knowledge/graph-demo.js.map +1 -0
  75. package/dist/agent/knowledge/index.d.ts +24 -0
  76. package/dist/agent/knowledge/index.d.ts.map +1 -0
  77. package/dist/agent/knowledge/index.js +50 -0
  78. package/dist/agent/knowledge/index.js.map +1 -0
  79. package/dist/agent/knowledge/indexer.d.ts +34 -0
  80. package/dist/agent/knowledge/indexer.d.ts.map +1 -0
  81. package/dist/agent/knowledge/indexer.js +271 -0
  82. package/dist/agent/knowledge/indexer.js.map +1 -0
  83. package/dist/agent/knowledge/lifecycle.d.ts +91 -0
  84. package/dist/agent/knowledge/lifecycle.d.ts.map +1 -0
  85. package/dist/agent/knowledge/lifecycle.js +205 -0
  86. package/dist/agent/knowledge/lifecycle.js.map +1 -0
  87. package/dist/agent/knowledge/provenance.d.ts +15 -0
  88. package/dist/agent/knowledge/provenance.d.ts.map +1 -0
  89. package/dist/agent/knowledge/provenance.js +37 -0
  90. package/dist/agent/knowledge/provenance.js.map +1 -0
  91. package/dist/agent/knowledge/reindex-cli.d.ts +13 -0
  92. package/dist/agent/knowledge/reindex-cli.d.ts.map +1 -0
  93. package/dist/agent/knowledge/reindex-cli.js +53 -0
  94. package/dist/agent/knowledge/reindex-cli.js.map +1 -0
  95. package/dist/agent/knowledge/reindex-spawn.d.ts +30 -0
  96. package/dist/agent/knowledge/reindex-spawn.d.ts.map +1 -0
  97. package/dist/agent/knowledge/reindex-spawn.js +97 -0
  98. package/dist/agent/knowledge/reindex-spawn.js.map +1 -0
  99. package/dist/agent/knowledge/shared-promote.d.ts +18 -0
  100. package/dist/agent/knowledge/shared-promote.d.ts.map +1 -0
  101. package/dist/agent/knowledge/shared-promote.js +29 -0
  102. package/dist/agent/knowledge/shared-promote.js.map +1 -0
  103. package/dist/agent/knowledge/shared-writer.d.ts +24 -0
  104. package/dist/agent/knowledge/shared-writer.d.ts.map +1 -0
  105. package/dist/agent/knowledge/shared-writer.js +97 -0
  106. package/dist/agent/knowledge/shared-writer.js.map +1 -0
  107. package/dist/agent/knowledge/types.d.ts +83 -0
  108. package/dist/agent/knowledge/types.d.ts.map +1 -0
  109. package/dist/agent/knowledge/types.js +12 -0
  110. package/dist/agent/knowledge/types.js.map +1 -0
  111. package/dist/agent/knowledge/wiki.d.ts +107 -0
  112. package/dist/agent/knowledge/wiki.d.ts.map +1 -0
  113. package/dist/agent/knowledge/wiki.js +415 -0
  114. package/dist/agent/knowledge/wiki.js.map +1 -0
  115. package/dist/agent/memory-metrics.d.ts +48 -0
  116. package/dist/agent/memory-metrics.d.ts.map +1 -0
  117. package/dist/agent/memory-metrics.js +156 -0
  118. package/dist/agent/memory-metrics.js.map +1 -0
  119. package/dist/agent/runner.d.ts +24 -1
  120. package/dist/agent/runner.d.ts.map +1 -1
  121. package/dist/agent/runner.js +82 -6
  122. package/dist/agent/runner.js.map +1 -1
  123. package/dist/agent/skill-learning/config.d.ts +18 -0
  124. package/dist/agent/skill-learning/config.d.ts.map +1 -0
  125. package/dist/agent/skill-learning/config.js +71 -0
  126. package/dist/agent/skill-learning/config.js.map +1 -0
  127. package/dist/agent/skill-learning/curator.d.ts +45 -0
  128. package/dist/agent/skill-learning/curator.d.ts.map +1 -0
  129. package/dist/agent/skill-learning/curator.js +171 -0
  130. package/dist/agent/skill-learning/curator.js.map +1 -0
  131. package/dist/agent/skill-learning/index.d.ts +79 -0
  132. package/dist/agent/skill-learning/index.d.ts.map +1 -0
  133. package/dist/agent/skill-learning/index.js +308 -0
  134. package/dist/agent/skill-learning/index.js.map +1 -0
  135. package/dist/agent/skill-learning/metrics.d.ts +13 -0
  136. package/dist/agent/skill-learning/metrics.d.ts.map +1 -0
  137. package/dist/agent/skill-learning/metrics.js +109 -0
  138. package/dist/agent/skill-learning/metrics.js.map +1 -0
  139. package/dist/agent/skill-learning/notifier.d.ts +111 -0
  140. package/dist/agent/skill-learning/notifier.d.ts.map +1 -0
  141. package/dist/agent/skill-learning/notifier.js +398 -0
  142. package/dist/agent/skill-learning/notifier.js.map +1 -0
  143. package/dist/agent/skill-learning/reviewer.d.ts +61 -0
  144. package/dist/agent/skill-learning/reviewer.d.ts.map +1 -0
  145. package/dist/agent/skill-learning/reviewer.js +271 -0
  146. package/dist/agent/skill-learning/reviewer.js.map +1 -0
  147. package/dist/agent/skill-learning/telemetry.d.ts +22 -0
  148. package/dist/agent/skill-learning/telemetry.d.ts.map +1 -0
  149. package/dist/agent/skill-learning/telemetry.js +90 -0
  150. package/dist/agent/skill-learning/telemetry.js.map +1 -0
  151. package/dist/agent/skill-learning/trigger.d.ts +23 -0
  152. package/dist/agent/skill-learning/trigger.d.ts.map +1 -0
  153. package/dist/agent/skill-learning/trigger.js +44 -0
  154. package/dist/agent/skill-learning/trigger.js.map +1 -0
  155. package/dist/agent/skill-learning/types.d.ts +165 -0
  156. package/dist/agent/skill-learning/types.d.ts.map +1 -0
  157. package/dist/agent/skill-learning/types.js +11 -0
  158. package/dist/agent/skill-learning/types.js.map +1 -0
  159. package/dist/agent/skill-learning/writer.d.ts +37 -0
  160. package/dist/agent/skill-learning/writer.d.ts.map +1 -0
  161. package/dist/agent/skill-learning/writer.js +223 -0
  162. package/dist/agent/skill-learning/writer.js.map +1 -0
  163. package/dist/agent/workspace-loader.d.ts +83 -6
  164. package/dist/agent/workspace-loader.d.ts.map +1 -1
  165. package/dist/agent/workspace-loader.js +207 -14
  166. package/dist/agent/workspace-loader.js.map +1 -1
  167. package/dist/api/gateway-router.d.ts +2 -0
  168. package/dist/api/gateway-router.d.ts.map +1 -1
  169. package/dist/api/gateway-router.js +293 -6
  170. package/dist/api/gateway-router.js.map +1 -1
  171. package/dist/api/share-router.d.ts.map +1 -1
  172. package/dist/api/share-router.js +27 -1
  173. package/dist/api/share-router.js.map +1 -1
  174. package/dist/api/skills-router.d.ts +2 -2
  175. package/dist/api/skills-router.d.ts.map +1 -1
  176. package/dist/api/skills-router.js +61 -1
  177. package/dist/api/skills-router.js.map +1 -1
  178. package/dist/apps/agent-manager.d.ts +7 -0
  179. package/dist/apps/agent-manager.d.ts.map +1 -1
  180. package/dist/apps/agent-manager.js +13 -1
  181. package/dist/apps/agent-manager.js.map +1 -1
  182. package/dist/config/migrator.d.ts +2 -0
  183. package/dist/config/migrator.d.ts.map +1 -1
  184. package/dist/config/migrator.js +72 -1
  185. package/dist/config/migrator.js.map +1 -1
  186. package/dist/history/cleanup.d.ts +7 -0
  187. package/dist/history/cleanup.d.ts.map +1 -1
  188. package/dist/history/cleanup.js +27 -0
  189. package/dist/history/cleanup.js.map +1 -1
  190. package/dist/history/db.d.ts +65 -0
  191. package/dist/history/db.d.ts.map +1 -1
  192. package/dist/history/db.js +261 -0
  193. package/dist/history/db.js.map +1 -1
  194. package/dist/index.js +122 -20
  195. package/dist/index.js.map +1 -1
  196. package/dist/session/process.d.ts +10 -0
  197. package/dist/session/process.d.ts.map +1 -1
  198. package/dist/session/process.js +86 -1
  199. package/dist/session/process.js.map +1 -1
  200. package/dist/share/share-store.d.ts +11 -0
  201. package/dist/share/share-store.d.ts.map +1 -1
  202. package/dist/share/share-store.js +3 -1
  203. package/dist/share/share-store.js.map +1 -1
  204. package/dist/skills/loader.d.ts +8 -0
  205. package/dist/skills/loader.d.ts.map +1 -1
  206. package/dist/skills/loader.js +29 -1
  207. package/dist/skills/loader.js.map +1 -1
  208. package/dist/types.d.ts +116 -0
  209. package/dist/types.d.ts.map +1 -1
  210. package/dist/ui/web-ui.d.ts.map +1 -1
  211. package/dist/ui/web-ui.js +1126 -3
  212. package/dist/ui/web-ui.js.map +1 -1
  213. package/mcp/package.json +2 -1
  214. package/mcp/server.ts +2 -0
  215. package/mcp/tools/image/module.ts +114 -9
  216. package/mcp/tools/memory/archive-reader.test.ts +212 -0
  217. package/mcp/tools/memory/archive-reader.ts +253 -0
  218. package/mcp/tools/memory/module.ts +152 -0
  219. package/mcp/tools/memory/retrieval-recorder.ts +66 -0
  220. package/mcp/tools/shared/share-client.ts +20 -1
  221. package/mcp/tools/skills/module.ts +28 -0
  222. package/package.json +1 -1
  223. package/resource/claude_gateway.svg +1038 -112
@@ -1 +1 @@
1
- {"version":3,"file":"web-ui.js","sourceRoot":"","sources":["../../src/ui/web-ui.ts"],"names":[],"mappings":";;AAIA,sDAq4BC;AAQD,8CAiFC;AAl+BD;;;GAGG;AACH,SAAgB,qBAAqB;IACnC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAm4BD,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"}
1
+ {"version":3,"file":"web-ui.js","sourceRoot":"","sources":["../../src/ui/web-ui.ts"],"names":[],"mappings":"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"}
package/mcp/package.json CHANGED
@@ -4,7 +4,8 @@
4
4
  "type": "module",
5
5
  "bin": "./server.ts",
6
6
  "scripts": {
7
- "start": "bun install --no-summary && bun server.ts"
7
+ "start": "bun install --no-summary && bun server.ts",
8
+ "test": "bun test"
8
9
  },
9
10
  "dependencies": {
10
11
  "@line/bot-sdk": "^11.0.2",
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 normalized = await this.normalizeRefs(rawImage ? [rawImage] : rawImages!);
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<{ urls: string[]; mintedShareIds: string[] } | { error: McpToolResult }> {
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 = entries.map((e) => (e.kind === 'url' ? e.url : minted[next++]!.url!));
451
- return { urls, mintedShareIds: minted.map((m) => m.share_id) };
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
- const registered = await registerArtifacts(files, { provider, model: modelName, taskId, prompt });
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
+ });