@vellumai/assistant 0.8.11 → 0.8.12-staging.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.
Files changed (244) hide show
  1. package/ARCHITECTURE.md +15 -17
  2. package/README.md +0 -6
  3. package/bun.lock +6 -122
  4. package/node_modules/@vellumai/gateway-client/bun.lock +1 -0
  5. package/node_modules/@vellumai/gateway-client/package.json +3 -1
  6. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -1
  7. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +87 -0
  8. package/node_modules/@vellumai/gateway-client/src/index.ts +3 -5
  9. package/openapi.yaml +633 -4
  10. package/package.json +1 -3
  11. package/src/__tests__/adaptive-thinking-repair.test.ts +185 -0
  12. package/src/__tests__/agent-loop-compaction-events.test.ts +7 -6
  13. package/src/__tests__/anthropic-provider.test.ts +129 -0
  14. package/src/__tests__/background-workers-disk-pressure.test.ts +4 -1
  15. package/src/__tests__/btw-routes.test.ts +7 -34
  16. package/src/__tests__/checker.test.ts +6 -12
  17. package/src/__tests__/config-loader-backfill.test.ts +4 -2
  18. package/src/__tests__/config-loader-quarantine-notice.test.ts +167 -0
  19. package/src/__tests__/config-watcher.test.ts +2 -2
  20. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  21. package/src/__tests__/conversation-error.test.ts +2 -6
  22. package/src/__tests__/conversation-history-web-search.test.ts +8 -0
  23. package/src/__tests__/conversation-title-service.test.ts +2 -1
  24. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  25. package/src/__tests__/disk-pressure-tools.test.ts +1 -1
  26. package/src/__tests__/exploration-drift-hook.test.ts +692 -0
  27. package/src/__tests__/filing-service.test.ts +8 -3
  28. package/src/__tests__/guardian-action-store.test.ts +0 -167
  29. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +1 -1
  30. package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -1
  31. package/src/__tests__/heartbeat-service.test.ts +5 -2
  32. package/src/__tests__/identity-intro-cache.test.ts +12 -5
  33. package/src/__tests__/identity-routes.test.ts +16 -57
  34. package/src/__tests__/injector-chain.test.ts +8 -3
  35. package/src/__tests__/injector-config-quarantine-notice.test.ts +115 -0
  36. package/src/__tests__/llm-catalog-parity.test.ts +16 -0
  37. package/src/__tests__/llm-usage-store.test.ts +11 -0
  38. package/src/__tests__/log-export-workspace.test.ts +468 -3
  39. package/src/__tests__/memory-v2-static-injector.test.ts +22 -0
  40. package/src/__tests__/model-intents.test.ts +1 -1
  41. package/src/__tests__/oauth-cli.test.ts +19 -8
  42. package/src/__tests__/openai-provider.test.ts +34 -0
  43. package/src/__tests__/prechat-onboarding-contract.test.ts +0 -1
  44. package/src/__tests__/recurrence-engine.test.ts +45 -0
  45. package/src/__tests__/schedule-routes.test.ts +34 -0
  46. package/src/__tests__/scheduler-disk-pressure.test.ts +1 -1
  47. package/src/__tests__/script-proxy-conversation-manager.test.ts +10 -5
  48. package/src/__tests__/secret-fixtures.ts +20 -0
  49. package/src/__tests__/skill-tool-factory.test.ts +49 -0
  50. package/src/__tests__/subagent-role-registry.test.ts +24 -1
  51. package/src/__tests__/subagent-tools.test.ts +1 -0
  52. package/src/__tests__/system-prompt.test.ts +109 -11
  53. package/src/__tests__/tool-approval-handler.test.ts +85 -0
  54. package/src/__tests__/tool-audit-listener.test.ts +86 -0
  55. package/src/__tests__/tool-error-hook.test.ts +1 -0
  56. package/src/__tests__/tool-result-spool.test.ts +337 -0
  57. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
  58. package/src/__tests__/validate-input.test.ts +95 -1
  59. package/src/__tests__/workspace-migration-098-remove-stale-updates-bulletin-file.test.ts +65 -0
  60. package/src/__tests__/workspace-migration-099-disable-cache-one-shot-callsites.test.ts +139 -0
  61. package/src/__tests__/workspace-migration-100-upgrade-quality-profile-to-fable-5.test.ts +174 -0
  62. package/src/__tests__/workspace-migration-101-upgrade-balanced-economy-to-minimax-m3.test.ts +162 -0
  63. package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +45 -95
  64. package/src/acp/__tests__/agent-process.test.ts +315 -2
  65. package/src/acp/__tests__/prepare-agent-env.test.ts +79 -5
  66. package/src/acp/agent-process.ts +163 -34
  67. package/src/acp/prepare-agent-env.ts +55 -15
  68. package/src/agent/loop.ts +81 -24
  69. package/src/api/events/usage-progress.ts +28 -0
  70. package/src/api/index.ts +6 -0
  71. package/src/background-wake/wake-intent-hooks.test.ts +2 -0
  72. package/src/bundler/app-bundler.ts +25 -42
  73. package/src/bundler/app-compiler.ts +8 -0
  74. package/src/calls/call-controller.ts +1 -1
  75. package/src/cli/commands/plugins.ts +248 -15
  76. package/src/cli/lib/__tests__/inspect-plugin.test.ts +318 -0
  77. package/src/cli/lib/__tests__/install-from-github.test.ts +16 -9
  78. package/src/cli/lib/__tests__/plugin-artifact.test.ts +183 -0
  79. package/src/cli/lib/__tests__/plugin-details.test.ts +158 -0
  80. package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +245 -0
  81. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +307 -0
  82. package/src/cli/lib/inspect-plugin.ts +252 -0
  83. package/src/cli/lib/install-from-github.ts +214 -21
  84. package/src/cli/lib/list-installed-plugins.ts +17 -6
  85. package/src/cli/lib/plugin-artifact.ts +103 -0
  86. package/src/cli/lib/plugin-details.ts +18 -1
  87. package/src/cli/lib/plugin-fingerprint.ts +197 -0
  88. package/src/cli/lib/upgrade-plugin.ts +225 -0
  89. package/src/config/bundled-skills/subagent/SKILL.md +2 -0
  90. package/src/config/bundled-skills/subagent/TOOLS.json +8 -2
  91. package/src/config/call-site-defaults.ts +13 -2
  92. package/src/config/feature-flag-registry.json +8 -16
  93. package/src/config/loader.ts +52 -59
  94. package/src/config/schema.ts +0 -2
  95. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  96. package/src/config/schemas/__tests__/memory-v3.test.ts +10 -0
  97. package/src/config/schemas/llm.ts +10 -0
  98. package/src/config/schemas/memory-v2.ts +13 -0
  99. package/src/config/schemas/memory-v3.ts +92 -0
  100. package/src/config/seed-inference-profiles.ts +4 -8
  101. package/src/context/post-turn-tool-result-truncation.ts +32 -18
  102. package/src/context/tool-result-spool.ts +104 -0
  103. package/src/credential-execution/feature-gates.ts +0 -1
  104. package/src/daemon/conversation-agent-loop-handlers.ts +41 -16
  105. package/src/daemon/conversation-error.ts +6 -15
  106. package/src/daemon/conversation.ts +9 -0
  107. package/src/daemon/disk-pressure-policy.ts +0 -1
  108. package/src/daemon/lifecycle.ts +1 -20
  109. package/src/daemon/message-types/conversations.ts +2 -15
  110. package/src/daemon/trust-context.ts +1 -1
  111. package/src/events/tool-audit-listener.ts +40 -9
  112. package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
  113. package/src/home/__tests__/home-content-refresh.test.ts +114 -0
  114. package/src/home/__tests__/suggested-prompts.test.ts +86 -5
  115. package/src/home/home-content-refresh.ts +43 -31
  116. package/src/home/home-greeting-cache.ts +8 -1
  117. package/src/home/home-greeting.ts +13 -9
  118. package/src/home/suggested-prompts.ts +77 -24
  119. package/src/ipc/routes/trust-rules.test.ts +66 -72
  120. package/src/media/image-credentials.ts +2 -2
  121. package/src/memory/__tests__/compaction-log-store-clickhouse.test.ts +432 -0
  122. package/src/memory/{compaction-log-writer-clickhouse.ts → compaction-log-store-clickhouse.ts} +264 -55
  123. package/src/memory/conversation-attention-store.ts +1 -0
  124. package/src/memory/conversation-bootstrap.ts +18 -9
  125. package/src/memory/conversation-crud.ts +12 -2
  126. package/src/memory/conversation-title-service.ts +53 -9
  127. package/src/memory/delivery-channels.ts +0 -69
  128. package/src/memory/graph/extraction-job.ts +0 -15
  129. package/src/memory/guardian-action-store.ts +1 -376
  130. package/src/memory/llm-usage-store.ts +5 -1
  131. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +2 -2
  132. package/src/memory/v2/__tests__/consolidation-job.test.ts +183 -2
  133. package/src/memory/v2/__tests__/injection.test.ts +70 -0
  134. package/src/memory/v2/__tests__/static-context.test.ts +12 -0
  135. package/src/memory/v2/consolidation-job.ts +93 -9
  136. package/src/memory/v2/injection.ts +53 -0
  137. package/src/memory/v2/prompts/consolidation.ts +1 -0
  138. package/src/memory/v2/static-context.ts +13 -1
  139. package/src/memory/v2/sweep-job.ts +1 -1
  140. package/src/memory/v2/types.ts +5 -0
  141. package/src/plugin-api/types.ts +7 -0
  142. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +300 -0
  143. package/src/plugins/defaults/exploration-drift/package.json +15 -0
  144. package/src/plugins/defaults/index.ts +25 -0
  145. package/src/plugins/defaults/memory-retrieval/injectors.ts +132 -4
  146. package/src/plugins/defaults/memory-v3-shadow/__tests__/card.test.ts +92 -0
  147. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +2 -1
  148. package/src/plugins/defaults/memory-v3-shadow/__tests__/fresh-set.test.ts +52 -0
  149. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  150. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +2 -1
  151. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +136 -5
  152. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +17 -0
  153. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +6 -0
  154. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +5 -1
  155. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +68 -4
  156. package/src/plugins/defaults/memory-v3-shadow/card.ts +49 -5
  157. package/src/plugins/defaults/memory-v3-shadow/fresh-set.ts +59 -0
  158. package/src/plugins/defaults/memory-v3-shadow/injector.ts +4 -2
  159. package/src/plugins/defaults/memory-v3-shadow/learned-edges.test.ts +169 -0
  160. package/src/plugins/defaults/memory-v3-shadow/learned-edges.ts +178 -0
  161. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +115 -26
  162. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -9
  163. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +144 -22
  164. package/src/plugins/defaults/memory-v3-shadow/types.ts +24 -6
  165. package/src/plugins/defaults/title-generate/hooks/stop.ts +13 -0
  166. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +16 -0
  167. package/src/prompts/cache-boundary.ts +17 -0
  168. package/src/prompts/sections.ts +50 -17
  169. package/src/prompts/system-prompt.ts +12 -4
  170. package/src/prompts/templates/system-sections.ts +22 -0
  171. package/src/providers/__tests__/unparseable-tool-args.test.ts +53 -0
  172. package/src/providers/anthropic/client.ts +74 -28
  173. package/src/providers/gemini/client.ts +5 -1
  174. package/src/providers/minimax/client.ts +9 -0
  175. package/src/providers/model-catalog.ts +28 -0
  176. package/src/providers/model-intents.ts +3 -3
  177. package/src/providers/openai/chat-completions-provider.ts +4 -2
  178. package/src/providers/openai/responses-provider.ts +7 -2
  179. package/src/providers/retry.ts +8 -0
  180. package/src/providers/types.ts +11 -0
  181. package/src/providers/unparseable-tool-args.ts +56 -0
  182. package/src/runtime/AGENTS.md +6 -0
  183. package/src/runtime/__tests__/agent-wake.test.ts +2 -2
  184. package/src/runtime/agent-wake.ts +5 -5
  185. package/src/runtime/background-job-runner.ts +2 -2
  186. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +150 -3
  187. package/src/runtime/migrations/vbundle-import-analyzer.ts +29 -6
  188. package/src/runtime/migrations/vbundle-import-policy.ts +23 -0
  189. package/src/runtime/migrations/vbundle-importer.ts +9 -4
  190. package/src/runtime/migrations/vbundle-streaming-importer.ts +8 -3
  191. package/src/runtime/pre-first-message-gate.ts +1 -1
  192. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +241 -0
  193. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +132 -0
  194. package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +97 -185
  195. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +17 -0
  196. package/src/runtime/routes/__tests__/plugins-routes.test.ts +348 -0
  197. package/src/runtime/routes/__tests__/task-routes.test.ts +3 -3
  198. package/src/runtime/routes/btw-routes.ts +0 -14
  199. package/src/runtime/routes/conversation-compaction-routes.ts +86 -19
  200. package/src/runtime/routes/conversation-list-routes.ts +77 -5
  201. package/src/runtime/routes/conversation-management-routes.ts +54 -0
  202. package/src/runtime/routes/conversation-query-routes.ts +79 -4
  203. package/src/runtime/routes/gateway-log-routes.ts +14 -64
  204. package/src/runtime/routes/home-feed-routes.ts +10 -0
  205. package/src/runtime/routes/identity-intro-cache.ts +1 -1
  206. package/src/runtime/routes/identity-routes.ts +76 -20
  207. package/src/runtime/routes/inbound-message-handler.ts +0 -36
  208. package/src/runtime/routes/log-export-routes.ts +143 -96
  209. package/src/runtime/routes/plugins-routes.ts +380 -0
  210. package/src/runtime/routes/redact-staged-export.ts +259 -0
  211. package/src/runtime/routes/schedule-routes.ts +19 -2
  212. package/src/runtime/routes/trust-rules-routes.ts +14 -67
  213. package/src/schedule/recurrence-engine.ts +34 -0
  214. package/src/schedule/scheduler.ts +1 -0
  215. package/src/security/redact-json.ts +61 -0
  216. package/src/skills/validate-input.ts +41 -1
  217. package/src/subagent/types.ts +26 -1
  218. package/src/telemetry/types.ts +15 -1
  219. package/src/telemetry/usage-telemetry-reporter.test.ts +6 -1
  220. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  221. package/src/tools/apps/executors.ts +1 -1
  222. package/src/tools/skills/skill-tool-factory.ts +19 -8
  223. package/src/tools/tool-approval-handler.ts +31 -0
  224. package/src/usage/types.ts +8 -1
  225. package/src/util/platform.ts +16 -0
  226. package/src/watcher/engine.ts +1 -0
  227. package/src/workspace/adaptive-thinking-repair.ts +113 -0
  228. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +70 -67
  229. package/src/workspace/migrations/098-remove-stale-updates-bulletin-file.ts +31 -0
  230. package/src/workspace/migrations/099-disable-cache-one-shot-callsites.ts +81 -0
  231. package/src/workspace/migrations/100-upgrade-quality-profile-to-fable-5.ts +86 -0
  232. package/src/workspace/migrations/101-upgrade-balanced-economy-to-minimax-m3.ts +70 -0
  233. package/src/workspace/migrations/registry.ts +8 -0
  234. package/src/__tests__/config-loader-quarantine-bulletin.test.ts +0 -202
  235. package/src/__tests__/conversation-starters-cadence.test.ts +0 -161
  236. package/src/__tests__/guardian-action-followup-executor.test.ts +0 -322
  237. package/src/__tests__/guardian-action-followup-store.test.ts +0 -373
  238. package/src/__tests__/guardian-action-late-reply.test.ts +0 -1083
  239. package/src/__tests__/update-bulletin-job.test.ts +0 -292
  240. package/src/config/schemas/updates.ts +0 -14
  241. package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +0 -227
  242. package/src/memory/conversation-starters-cadence.ts +0 -78
  243. package/src/prompts/update-bulletin-job.ts +0 -180
  244. package/src/runtime/guardian-action-followup-executor.ts +0 -306
