@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
@@ -34,6 +34,12 @@
34
34
 
35
35
  import { beforeEach, describe, expect, mock, test } from "bun:test";
36
36
 
37
+ import {
38
+ type InspectPluginDeps,
39
+ type InspectPluginOptions,
40
+ type PluginInspection,
41
+ PluginInspectNotFoundError,
42
+ } from "../../../cli/lib/inspect-plugin.js";
37
43
  import {
38
44
  type InstallPluginOptions,
39
45
  type InstallPluginResult,
@@ -59,6 +65,12 @@ import {
59
65
  type UninstallPluginOptions,
60
66
  type UninstallPluginResult,
61
67
  } from "../../../cli/lib/uninstall-plugin.js";
68
+ import {
69
+ PluginNotUpgradableError,
70
+ type PluginUpgradeResult,
71
+ type UpgradePluginDeps,
72
+ type UpgradePluginOptions,
73
+ } from "../../../cli/lib/upgrade-plugin.js";
62
74
 
63
75
  // Mutable list returned by the mocked library function. Tests reassign
64
76
  // `installedFixture` before invoking the handler.
@@ -133,6 +145,38 @@ mock.module("../../../cli/lib/plugin-details.js", () => ({
133
145
  getPluginDetails: detailsSpy,
134
146
  }));
135
147
 
148
+ // Mock inspectPlugin: the lib computes the local-vs-remote drift (covered by
149
+ // inspect-plugin.test.ts); the route only forwards the name and maps errors.
150
+ const inspectSpy = mock(
151
+ async (
152
+ _opts: InspectPluginOptions,
153
+ _deps: InspectPluginDeps,
154
+ ): Promise<PluginInspection> => {
155
+ throw new Error("inspectSpy default impl not configured");
156
+ },
157
+ );
158
+
159
+ mock.module("../../../cli/lib/inspect-plugin.js", () => ({
160
+ PluginInspectNotFoundError,
161
+ inspectPlugin: inspectSpy,
162
+ }));
163
+
164
+ // Mock upgradePlugin: the lib performs the re-pin (covered by
165
+ // upgrade-plugin.test.ts); the route projects its result and maps errors.
166
+ const upgradeSpy = mock(
167
+ async (
168
+ _opts: UpgradePluginOptions,
169
+ _deps: UpgradePluginDeps,
170
+ ): Promise<PluginUpgradeResult> => {
171
+ throw new Error("upgradeSpy default impl not configured");
172
+ },
173
+ );
174
+
175
+ mock.module("../../../cli/lib/upgrade-plugin.js", () => ({
176
+ PluginNotUpgradableError,
177
+ upgradePlugin: upgradeSpy,
178
+ }));
179
+
136
180
  import {
137
181
  BadRequestError,
138
182
  ConflictError,
@@ -154,6 +198,8 @@ const searchHandler = findHandler("plugins_search");
154
198
  const uninstallHandler = findHandler("plugins_uninstall");
155
199
  const getHandler = findHandler("plugins_get");
156
200
  const installHandler = findHandler("plugins_install");
201
+ const inspectHandler = findHandler("plugins_inspect");
202
+ const upgradeHandler = findHandler("plugins_upgrade");
157
203
 
158
204
  function invoke(args: RouteHandlerArgs = {}): {
159
205
  plugins: Array<Record<string, unknown>>;
@@ -643,6 +689,7 @@ function pluginDetails(overrides: Partial<PluginDetails> = {}): PluginDetails {
643
689
  },
644
690
  readme: overrides.readme ?? null,
645
691
  ref: overrides.ref ?? "main",
692
+ artifact: overrides.artifact ?? null,
646
693
  };
647
694
  }
648
695
 
@@ -880,3 +927,304 @@ describe("POST /v1/plugins/install", () => {
880
927
  expect((caught as Error).message).toContain("ECONNRESET");
881
928
  });
882
929
  });