@@ -150,9 +150,6 @@ describe("DefaultPathResolver prompts/USER.md translation", () => {
150
150
  expect(resolver.resolve("prompts/SOUL.md")).toBe(
151
151
  join(WORKSPACE_ROOT, "SOUL.md"),
152
152
  );
153
- expect(resolver.resolve("prompts/UPDATES.md")).toBe(
154
- join(WORKSPACE_ROOT, "UPDATES.md"),
155
- );
156
153
  });
157
154
 
158
155
  test("skips unknown prompt filenames regardless of guardian state", () => {
@@ -230,6 +227,71 @@ describe("analyzeImport for legacy prompts/USER.md", () => {
230
227
  const userMd = report.files.find((f) => f.path === "prompts/USER.md");
231
228
  expect(userMd!.action).toBe("skip");
232
229
  });
230
+
231
+ test("retired workspace/UPDATES.md (current format) is a non-blocking skip", () => {
232
+ const resolver = new DefaultPathResolver(
233
+ WORKSPACE_ROOT,
234
+ undefined,
235
+ () => null,
236
+ );
237
+
238
+ const { manifest } = buildVBundle({
239
+ files: [
240
+ {
241
+ path: "data/db/assistant.db",
242
+ data: new Uint8Array(),
243
+ },
244
+ {
245
+ path: "workspace/UPDATES.md",
246
+ data: new TextEncoder().encode("## Old release notes\n"),
247
+ },
248
+ ],
249
+ ...defaultV1Options(),
250
+ });
251
+
252
+ const report = analyzeImport({ manifest, pathResolver: resolver });
253
+
254
+ expect(report.can_import).toBe(true);
255
+ expect(report.conflicts).toHaveLength(0);
256
+ const updatesMd = report.files.find(
257
+ (f) => f.path === "workspace/UPDATES.md",
258
+ );
259
+ expect(updatesMd).toBeDefined();
260
+ expect(updatesMd!.action).toBe("skip");
261
+ });
262
+
263
+ test("retired prompts/UPDATES.md in a legacy bundle is a non-blocking skip", () => {
264
+ const resolver = new DefaultPathResolver(
265
+ WORKSPACE_ROOT,
266
+ undefined,
267
+ () => null,
268
+ );
269
+
270
+ const { manifest } = buildVBundle({
271
+ files: [
272
+ {
273
+ path: "data/db/assistant.db",
274
+ data: new Uint8Array(),
275
+ },
276
+ {
277
+ path: "prompts/UPDATES.md",
278
+ data: new TextEncoder().encode("## Old release notes\n"),
279
+ },
280
+ ],
281
+ ...defaultV1Options(),
282
+ });
283
+
284
+ const report = analyzeImport({ manifest, pathResolver: resolver });
285
+
286
+ // Bundles exported while the update-bulletin feature existed must stay
287
+ // restorable — the retired file is dropped, never written back, and
288
+ // must not register an UNKNOWN_ARCHIVE_PATH conflict.
289
+ expect(report.can_import).toBe(true);
290
+ expect(report.conflicts).toHaveLength(0);
291
+ const updatesMd = report.files.find((f) => f.path === "prompts/UPDATES.md");
292
+ expect(updatesMd).toBeDefined();
293
+ expect(updatesMd!.action).toBe("skip");
294
+ });
233
295
  });