930
+
931
+ function inspection(
932
+ overrides: Partial<PluginInspection> = {},
933
+ ): PluginInspection {
934
+ return {
935
+ name: overrides.name ?? "level-up",
936
+ installed: overrides.installed ?? true,
937
+ status: overrides.status ?? "update-available",
938
+ local:
939
+ overrides.local === undefined
940
+ ? {
941
+ target: "/workspace/.vellum/plugins/level-up",
942
+ commit: "60a392b0000000000000000000000000000000aa",
943
+ version: "0.1.0",
944
+ description: "Surfaces a Level Up diff card.",
945
+ installedAt: "2026-06-08T00:00:00.000Z",
946
+ source: {
947
+ kind: "github",
948
+ owner: "vellum-ai",
949
+ repo: "level-up",
950
+ ref: "60a392b0000000000000000000000000000000aa",
951
+ },
952
+ localChanges: {
953
+ modified: [],
954
+ added: [],
955
+ removed: [],
956
+ clean: true,
957
+ },
958
+ issues: [],
959
+ }
960
+ : overrides.local,
961
+ remote:
962
+ overrides.remote === undefined
963
+ ? {
964
+ repo: "vellum-ai/level-up",
965
+ path: "",
966
+ commit: "3eae1820000000000000000000000000000000bb",
967
+ description: "Surfaces a Level Up diff card.",
968
+ homepage: "https://github.com/vellum-ai/level-up",
969
+ license: "MIT",
970
+ category: null,
971
+ marketplaceRef: "main",
972
+ }
973
+ : overrides.remote,
974
+ remoteError: overrides.remoteError ?? null,
975
+ };
976
+ }
977
+
978
+ async function invokeInspect(
979
+ args: RouteHandlerArgs = {},
980
+ ): Promise<PluginInspection> {
981
+ return (await inspectHandler(args)) as PluginInspection;
982
+ }
983
+
984
+ describe("GET /v1/plugins/:name/inspect", () => {
985
+ beforeEach(() => {
986
+ inspectSpy.mockReset();
987
+ });
988
+
989
+ test("forwards the name to inspectPlugin and returns the drift verbatim", async () => {
990
+ // GIVEN inspectPlugin reports an available update for an installed plugin
991
+ const view = inspection({ status: "update-available" });
992
+ inspectSpy.mockImplementation(async () => view);
993
+
994
+ // WHEN the route handler is invoked with the path name
995
+ const result = await invokeInspect({ pathParams: { name: "level-up" } });
996
+
997
+ // THEN the inspection is returned unchanged
998
+ expect(result).toEqual(view);
999
+ // AND the name is forwarded to the lib (ref is never caller-supplied)
1000
+ expect(inspectSpy.mock.calls[0]?.[0]).toEqual({ name: "level-up" });
1001
+ });
1002
+
1003
+ test("a captured marketplace error is returned as 200 with remote-unavailable, not thrown", async () => {
1004
+ // GIVEN the catalog was unreachable but a local copy exists, so the lib
1005
+ // returns a status rather than throwing
1006
+ const view = inspection({
1007
+ status: "remote-unavailable",
1008
+ remote: null,
1009
+ remoteError: "ENOTFOUND raw.githubusercontent.com",
1010
+ });
1011
+ inspectSpy.mockImplementation(async () => view);
1012
+
1013
+ // WHEN the handler runs
1014
+ const result = await invokeInspect({ pathParams: { name: "level-up" } });
1015
+
1016
+ // THEN it resolves (does not throw) and surfaces the captured error
1017
+ expect(result.status).toBe("remote-unavailable");
1018
+ expect(result.remoteError).toContain("ENOTFOUND");
1019
+ });
1020
+
1021
+ test("InvalidPluginNameError → BadRequestError (400)", async () => {
1022
+ inspectSpy.mockImplementation(async () => {
1023
+ throw new InvalidPluginNameError("../escape");
1024
+ });
1025
+
1026
+ await expect(
1027
+ invokeInspect({ pathParams: { name: "../escape" } }),
1028
+ ).rejects.toBeInstanceOf(BadRequestError);
1029
+ });
1030
+
1031
+ test("PluginInspectNotFoundError → NotFoundError (404)", async () => {
1032
+ inspectSpy.mockImplementation(async () => {
1033
+ throw new PluginInspectNotFoundError("ghost");
1034
+ });
1035
+
1036
+ await expect(
1037
+ invokeInspect({ pathParams: { name: "ghost" } }),
1038
+ ).rejects.toBeInstanceOf(NotFoundError);
1039
+ });
1040
+
1041
+ test("unknown errors → InternalError with original message preserved", async () => {
1042
+ inspectSpy.mockImplementation(async () => {
1043
+ throw new Error("EUNEXPECTED");
1044
+ });
1045
+
1046
+ let caught: unknown;
1047
+ try {
1048
+ await invokeInspect({ pathParams: { name: "level-up" } });
1049
+ } catch (err) {
1050
+ caught = err;
1051
+ }
1052
+ expect(caught).toBeInstanceOf(InternalError);
1053
+ expect((caught as Error).message).toContain("EUNEXPECTED");
1054
+ });
1055
+ });
1056
+
1057
+ function upgradeResult(
1058
+ overrides: Partial<PluginUpgradeResult> = {},
1059
+ ): PluginUpgradeResult {
1060
+ return {
1061
+ name: overrides.name ?? "level-up",
1062
+ outcome: overrides.outcome ?? "upgraded",
1063
+ fromCommit:
1064
+ overrides.fromCommit === undefined
1065
+ ? "60a392b0000000000000000000000000000000aa"
1066
+ : overrides.fromCommit,
1067
+ toCommit: overrides.toCommit ?? "3eae1820000000000000000000000000000000bb",
1068
+ target: overrides.target ?? "/workspace/.vellum/plugins/level-up",
1069
+ fileCount: overrides.fileCount === undefined ? 12 : overrides.fileCount,
1070
+ dryRun: overrides.dryRun ?? false,
1071
+ provenanceWasUnknown: overrides.provenanceWasUnknown ?? false,
1072
+ };
1073
+ }
1074
+
1075
+ async function invokeUpgrade(args: RouteHandlerArgs = {}): Promise<{
1076
+ name: string;
1077
+ outcome: string;
1078
+ fromCommit: string | null;
1079
+ toCommit: string;
1080
+ target: string;
1081
+ fileCount: number | null;
1082
+ dryRun: boolean;
1083
+ provenanceWasUnknown: boolean;
1084
+ }> {
1085
+ return (await upgradeHandler(args)) as {
1086
+ name: string;
1087
+ outcome: string;
1088
+ fromCommit: string | null;
1089
+ toCommit: string;
1090
+ target: string;
1091
+ fileCount: number | null;
1092
+ dryRun: boolean;
1093
+ provenanceWasUnknown: boolean;
1094
+ };
1095
+ }
1096
+
1097
+ describe("POST /v1/plugins/:name/upgrade", () => {
1098
+ beforeEach(() => {
1099
+ upgradeSpy.mockReset();
1100
+ });
1101
+
1102
+ test("forwards name + dryRun and projects the upgrade result", async () => {
1103
+ // GIVEN upgradePlugin reports a successful re-pin
1104
+ upgradeSpy.mockImplementation(async () => upgradeResult());
1105
+
1106
+ // WHEN the handler runs with an explicit dryRun flag
1107
+ const result = await invokeUpgrade({
1108
+ pathParams: { name: "level-up" },
1109
+ body: { dryRun: false },
1110
+ });
1111
+
1112
+ // THEN the result is projected onto the wire shape
1113
+ expect(result).toEqual({
1114
+ name: "level-up",
1115
+ outcome: "upgraded",
1116
+ fromCommit: "60a392b0000000000000000000000000000000aa",
1117
+ toCommit: "3eae1820000000000000000000000000000000bb",
1118
+ target: "/workspace/.vellum/plugins/level-up",
1119
+ fileCount: 12,
1120
+ dryRun: false,
1121
+ provenanceWasUnknown: false,
1122
+ });
1123
+ // AND the name + dryRun are forwarded to the lib
1124
+ expect(upgradeSpy.mock.calls[0]?.[0]).toEqual({
1125
+ name: "level-up",
1126
+ dryRun: false,
1127
+ });
1128
+ });
1129
+
1130
+ test("omits dryRun (passes undefined) when the body flag is absent", async () => {
1131
+ // GIVEN a no-op upgrade where the install already matches the pin
1132
+ upgradeSpy.mockImplementation(async () =>
1133
+ upgradeResult({
1134
+ outcome: "already-up-to-date",
1135
+ toCommit: "60a392b0000000000000000000000000000000aa",
1136
+ fileCount: null,
1137
+ }),
1138
+ );
1139
+
1140
+ // WHEN invoked without a body
1141
+ const result = await invokeUpgrade({ pathParams: { name: "level-up" } });
1142
+
1143
+ // THEN the no-op outcome is surfaced
1144
+ expect(result.outcome).toBe("already-up-to-date");
1145
+ expect(result.fileCount).toBeNull();
1146
+ // AND dryRun is passed through as undefined (not coerced to false)
1147
+ expect(upgradeSpy.mock.calls[0]?.[0]).toEqual({
1148
+ name: "level-up",
1149
+ dryRun: undefined,
1150
+ });
1151
+ });
1152
+
1153
+ test("InvalidPluginNameError → BadRequestError (400)", async () => {
1154
+ upgradeSpy.mockImplementation(async () => {
1155
+ throw new InvalidPluginNameError("../escape");
1156
+ });
1157
+
1158
+ await expect(
1159
+ invokeUpgrade({ pathParams: { name: "../escape" } }),
1160
+ ).rejects.toBeInstanceOf(BadRequestError);
1161
+ });
1162
+
1163
+ test("PluginNotInstalledError → NotFoundError (404)", async () => {
1164
+ upgradeSpy.mockImplementation(async () => {
1165
+ throw new PluginNotInstalledError(
1166
+ "ghost",
1167
+ "/workspace/.vellum/plugins/ghost",
1168
+ );
1169
+ });
1170
+
1171
+ await expect(
1172
+ invokeUpgrade({ pathParams: { name: "ghost" } }),
1173
+ ).rejects.toBeInstanceOf(NotFoundError);
1174
+ });
1175
+
1176
+ test("PluginNotUpgradableError → ConflictError (409)", async () => {
1177
+ // The install exists but has no marketplace entry to advance to: a
1178
+ // well-formed request that isn't actionable in the current state.
1179
+ upgradeSpy.mockImplementation(async () => {
1180
+ throw new PluginNotUpgradableError(
1181
+ "level-up",
1182
+ "it has no marketplace entry to upgrade from",
1183
+ );
1184
+ });
1185
+
1186
+ await expect(
1187
+ invokeUpgrade({ pathParams: { name: "level-up" } }),
1188
+ ).rejects.toBeInstanceOf(ConflictError);
1189
+ });
1190
+
1191
+ test("PluginNotFoundError → NotFoundError (404)", async () => {
1192
+ upgradeSpy.mockImplementation(async () => {
1193
+ throw new PluginNotFoundError("level-up", "main", "vellum-ai/level-up");
1194
+ });
1195
+
1196
+ await expect(
1197
+ invokeUpgrade({ pathParams: { name: "level-up" } }),
1198
+ ).rejects.toBeInstanceOf(NotFoundError);
1199
+ });
1200
+
1201
+ test("PluginSourceUnavailableError → ServiceUnavailableError (503)", async () => {
1202
+ // The re-install fetch can hit a rate-limited GitHub; that is retryable,
1203
+ // so surface 503 rather than a misleading 500.
1204
+ upgradeSpy.mockImplementation(async () => {
1205
+ throw new PluginSourceUnavailableError(
1206
+ "GitHub tree listing for vellum-ai/level-up: HTTP 403",
1207
+ 403,
1208
+ );
1209
+ });
1210
+
1211
+ await expect(
1212
+ invokeUpgrade({ pathParams: { name: "level-up" } }),
1213
+ ).rejects.toBeInstanceOf(ServiceUnavailableError);
1214
+ });
1215
+
1216
+ test("unknown errors → InternalError with original message preserved", async () => {
1217
+ upgradeSpy.mockImplementation(async () => {
1218
+ throw new Error("ECONNRESET");
1219
+ });
1220
+
1221
+ let caught: unknown;
1222
+ try {
1223
+ await invokeUpgrade({ pathParams: { name: "level-up" } });
1224
+ } catch (err) {
1225
+ caught = err;
1226
+ }
1227
+ expect(caught).toBeInstanceOf(InternalError);
1228
+ expect((caught as Error).message).toContain("ECONNRESET");
1229
+ });
1230
+ });
@@ -206,6 +206,8 @@ mock.module("../../../util/platform.js", () => {
206
206
  getPlatformName: () => "linux",
207
207
  normalizeAssistantId: (id: string) => id,
208
208
  getDataDir: stub,
209
+ getConfigQuarantineNoticePath: () =>
210
+ "/mock/workspace/data/config-quarantine-notice.json",
209
211
  getEmbeddingModelsDir: stub,
210
212
  getSandboxRootDir: stub,
211
213
  getSandboxWorkingDir: stub,
@@ -414,9 +416,7 @@ describe("task_delete route", () => {
414
416
 
415
417
  test("throws Zod validation error for empty task_ids array", async () => {
416
418
  const route = findRoute("task_delete");
417
- await expect(
418
- route.handler({ body: { task_ids: [] } }),
419
- ).rejects.toThrow();
419
+ await expect(route.handler({ body: { task_ids: [] } })).rejects.toThrow();
420
420
 
421
421
  expect(mockTaskDeleteCalls).toHaveLength(0);
422
422
  });
@@ -14,7 +14,6 @@
14
14
 
15
15
  import { z } from "zod";
16
16
 
17
- import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
18
17
  import { getConfig } from "../../config/loader.js";
19
18
  import { getOrCreateConversation } from "../../daemon/conversation-store.js";
20
19
  import { readNowScratchpad } from "../../daemon/now-scratchpad.js";
@@ -34,9 +33,6 @@ const IDENTITY_INTRO_KEY = "identity-intro";
34
33
 
35
34
  /** Conversation key used by the client for empty-state greeting generation. */
36
35
  const GREETING_KEY = "greeting";
37
- const EMPTY_STATE_DYNAMIC_GREETINGS_FLAG =
38
- "empty-state-dynamic-greetings" as const;
39
- const STATIC_EMPTY_STATE_GREETING = "What are we working on?";
40
36
 
41
37
  // ---------------------------------------------------------------------------
42
38
  // SSE helpers
@@ -68,16 +64,6 @@ async function handleBtw({
68
64
 
69
65
  const trimmedContent = content.trim();
70
66
 
71
- if (
72
- conversationKey === GREETING_KEY &&
73
- !isAssistantFeatureFlagEnabled(
74
- EMPTY_STATE_DYNAMIC_GREETINGS_FLAG,
75
- getConfig(),
76
- )
77
- ) {
78
- return streamText(STATIC_EMPTY_STATE_GREETING);
79
- }
80
-
81
67
  // ----- Cached identity intro fast-path -----
82
68
  if (conversationKey === IDENTITY_INTRO_KEY) {
83
69
  const fastText = getCachedIntro()?.greetings[0];
@@ -34,30 +34,37 @@
34
34
  * which has no compaction immediately before it but had several
35
35
  * earlier in the same turn. The turn is the right unit of analysis.
36
36
  *
37
- * # Data model decision (in progress)
37
+ * # Data sources
38
38
  *
39
- * Today the trail is projected from `llm_request_logs` rows alone — no
40
- * `compaction_logs` table exists yet (#32055 remains a draft). This MVP
41
- * route exists precisely to test whether the projected shape is enough.
42
- * If real-world use surfaces UX needs that aren't in `llm_request_logs`
43
- * (most likely: per-event duration, structured before/after counts,
44
- * trigger reason), that becomes the concrete justification for the new
45
- * table. Until then, missing fields surface as `null` and the Compaction
46
- * tab renders `"Unavailable"` for them.
39
+ * When `compactionLogs.destination = "clickhouse"` is configured, the
40
+ * trail is served from the first-class compaction log: the agent loop's
41
+ * start/end event pairs written by
42
+ * `memory/compaction-log-store-clickhouse.ts`. Those rows carry real
43
+ * durations, summary-model token totals, and trigger reasons — none of
44
+ * which exist on `llm_request_logs`.
47
45
  *
48
- * In particular, `durationMs` is always `null` for now — the column
49
- * doesn't exist on `llm_request_logs` and we deliberately ship the route
50
- * without it to surface the gap in the UI.
46
+ * The legacy projection over `llm_request_logs` rows with
47
+ * `call_site = "compactionAgent"` remains as the fallback: it serves
48
+ * turns that predate the compaction log (the table is append-only from
49
+ * the moment the destination is configured), assistants that never opt
50
+ * in, and reads where the ClickHouse query fails. On the legacy path
51
+ * `durationMs` is always `null` — the column doesn't exist on
52
+ * `llm_request_logs`.
51
53
  */
52
54
 
53
55
  import { z } from "zod";
54
56
 
57
+ import {
58
+ type CompactionLogEvent,
59
+ getCompactionLogStore,
60
+ } from "../../memory/compaction-log-store-clickhouse.js";
55
61
  import {
56
62
  getConversation,
57
63
  getTurnTimeBounds,
58
64
  } from "../../memory/conversation-crud.js";
59
65
  import { getLlmRequestLogSource } from "../../memory/llm-request-log-source.js";
60
66
  import type { LogRow } from "../../memory/llm-request-log-store.js";
67
+ import { getLogger } from "../../util/logger.js";
61
68
  import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
62
69
  import { BadRequestError, NotFoundError } from "./errors.js";
63
70
  import {
@@ -66,6 +73,8 @@ import {
66
73
  } from "./llm-context-normalization.js";
67
74
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
68
75
 
76
+ const log = getLogger("conversation-compaction-routes");
77
+
69
78
  /**
70
79
  * Wire shape for a single compaction event. Mirrors the React Query
71
80
  * response type at
@@ -81,12 +90,9 @@ export interface CompactionTrailEvent {
81
90
  inputTokens: number | null;
82
91
  outputTokens: number | null;
83
92
  /**
84
- * Per-call wall-clock latency, in milliseconds. **Always `null` for
85
- * now** — `llm_request_logs` doesn't carry a duration column. Surfacing
86
- * the gap in the UI is intentional: if engineers consistently miss
87
- * having latency here, that's a concrete signal to extend the row
88
- * (either as a column on `llm_request_logs` or on a dedicated
89
- * `compaction_logs` table per the open data-model decision).
93
+ * Per-attempt wall-clock latency, in milliseconds. Populated from the
94
+ * compaction log when configured; always `null` on the legacy
95
+ * `llm_request_logs` path, which has no duration column.
90
96
  */
91
97
  durationMs: number | null;
92
98
  responsePreview: string | null;
@@ -172,6 +178,33 @@ export function projectLogRowToCompactionTrailEvent(
172
178
  };
173
179
  }
174
180
 
181
+ /**
182
+ * Project a paired compaction-log event into the trail wire shape. Token
183
+ * counts are the summarizer's own usage (what the compaction itself cost),
184
+ * matching what the legacy path derived from the compaction agent's
185
+ * request/response payloads.
186
+ *
187
+ * Exported only for unit tests; the route handler is the sole production
188
+ * caller.
189
+ */
190
+ export function projectCompactionLogEventToTrailEvent(
191
+ event: CompactionLogEvent,
192
+ ): CompactionTrailEvent {
193
+ return {
194
+ id: event.compactionId,
195
+ createdAt: event.startedAt,
196
+ model: event.summaryModel,
197
+ provider: null,
198
+ inputTokens: event.summaryInputTokens,
199
+ outputTokens: event.summaryOutputTokens,
200
+ durationMs: event.durationMs,
201
+ responsePreview: event.summaryText,
202
+ requestMessageCount: event.preMessageCount,
203
+ stopReason: event.reason,
204
+ estimatedCostUsd: null,
205
+ };
206
+ }
207
+
175
208
  // ---------------------------------------------------------------------------
176
209
  // Handler
177
210
  // ---------------------------------------------------------------------------
@@ -232,6 +265,40 @@ async function handleGetCompactionTrail({
232
265
  turnBounds !== null ? turnBounds.startTime - 1 : null;
233
266
  const beforeCreatedAt: number =
234
267
  turnBounds !== null ? turnBounds.endTime + 1 : selectedCall.createdAt;
268
+ // Prefer the first-class compaction log when the assistant has opted
269
+ // in. Zero rows means the turn predates the log (it is append-only from
270
+ // the moment the destination is configured), so fall through to the
271
+ // legacy projection rather than returning an empty trail; same for a
272
+ // failed ClickHouse read. Writes are best-effort and the start/end rows
273
+ // land independently, so an event without its end row (no `finishedAt`)
274
+ // means the end write failed or is lagging — in that case the legacy
275
+ // projection may still have the summarizer call details, so fall back
276
+ // rather than serve a trail with null model/tokens/duration.
277
+ const compactionStore = getCompactionLogStore();
278
+ if (compactionStore) {
279
+ try {
280
+ const events = await compactionStore.getEventsBetween(
281
+ conversationId,
282
+ afterCreatedAt,
283
+ beforeCreatedAt,
284
+ );
285
+ if (
286
+ events.length > 0 &&
287
+ events.every((event) => event.finishedAt !== null)
288
+ ) {
289
+ return {
290
+ conversationId,
291
+ events: events.map(projectCompactionLogEventToTrailEvent),
292
+ };
293
+ }
294
+ } catch (err) {
295
+ log.warn(
296
+ { err, conversationId, callId },
297
+ "Compaction log read failed; falling back to llm_request_logs projection",
298
+ );
299
+ }
300
+ }
301
+
235
302
  const logs = await source.getCompactionLogsBetween(
236
303
  conversationId,
237
304
  afterCreatedAt,
@@ -259,7 +326,7 @@ export const ROUTES: RouteDefinition[] = [
259
326
  },
260
327
  summary: "Get the compaction trail leading up to an LLM call",
261
328
  description:
262
- "Return the chronological list of compaction events that ran in the same agent turn as the call identified by `callId`. Turn bounds are walked from the `messages` table (real user messages — tool-result user messages are not boundaries). Projected from `llm_request_logs` rows where `call_site = \"compactionAgent\"`. When the conversation has no other messages around the selected call, every compaction strictly before the call's `createdAt` is in scope. Drives the Inspector's Compaction tab.",
329
+ 'Return the chronological list of compaction events that ran in the same agent turn as the call identified by `callId`. Turn bounds are walked from the `messages` table (real user messages — tool-result user messages are not boundaries). Served from the first-class compaction log when `compactionLogs.destination = "clickhouse"` is configured, falling back to the legacy projection over `llm_request_logs` rows where `call_site = "compactionAgent"`. When the conversation has no other messages around the selected call, every compaction strictly before the call\'s `createdAt` is in scope. Drives the Inspector\'s Compaction tab.',
263
330
  tags: ["conversations"],
264
331
  pathParams: [
265
332
  {