234
296
 
235
297
  // ---------------------------------------------------------------------------
@@ -358,6 +420,91 @@ describe("commitImport for legacy prompts/USER.md", () => {
358
420
  expect(existsSync(join(USERS_DIR, "USER.md"))).toBe(false);
359
421
  });
360
422
 
423
+ test("retired workspace/UPDATES.md (current format) is never written back", () => {
424
+ const resolver = new DefaultPathResolver(
425
+ WORKSPACE_ROOT,
426
+ undefined,
427
+ () => null,
428
+ );
429
+
430
+ const { archive } = buildVBundle({
431
+ files: [
432
+ {
433
+ path: "data/db/assistant.db",
434
+ data: new Uint8Array(),
435
+ },
436
+ {
437
+ path: "workspace/UPDATES.md",
438
+ data: new TextEncoder().encode("## Old release notes\n"),
439
+ },
440
+ ],
441
+ ...defaultV1Options(),
442
+ });
443
+
444
+ const result = commitImport({
445
+ archiveData: archive,
446
+ pathResolver: resolver,
447
+ });
448
+
449
+ expect(result.ok).toBe(true);
450
+ if (result.ok) {
451
+ expect(result.report.success).toBe(true);
452
+ expect(
453
+ result.report.files.find((f) => f.path === "workspace/UPDATES.md")
454
+ ?.action,
455
+ ).toBe("skipped");
456
+ expect(result.report.warnings.some((w) => w.includes("UPDATES.md"))).toBe(
457
+ false,
458
+ );
459
+ }
460
+
461
+ // The retired bulletin file must not be resurrected in the workspace.
462
+ expect(existsSync(join(WORKSPACE_ROOT, "UPDATES.md"))).toBe(false);
463
+ });
464
+
465
+ test("retired prompts/UPDATES.md skips silently — no warning, nothing written", () => {
466
+ const resolver = new DefaultPathResolver(
467
+ WORKSPACE_ROOT,
468
+ undefined,
469
+ () => null,
470
+ );
471
+
472
+ const { archive } = buildVBundle({
473
+ files: [
474
+ {
475
+ path: "data/db/assistant.db",
476
+ data: new Uint8Array(),
477
+ },
478
+ {
479
+ path: "prompts/UPDATES.md",
480
+ data: new TextEncoder().encode("## Old release notes\n"),
481
+ },
482
+ ],
483
+ ...defaultV1Options(),
484
+ });
485
+
486
+ const result = commitImport({
487
+ archiveData: archive,
488
+ pathResolver: resolver,
489
+ });
490
+
491
+ expect(result.ok).toBe(true);
492
+ if (result.ok) {
493
+ expect(result.report.success).toBe(true);
494
+ expect(
495
+ result.report.files.find((f) => f.path === "prompts/UPDATES.md")
496
+ ?.action,
497
+ ).toBe("skipped");
498
+ // Silent skip: the commit report must agree with preflight, which
499
+ // raises neither a conflict nor a warning for retired paths.
500
+ expect(
501
+ result.report.warnings.some((w) => w.includes("prompts/UPDATES.md")),
502
+ ).toBe(false);
503
+ }
504
+
505
+ expect(existsSync(join(WORKSPACE_ROOT, "UPDATES.md"))).toBe(false);
506
+ });
507
+
361
508
  test("does NOT overwrite a customized users/<slug>.md", () => {
362
509
  const slug = "captain.md";
363
510
  const guardianPath = join(USERS_DIR, slug);
@@ -19,6 +19,7 @@ import { join, resolve } from "node:path";
19
19
 
20
20
  import { resolveGuardianPersonaPath } from "../../prompts/persona-resolver.js";
21
21
  import { getLogger } from "../../util/logger.js";
22
+ import { isRetiredArchivePath } from "./vbundle-import-policy.js";
22
23
  import type { ManifestType } from "./vbundle-validator.js";
23
24
 
24
25
  const log = getLogger("vbundle-import-analyzer");
@@ -30,12 +31,7 @@ const log = getLogger("vbundle-import-analyzer");
30
31
  * on import, its content is translated to `users/<slug>.md` at the
31
32
  * current guardian's location (see `DefaultPathResolver.resolve`).
32
33
  */
33
- const ALLOWED_PROMPT_FILENAMES = new Set([
34
- "IDENTITY.md",
35
- "SOUL.md",
36
- "USER.md",
37
- "UPDATES.md",
38
- ]);
34
+ const ALLOWED_PROMPT_FILENAMES = new Set(["IDENTITY.md", "SOUL.md", "USER.md"]);
39
35
 
40
36
  /** Archive path for the legacy guardian user persona file. */
41
37
  const LEGACY_USER_MD_ARCHIVE_PATH = "prompts/USER.md";
@@ -123,6 +119,14 @@ export class DefaultPathResolver implements PathResolver {
123
119
  return null;
124
120
  }
125
121
 
122
+ // Retired-feature files (both legacy and workspace-format spellings)
123
+ // must never resolve to a disk target — resolving would write them
124
+ // back into the workspace on import. The analyzer and both importers
125
+ // turn the resulting null into a silent non-blocking skip.
126
+ if (isRetiredArchivePath(archivePath)) {
127
+ return null;
128
+ }
129
+
126
130
  // New format: workspace/ prefix — maps directly into the workspace dir
127
131
  if (archivePath.startsWith("workspace/") && this.workspaceDir) {
128
132
  const relPath = archivePath.slice("workspace/".length);
@@ -288,6 +292,25 @@ export function analyzeImport(
288
292
  continue;
289
293
  }
290
294
 
295
+ // Retired-feature file from a legacy bundle: nothing consumes it
296
+ // anymore, so drop it silently instead of blocking the restore with
297
+ // an UNKNOWN_ARCHIVE_PATH conflict.
298
+ if (isRetiredArchivePath(fileEntry.path)) {
299
+ log.info(
300
+ { path: fileEntry.path },
301
+ "Retired archive path in legacy bundle — will be skipped on import",
302
+ );
303
+ files.push({
304
+ path: fileEntry.path,
305
+ action: "skip",
306
+ bundle_size: fileEntry.size_bytes,
307
+ bundle_sha256: fileEntry.sha256,
308
+ current_size: null,
309
+ current_sha256: null,
310
+ });
311
+ continue;
312
+ }
313
+
291
314
  // Unknown archive path — would have nowhere to write
292
315
  conflicts.push({
293
316
  code: "UNKNOWN_ARCHIVE_PATH",
@@ -34,6 +34,29 @@ export function isLegacyPersonaArchivePath(archivePath: string): boolean {
34
34
  return archivePath === LEGACY_USER_MD_ARCHIVE_PATH;
35
35
  }
36
36
 
37
+ /**
38
+ * Archive paths for retired features. Bundles exported while those features
39
+ * existed still contain these entries; preflight and both importers must
40
+ * treat them as silent non-blocking skips (no conflict, no warning) so old
41
+ * backups stay restorable without noise, and the files are never written
42
+ * back to the workspace.
43
+ *
44
+ * `UPDATES.md` carried the retired update-bulletin release notes. Legacy
45
+ * bundles archived it as `prompts/UPDATES.md`; current-format bundles walk
46
+ * the workspace dir, so it appears as `workspace/UPDATES.md`. Both spellings
47
+ * must be retired — `DefaultPathResolver.resolve()` short-circuits them to
48
+ * `null` before any prefix handling so the workspace-format entry cannot
49
+ * resolve normally and recreate the file on import.
50
+ */
51
+ export const RETIRED_ARCHIVE_PATHS: ReadonlySet<string> = new Set([
52
+ "prompts/UPDATES.md",
53
+ "workspace/UPDATES.md",
54
+ ]);
55
+
56
+ export function isRetiredArchivePath(archivePath: string): boolean {
57
+ return RETIRED_ARCHIVE_PATHS.has(archivePath);
58
+ }
59
+
37
60
  export function isConfigArchivePath(archivePath: string): boolean {
38
61
  return CONFIG_ARCHIVE_PATHS.has(archivePath);
39
62
  }
@@ -322,7 +322,10 @@ export function commitImport(options: ImportCommitOptions): ImportCommitResult {
322
322
  const diskPath = pathResolver.resolve(fileEntry.path);
323
323
 
324
324
  if (!diskPath) {
325
- // Unknown archive path — skip it
325
+ // Unknown archive path — skip it. Retired-feature paths (see
326
+ // `policy.RETIRED_ARCHIVE_PATHS`) are expected in legacy bundles and
327
+ // skip silently so the import report matches preflight; anything else
328
+ // unresolvable earns a warning.
326
329
  importedFiles.push({
327
330
  path: fileEntry.path,
328
331
  disk_path: "",
@@ -331,9 +334,11 @@ export function commitImport(options: ImportCommitOptions): ImportCommitResult {
331
334
  sha256: fileEntry.sha256,
332
335
  backup_path: null,
333
336
  });
334
- warnings.push(
335
- `Skipped "${fileEntry.path}": no known disk target for this archive path`,
336
- );
337
+ if (!policy.isRetiredArchivePath(fileEntry.path)) {
338
+ warnings.push(
339
+ `Skipped "${fileEntry.path}": no known disk target for this archive path`,
340
+ );
341
+ }
337
342
  continue;
338
343
  }
339
344
 
@@ -695,9 +695,14 @@ export async function streamCommitImport(
695
695
  sha256: expectedEntry.sha256,
696
696
  backup_path: null,
697
697
  });
698
- warnings.push(
699
- `Skipped "${archivePath}": no known disk target for this archive path`,
700
- );
698
+ // Retired-feature paths (see `policy.RETIRED_ARCHIVE_PATHS`) are
699
+ // expected in legacy bundles and skip silently so the import report
700
+ // matches preflight; anything else unresolvable earns a warning.
701
+ if (!policy.isRetiredArchivePath(archivePath)) {
702
+ warnings.push(
703
+ `Skipped "${archivePath}": no known disk target for this archive path`,
704
+ );
705
+ }
701
706
  seen.add(archivePath);
702
707
  onProgress?.({
703
708
  archivePath,
@@ -25,7 +25,7 @@
25
25
  * interaction resumes, so re-querying on miss is the correct behavior.)
26
26
  *
27
27
  * Callers should treat the gate as advisory + defense-in-depth: prefer to
28
- * skip at the service level (heartbeat, update-bulletin, etc.) so no run
28
+ * skip at the service level (heartbeat, etc.) so no run
29
29
  * row / conversation row is created at all, and rely on the gate inside
30
30
  * `runBackgroundJob` as the universal backstop.
31
31
  */
@@ -34,6 +34,7 @@ mock.module("../../../config/loader.js", () => ({
34
34
  // Source + conversation-crud module mocks
35
35
  // ---------------------------------------------------------------------
36
36
 
37
+ import type { CompactionLogEvent } from "../../../memory/compaction-log-store-clickhouse.js";
37
38
  import type { LogRow } from "../../../memory/llm-request-log-store.js";
38
39
 
39
40
  interface FakeSourceState {
@@ -41,6 +42,9 @@ interface FakeSourceState {
41
42
  selectedCall: LogRow | null;
42
43
  turnBounds: { startTime: number; endTime: number } | null;
43
44
  compactionLogs: LogRow[];
45
+ /** null = compactionLogs destination not configured (legacy-only). */
46
+ compactionStoreEvents: CompactionLogEvent[] | null;
47
+ compactionStoreError: Error | null;
44
48
  }
45
49
 
46
50
  const state: FakeSourceState = {
@@ -48,6 +52,8 @@ const state: FakeSourceState = {
48
52
  selectedCall: null,
49
53
  turnBounds: null,
50
54
  compactionLogs: [],
55
+ compactionStoreEvents: null,
56
+ compactionStoreError: null,
51
57
  };
52
58
 
53
59
  // Records the inputs the handler passed to its collaborators so tests
@@ -63,8 +69,34 @@ const sourceCalls = {
63
69
  afterCreatedAt: number | null;
64
70
  beforeCreatedAt: number;
65
71
  }>,
72
+ getEventsBetweenArgs: [] as Array<{
73
+ conversationId: string;
74
+ afterStartedAt: number | null;
75
+ beforeStartedAt: number;
76
+ }>,
66
77
  };
67
78
 
79
+ mock.module("../../../memory/compaction-log-store-clickhouse.js", () => ({
80
+ getCompactionLogStore: () =>
81
+ state.compactionStoreEvents === null && state.compactionStoreError === null
82
+ ? null
83
+ : {
84
+ getEventsBetween: async (
85
+ conversationId: string,
86
+ afterStartedAt: number | null,
87
+ beforeStartedAt: number,
88
+ ) => {
89
+ sourceCalls.getEventsBetweenArgs.push({
90
+ conversationId,
91
+ afterStartedAt,
92
+ beforeStartedAt,
93
+ });
94
+ if (state.compactionStoreError) throw state.compactionStoreError;
95
+ return state.compactionStoreEvents ?? [];
96
+ },
97
+ },
98
+ }));
99
+
68
100
  mock.module("../../../memory/conversation-crud.js", () => ({
69
101
  getConversation: (id: string) =>
70
102
  state.conversation && state.conversation.id === id
@@ -105,6 +137,7 @@ mock.module("../../../memory/llm-request-log-source.js", () => ({
105
137
 
106
138
  // Imported AFTER the mocks so the handler picks up the fakes.
107
139
  import {
140
+ projectCompactionLogEventToTrailEvent,
108
141
  projectLogRowToCompactionTrailEvent,
109
142
  ROUTES,
110
143
  } from "../conversation-compaction-routes.js";
@@ -132,14 +165,52 @@ function fakeLogRow(overrides: Partial<LogRow> = {}): LogRow {
132
165
  };
133
166
  }
134
167
 
168
+ function fakeCompactionLogEvent(
169
+ overrides: Partial<CompactionLogEvent> = {},
170
+ ): CompactionLogEvent {
171
+ return {
172
+ compactionId: "comp-1",
173
+ requestId: "req-1",
174
+ trigger: "budget",
175
+ startedAt: 3000,
176
+ finishedAt: 3500,
177
+ durationMs: 500,
178
+ preMessageCount: 12,
179
+ resultMessageCount: 4,
180
+ compacted: true,
181
+ previousEstimatedInputTokens: 900,
182
+ estimatedInputTokens: 300,
183
+ maxInputTokens: 1000,
184
+ thresholdTokens: 850,
185
+ compactedMessages: 10,
186
+ compactedPersistedMessages: 8,
187
+ preservedTailMessages: 2,
188
+ summaryCalls: 1,
189
+ summaryInputTokens: 880,
190
+ summaryOutputTokens: 120,
191
+ summaryModel: "test-model",
192
+ summaryFailed: false,
193
+ reason: "auto",
194
+ exhausted: false,
195
+ injectionMode: null,
196
+ autoCompressApplied: false,
197
+ summaryText: "summary text",
198
+ completed: true,
199
+ ...overrides,
200
+ };
201
+ }
202
+
135
203
  beforeEach(() => {
136
204
  state.conversation = null;
137
205
  state.selectedCall = null;
138
206
  state.turnBounds = null;
139
207
  state.compactionLogs = [];
208
+ state.compactionStoreEvents = null;
209
+ state.compactionStoreError = null;
140
210
  sourceCalls.getRequestLogByIdArgs.length = 0;
141
211
  sourceCalls.getTurnTimeBoundsArgs.length = 0;
142
212
  sourceCalls.getCompactionLogsBetweenArgs.length = 0;
213
+ sourceCalls.getEventsBetweenArgs.length = 0;
143
214
  });
144
215
 
145
216
  // ---------------------------------------------------------------------
@@ -353,10 +424,180 @@ describe("handleGetCompactionTrail — happy path", () => {
353
424
  });
354
425
  });
355
426
 
427
+ // ---------------------------------------------------------------------
428
+ // Handler — compaction-log store path
429
+ // ---------------------------------------------------------------------
430
+
431
+ describe("handleGetCompactionTrail — compaction log store", () => {
432
+ test("serves the trail from the store and skips the legacy projection", async () => {
433
+ state.conversation = { id: "conv-1" };
434
+ state.selectedCall = fakeLogRow({
435
+ id: "call-1",
436
+ conversationId: "conv-1",
437
+ createdAt: 5000,
438
+ });
439
+ state.turnBounds = { startTime: 2000, endTime: 9000 };
440
+ state.compactionStoreEvents = [fakeCompactionLogEvent()];
441
+
442
+ const result = await handler({
443
+ pathParams: { id: "conv-1" },
444
+ queryParams: { callId: "call-1" },
445
+ });
446
+
447
+ // Same ±1ms-shifted turn window as the legacy path.
448
+ expect(sourceCalls.getEventsBetweenArgs).toEqual([
449
+ {
450
+ conversationId: "conv-1",
451
+ afterStartedAt: 1999,
452
+ beforeStartedAt: 9001,
453
+ },
454
+ ]);
455
+ expect(sourceCalls.getCompactionLogsBetweenArgs).toEqual([]);
456
+ expect(result.events).toEqual([
457
+ {
458
+ id: "comp-1",
459
+ createdAt: 3000,
460
+ model: "test-model",
461
+ provider: null,
462
+ inputTokens: 880,
463
+ outputTokens: 120,
464
+ durationMs: 500,
465
+ responsePreview: "summary text",
466
+ requestMessageCount: 12,
467
+ stopReason: "auto",
468
+ estimatedCostUsd: null,
469
+ },
470
+ ]);
471
+ });
472
+
473
+ test("falls back to the legacy projection when the store has no rows for the window", async () => {
474
+ state.conversation = { id: "conv-1" };
475
+ state.selectedCall = fakeLogRow({
476
+ id: "call-1",
477
+ conversationId: "conv-1",
478
+ createdAt: 5000,
479
+ });
480
+ state.turnBounds = { startTime: 2000, endTime: 9000 };
481
+ state.compactionStoreEvents = [];
482
+ state.compactionLogs = [
483
+ fakeLogRow({ id: "compaction-legacy", conversationId: "conv-1" }),
484
+ ];
485
+
486
+ const result = await handler({
487
+ pathParams: { id: "conv-1" },
488
+ queryParams: { callId: "call-1" },
489
+ });
490
+
491
+ expect(sourceCalls.getEventsBetweenArgs).toHaveLength(1);
492
+ expect(sourceCalls.getCompactionLogsBetweenArgs).toHaveLength(1);
493
+ expect(result.events).toHaveLength(1);
494
+ expect((result.events[0] as { id: string }).id).toBe("compaction-legacy");
495
+ });
496
+
497
+ test("falls back to the legacy projection when the store read throws", async () => {
498
+ state.conversation = { id: "conv-1" };
499
+ state.selectedCall = fakeLogRow({
500
+ id: "call-1",
501
+ conversationId: "conv-1",
502
+ createdAt: 5000,
503
+ });
504
+ state.turnBounds = { startTime: 2000, endTime: 9000 };
505
+ state.compactionStoreError = new Error("clickhouse unreachable");
506
+ state.compactionLogs = [
507
+ fakeLogRow({ id: "compaction-legacy", conversationId: "conv-1" }),
508
+ ];
509
+
510
+ const result = await handler({
511
+ pathParams: { id: "conv-1" },
512
+ queryParams: { callId: "call-1" },
513
+ });
514
+
515
+ expect(sourceCalls.getCompactionLogsBetweenArgs).toHaveLength(1);
516
+ expect(result.events).toHaveLength(1);
517
+ expect((result.events[0] as { id: string }).id).toBe("compaction-legacy");
518
+ });
519
+
520
+ test("falls back to the legacy projection when any event is missing its end row", async () => {
521
+ state.conversation = { id: "conv-1" };
522
+ state.selectedCall = fakeLogRow({
523
+ id: "call-1",
524
+ conversationId: "conv-1",
525
+ createdAt: 5000,
526
+ });
527
+ state.turnBounds = { startTime: 2000, endTime: 9000 };
528
+ state.compactionStoreEvents = [
529
+ fakeCompactionLogEvent(),
530
+ fakeCompactionLogEvent({
531
+ compactionId: "comp-2",
532
+ finishedAt: null,
533
+ durationMs: null,
534
+ completed: false,
535
+ }),
536
+ ];
537
+ state.compactionLogs = [
538
+ fakeLogRow({ id: "compaction-legacy", conversationId: "conv-1" }),
539
+ ];
540
+
541
+ const result = await handler({
542
+ pathParams: { id: "conv-1" },
543
+ queryParams: { callId: "call-1" },
544
+ });
545
+
546
+ expect(sourceCalls.getEventsBetweenArgs).toHaveLength(1);
547
+ expect(sourceCalls.getCompactionLogsBetweenArgs).toHaveLength(1);
548
+ expect(result.events).toHaveLength(1);
549
+ expect((result.events[0] as { id: string }).id).toBe("compaction-legacy");
550
+ });
551
+ });
552
+
356
553
  // ---------------------------------------------------------------------
357
554
  // Projection unit tests
358
555
  // ---------------------------------------------------------------------
359
556
 
557
+ describe("projectCompactionLogEventToTrailEvent", () => {
558
+ test("maps a completed event onto the wire shape", () => {
559
+ const event = projectCompactionLogEventToTrailEvent(
560
+ fakeCompactionLogEvent(),
561
+ );
562
+ expect(event).toEqual({
563
+ id: "comp-1",
564
+ createdAt: 3000,
565
+ model: "test-model",
566
+ provider: null,
567
+ inputTokens: 880,
568
+ outputTokens: 120,
569
+ durationMs: 500,
570
+ responsePreview: "summary text",
571
+ requestMessageCount: 12,
572
+ stopReason: "auto",
573
+ estimatedCostUsd: null,
574
+ });
575
+ });
576
+
577
+ test("maps an incomplete (start-only) event with nulls for end-phase fields", () => {
578
+ const event = projectCompactionLogEventToTrailEvent(
579
+ fakeCompactionLogEvent({
580
+ completed: false,
581
+ finishedAt: null,
582
+ durationMs: null,
583
+ summaryInputTokens: null,
584
+ summaryOutputTokens: null,
585
+ summaryModel: null,
586
+ reason: null,
587
+ summaryText: null,
588
+ }),
589
+ );
590
+ expect(event.durationMs).toBeNull();
591
+ expect(event.model).toBeNull();
592
+ expect(event.inputTokens).toBeNull();
593
+ expect(event.outputTokens).toBeNull();
594
+ expect(event.responsePreview).toBeNull();
595
+ expect(event.stopReason).toBeNull();
596
+ // The start row always carries the pre-compaction message count.
597
+ expect(event.requestMessageCount).toBe(12);
598
+ });
599
+ });
600
+
360
601
  describe("projectLogRowToCompactionTrailEvent", () => {
361
602
  test("returns null for every summary-derived field when payloads are empty", () => {
362
603
  const event = projectLogRowToCompactionTrailEvent(fakeLogRow());