@vellumai/assistant 0.8.12 → 0.9.0-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 (442) hide show
  1. package/AGENTS.md +0 -14
  2. package/ARCHITECTURE.md +45 -45
  3. package/README.md +1 -1
  4. package/bun.lock +200 -154
  5. package/docs/architecture/integrations.md +3 -3
  6. package/docs/architecture/memory.md +2 -2
  7. package/docs/architecture/security.md +10 -10
  8. package/docs/runbook-trusted-contacts.md +12 -12
  9. package/docs/skills.md +6 -6
  10. package/docs/workflows-testing.md +221 -0
  11. package/docs/workflows.md +510 -0
  12. package/examples/plugins/echo/README.md +5 -5
  13. package/knip.json +2 -0
  14. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +105 -0
  15. package/node_modules/@vellumai/gateway-client/src/index.ts +12 -0
  16. package/openapi.yaml +7197 -5708
  17. package/package.json +8 -4
  18. package/scripts/generate-openapi.ts +66 -114
  19. package/src/__tests__/access-request-seed-content-blocks.test.ts +213 -0
  20. package/src/__tests__/adaptive-thinking-repair.test.ts +32 -3
  21. package/src/__tests__/agent-loop-output-hooks.test.ts +183 -0
  22. package/src/__tests__/agent-loop-regrowth-guard.test.ts +506 -0
  23. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  24. package/src/__tests__/agent-wake-override-profile.test.ts +77 -0
  25. package/src/__tests__/app-compiler.test.ts +7 -1
  26. package/src/__tests__/app-dir-path-guard.test.ts +27 -3
  27. package/src/__tests__/app-executors.test.ts +43 -0
  28. package/src/__tests__/approval-cascade.test.ts +0 -5
  29. package/src/__tests__/approval-routes-http.test.ts +91 -0
  30. package/src/__tests__/assistant-stream-state.test.ts +107 -0
  31. package/src/__tests__/browser-fill-credential.test.ts +3 -3
  32. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -1
  33. package/src/__tests__/compaction-events.test.ts +63 -7
  34. package/src/__tests__/compaction-trail-store.test.ts +74 -1
  35. package/src/__tests__/compaction.benchmark.test.ts +63 -41
  36. package/src/__tests__/compactor-low-watermark-cut.test.ts +349 -0
  37. package/src/__tests__/context-window-manager-compact-retry.test.ts +64 -0
  38. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -5
  39. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -5
  40. package/src/__tests__/conversation-history-web-search.test.ts +7 -0
  41. package/src/__tests__/conversation-process-callsite.test.ts +0 -5
  42. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -5
  43. package/src/__tests__/conversation-queue.test.ts +0 -5
  44. package/src/__tests__/conversation-slash-queue.test.ts +0 -5
  45. package/src/__tests__/conversation-slash-unknown.test.ts +0 -5
  46. package/src/__tests__/conversation-speed-override.test.ts +0 -5
  47. package/src/__tests__/conversation-surfaces-data-persist.test.ts +97 -0
  48. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  49. package/src/__tests__/conversation-usage.test.ts +2 -0
  50. package/src/__tests__/conversation-workspace-injection.test.ts +0 -5
  51. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -5
  52. package/src/__tests__/credential-broker-browser-fill.test.ts +2 -2
  53. package/src/__tests__/credential-broker-server-use.test.ts +2 -2
  54. package/src/__tests__/credential-broker.test.ts +1 -1
  55. package/src/__tests__/credential-prompt-route.test.ts +417 -0
  56. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  57. package/src/__tests__/credential-vault.test.ts +37 -0
  58. package/src/__tests__/db-schedule-syntax-migration.test.ts +24 -0
  59. package/src/__tests__/dynamic-page-surface.test.ts +219 -0
  60. package/src/__tests__/empty-state-greeting-cache.test.ts +94 -0
  61. package/src/__tests__/gateway-flag-listener.test.ts +24 -7
  62. package/src/__tests__/guardian-action-sweep.test.ts +56 -219
  63. package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
  64. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  65. package/src/__tests__/list-messages-hidden-metadata.test.ts +99 -0
  66. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +87 -1
  67. package/src/__tests__/llm-resolver.test.ts +115 -0
  68. package/src/__tests__/managed-profile-guard.test.ts +6 -5
  69. package/src/__tests__/max-tokens-continue-hook.test.ts +184 -0
  70. package/src/__tests__/media-generate-image.test.ts +20 -9
  71. package/src/__tests__/mock-gateway-ipc.ts +23 -0
  72. package/src/__tests__/model-intents.test.ts +1 -1
  73. package/src/__tests__/normalize-onboarding.test.ts +26 -0
  74. package/src/__tests__/notification-decision-strategy.test.ts +4 -2
  75. package/src/__tests__/notification-telegram-adapter.test.ts +21 -3
  76. package/src/__tests__/pending-interactions-resolved-event.test.ts +62 -0
  77. package/src/__tests__/post-turn-tool-result-truncation.test.ts +72 -18
  78. package/src/__tests__/require-fresh-approval.test.ts +425 -1
  79. package/src/__tests__/resolve-app-id.test.ts +56 -0
  80. package/src/__tests__/runtime-events-sse-parity.test.ts +2 -0
  81. package/src/__tests__/schedule-routes-workflow-validation.test.ts +408 -0
  82. package/src/__tests__/schedule-routes.test.ts +257 -4
  83. package/src/__tests__/schedule-store.test.ts +60 -0
  84. package/src/__tests__/schedule-tools.test.ts +247 -2
  85. package/src/__tests__/skill-execute-input.test.ts +85 -0
  86. package/src/__tests__/skill-secret-handling-guard.test.ts +21 -20
  87. package/src/__tests__/skills.test.ts +3 -3
  88. package/src/__tests__/slack-app-setup-skill-regression.test.ts +1 -1
  89. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  90. package/src/__tests__/subagent-tool-gate-mode.test.ts +547 -0
  91. package/src/__tests__/system-prompt.test.ts +1 -1
  92. package/src/__tests__/task-progress-nudge-hook.test.ts +372 -0
  93. package/src/__tests__/task-scheduler.test.ts +299 -0
  94. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +209 -0
  95. package/src/__tests__/tool-result-spool.test.ts +3 -1
  96. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +181 -0
  97. package/src/__tests__/workspace-migration-103-upgrade-quality-profile-to-opus-4-8.test.ts +174 -0
  98. package/src/agent/compaction-circuit.ts +11 -0
  99. package/src/agent/loop.ts +181 -12
  100. package/src/api/constants/call-sites.ts +12 -0
  101. package/src/api/events/assistant-thinking-delta.ts +10 -0
  102. package/src/api/events/usage-update.ts +7 -0
  103. package/src/api/index.ts +4 -1
  104. package/src/api/responses/memory-v3-selection-log.ts +18 -11
  105. package/src/approvals/approval-primitive.ts +2 -2
  106. package/src/background-wake/background-wake-routes.test.ts +5 -2
  107. package/src/bundler/compiler-tools.ts +1 -1
  108. package/src/bundler/package-resolver.ts +0 -1
  109. package/src/calls/call-domain.ts +1 -1
  110. package/src/calls/guardian-action-sweep.ts +16 -93
  111. package/src/cli/AGENTS.md +4 -0
  112. package/src/cli/commands/__tests__/schedules.test.ts +430 -1
  113. package/src/cli/commands/credentials.ts +28 -24
  114. package/src/cli/commands/image-generation.ts +23 -9
  115. package/src/cli/commands/notifications.ts +1 -1
  116. package/src/cli/commands/plugins.ts +89 -46
  117. package/src/cli/commands/schedules.ts +384 -11
  118. package/src/cli/lib/__tests__/inspect-plugin.test.ts +69 -5
  119. package/src/cli/lib/__tests__/install-from-github.test.ts +15 -0
  120. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +81 -4
  121. package/src/cli/lib/inspect-plugin.ts +62 -1
  122. package/src/cli/lib/install-from-github.ts +52 -5
  123. package/src/cli/lib/upgrade-plugin.ts +18 -0
  124. package/src/config/__tests__/workflows-schema.test.ts +60 -0
  125. package/src/config/bundled-skills/acp/SKILL.md +2 -2
  126. package/src/config/bundled-skills/app-builder/SKILL.md +1 -1
  127. package/src/config/bundled-skills/app-builder/tools/app-create.ts +6 -1
  128. package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +7 -1
  129. package/src/config/bundled-skills/app-builder/tools/app-refresh.ts +7 -1
  130. package/src/config/bundled-skills/app-builder/tools/app-update.ts +10 -1
  131. package/src/config/bundled-skills/image-studio/SKILL.md +66 -19
  132. package/src/config/bundled-skills/image-studio/TOOLS.json +1 -6
  133. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +22 -3
  134. package/src/config/bundled-skills/personal-page/SKILL.md +57 -0
  135. package/src/config/bundled-skills/personal-page/TOOLS.json +27 -0
  136. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +17 -0
  137. package/src/config/bundled-skills/schedule/SKILL.md +7 -2
  138. package/src/config/bundled-skills/schedule/TOOLS.json +48 -4
  139. package/src/config/bundled-skills/workflows/SKILL.md +214 -0
  140. package/src/config/bundled-skills/workflows/TOOLS.json +84 -0
  141. package/src/config/bundled-skills/workflows/tools/manage-workflows.ts +12 -0
  142. package/src/config/bundled-skills/workflows/tools/run-workflow.ts +12 -0
  143. package/src/config/bundled-tool-registry.ts +14 -2
  144. package/src/config/call-site-defaults.ts +5 -0
  145. package/src/config/feature-flag-registry.json +12 -4
  146. package/src/config/llm-context-resolution.ts +8 -0
  147. package/src/config/llm-resolver.ts +30 -0
  148. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +22 -0
  149. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +16 -0
  150. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +77 -0
  151. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +57 -0
  152. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +28 -0
  153. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +234 -0
  154. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +48 -0
  155. package/src/config/preloaded-apps/personal-page/src/components/media.ts +16 -0
  156. package/src/config/preloaded-apps/personal-page/src/index.html +20 -0
  157. package/src/config/preloaded-apps/personal-page/src/main.tsx +7 -0
  158. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +82 -0
  159. package/src/config/preloaded-apps/personal-page/src/styles.css +759 -0
  160. package/src/config/schema.ts +2 -0
  161. package/src/config/schemas/call-site-catalog.ts +7 -0
  162. package/src/config/schemas/heartbeat.ts +4 -1
  163. package/src/config/schemas/llm.ts +33 -26
  164. package/src/config/schemas/memory-retrospective.ts +19 -0
  165. package/src/config/schemas/platform.ts +8 -0
  166. package/src/config/schemas/services.ts +5 -2
  167. package/src/config/schemas/workflows.ts +42 -0
  168. package/src/config/skills.ts +3 -3
  169. package/src/context/compactor.ts +273 -39
  170. package/src/context/post-turn-tool-result-truncation.ts +23 -6
  171. package/src/context/tool-result-spool.ts +12 -17
  172. package/src/credential-execution/executable-discovery.ts +1 -1
  173. package/src/credential-execution/process-manager.ts +37 -3
  174. package/src/credential-execution/prompted-credential.ts +205 -0
  175. package/src/daemon/conversation-agent-loop-handlers.ts +14 -0
  176. package/src/daemon/conversation-process.ts +11 -2
  177. package/src/daemon/conversation-surfaces.ts +167 -3
  178. package/src/daemon/conversation-tool-setup.ts +103 -26
  179. package/src/daemon/conversation-usage.ts +2 -0
  180. package/src/daemon/conversation.ts +115 -11
  181. package/src/daemon/handlers/shared.ts +26 -14
  182. package/src/daemon/host-cu-proxy.ts +15 -12
  183. package/src/daemon/host-file-proxy.ts +15 -12
  184. package/src/daemon/host-transfer-proxy.ts +30 -24
  185. package/src/daemon/lifecycle.ts +40 -3
  186. package/src/daemon/message-protocol.ts +3 -0
  187. package/src/daemon/message-types/messages.ts +2 -10
  188. package/src/daemon/message-types/workflows.ts +49 -0
  189. package/src/daemon/parse-actual-tokens-from-error.test.ts +62 -1
  190. package/src/daemon/parse-actual-tokens-from-error.ts +43 -4
  191. package/src/daemon/process-message.ts +6 -0
  192. package/src/daemon/tool-setup-types.ts +57 -0
  193. package/src/daemon/wake-conversation-ops.ts +18 -0
  194. package/src/heartbeat/heartbeat-run-store.ts +8 -2
  195. package/src/home/feed-types.ts +1 -1
  196. package/src/ipc/gateway-flag-listener.ts +28 -6
  197. package/src/mcp/mcp-auth-state.ts +8 -20
  198. package/src/media/__tests__/image-models.test.ts +57 -0
  199. package/src/media/image-models.ts +66 -0
  200. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +38 -0
  201. package/src/memory/__tests__/find-most-recent-retrospective-for.test.ts +12 -2
  202. package/src/memory/__tests__/memory-retrospective-job.test.ts +911 -34
  203. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +227 -5
  204. package/src/memory/__tests__/memory-retrospective-state.test.ts +195 -0
  205. package/src/memory/__tests__/preloaded-apps.test.ts +85 -0
  206. package/src/memory/auto-analysis-enqueue.ts +14 -1
  207. package/src/memory/compaction-log-store-clickhouse.ts +6 -4
  208. package/src/memory/conversation-crud.ts +9 -2
  209. package/src/memory/conversation-disk-view.ts +1 -1
  210. package/src/memory/conversation-queries.ts +22 -7
  211. package/src/memory/db-init.ts +20 -0
  212. package/src/memory/db-maintenance.ts +16 -0
  213. package/src/memory/embedding-runtime-manager.ts +1 -1
  214. package/src/memory/llm-request-log-source-clickhouse.ts +112 -14
  215. package/src/memory/llm-request-log-source-local.ts +19 -1
  216. package/src/memory/llm-request-log-source.ts +35 -6
  217. package/src/memory/llm-request-log-store.ts +90 -2
  218. package/src/memory/memory-retrospective-constants.ts +9 -0
  219. package/src/memory/memory-retrospective-enqueue.ts +3 -6
  220. package/src/memory/memory-retrospective-fork-boundary.ts +94 -0
  221. package/src/memory/memory-retrospective-job.ts +500 -208
  222. package/src/memory/memory-retrospective-startup-cleanup.ts +97 -19
  223. package/src/memory/memory-retrospective-state.ts +85 -2
  224. package/src/memory/migrations/281-memory-retrospective-remembered-log.ts +40 -0
  225. package/src/memory/migrations/282-schedule-inference-profile.test.ts +77 -0
  226. package/src/memory/migrations/282-schedule-inference-profile.ts +26 -0
  227. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.test.ts +102 -0
  228. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.ts +53 -0
  229. package/src/memory/migrations/284-workflow-runs.ts +51 -0
  230. package/src/memory/migrations/285-schedule-workflow-mode.ts +26 -0
  231. package/src/memory/migrations/286-workflow-run-trust.ts +27 -0
  232. package/src/memory/migrations/287-conversation-origin-channel-index.ts +15 -0
  233. package/src/memory/migrations/288-backfill-origin-channel-from-bindings.ts +43 -0
  234. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +115 -0
  235. package/src/memory/migrations/290-schedule-capabilities.test.ts +77 -0
  236. package/src/memory/migrations/290-schedule-capabilities.ts +25 -0
  237. package/src/memory/migrations/__tests__/281-memory-retrospective-remembered-log.test.ts +96 -0
  238. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +571 -0
  239. package/src/memory/migrations/index.ts +10 -0
  240. package/src/memory/preloaded-apps.ts +116 -0
  241. package/src/memory/schema/infrastructure.ts +4 -0
  242. package/src/memory/schema/memory-core.ts +4 -0
  243. package/src/memory/v2/__tests__/concept-page-frontmatter-schema.test.ts +45 -0
  244. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +11 -7
  245. package/src/memory/v2/__tests__/page-store.test.ts +13 -2
  246. package/src/memory/v2/__tests__/qdrant.test.ts +24 -0
  247. package/src/memory/v2/frontmatter-sweep.ts +7 -6
  248. package/src/memory/v2/page-store.ts +4 -3
  249. package/src/memory/v2/qdrant.ts +42 -3
  250. package/src/memory/v2/types.ts +16 -10
  251. package/src/messaging/draft-store.ts +1 -1
  252. package/src/notifications/access-request-copy.ts +200 -113
  253. package/src/notifications/adapters/slack.ts +250 -111
  254. package/src/notifications/adapters/telegram.ts +7 -44
  255. package/src/notifications/approval-card-builder.ts +93 -0
  256. package/src/notifications/broadcaster.ts +74 -0
  257. package/src/notifications/conversation-pairing.ts +8 -6
  258. package/src/notifications/copy-composer.ts +32 -26
  259. package/src/notifications/decision-engine.ts +59 -7
  260. package/src/notifications/guardian-question-mode.ts +145 -155
  261. package/src/notifications/home-feed-side-effect.ts +28 -11
  262. package/src/notifications/notification-utils.ts +66 -0
  263. package/src/notifications/signal.ts +6 -0
  264. package/src/notifications/tool-approval-copy.ts +142 -0
  265. package/src/notifications/types.ts +19 -0
  266. package/src/permissions/threshold.ts +11 -0
  267. package/src/plugin-api/types.ts +16 -4
  268. package/src/plugins/defaults/compaction/window-manager.ts +44 -0
  269. package/src/plugins/defaults/index.ts +46 -0
  270. package/src/plugins/defaults/max-tokens-continue/continue-state-store.ts +53 -0
  271. package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +80 -0
  272. package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +20 -0
  273. package/src/plugins/defaults/max-tokens-continue/package.json +14 -0
  274. package/src/plugins/defaults/memory-retrieval/hooks/__tests__/user-prompt-submit.test.ts +37 -0
  275. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +29 -1
  276. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +8 -3
  277. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +4 -2
  278. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +28 -18
  279. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +67 -0
  280. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +122 -22
  281. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +2 -0
  282. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +63 -1
  283. package/src/plugins/defaults/memory-v3-shadow/injector.ts +61 -18
  284. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +1 -1
  285. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +39 -10
  286. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +34 -1
  287. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +112 -47
  288. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +78 -15
  289. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +206 -0
  290. package/src/plugins/defaults/task-progress-nudge/package.json +15 -0
  291. package/src/prompts/__tests__/system-prompt.test.ts +100 -1
  292. package/src/prompts/__tests__/task-progress-hint-section.test.ts +5 -7
  293. package/src/prompts/normalize-onboarding.ts +2 -0
  294. package/src/prompts/persona-resolver.ts +3 -0
  295. package/src/prompts/system-prompt.ts +51 -2
  296. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +3 -1
  297. package/src/prompts/templates/system-sections.ts +8 -3
  298. package/src/providers/call-site-routing.ts +6 -3
  299. package/src/providers/fireworks/client.ts +3 -0
  300. package/src/providers/inference/auth.ts +52 -46
  301. package/src/providers/model-intents.ts +2 -2
  302. package/src/providers/openai/__tests__/coerce-object-args.test.ts +105 -0
  303. package/src/providers/openai/chat-completions-provider.ts +47 -9
  304. package/src/providers/openai/coerce-object-args.ts +104 -0
  305. package/src/providers/retry.ts +8 -5
  306. package/src/providers/types.ts +10 -0
  307. package/src/runtime/__tests__/agent-wake.test.ts +629 -7
  308. package/src/runtime/access-request-helper.ts +16 -9
  309. package/src/runtime/agent-wake.ts +302 -51
  310. package/src/runtime/assistant-stream-state.ts +141 -8
  311. package/src/runtime/background-job-runner.ts +9 -0
  312. package/src/runtime/channel-approval-types.ts +1 -0
  313. package/src/runtime/channel-invite-transports/telegram.ts +6 -5
  314. package/src/runtime/channel-invite-transports/voice.ts +2 -2
  315. package/src/runtime/channel-invite-types.ts +4 -2
  316. package/src/runtime/channel-retry-sweep.ts +19 -41
  317. package/src/runtime/finalize-event-delivery.ts +72 -0
  318. package/src/runtime/guardian-action-message-composer.ts +0 -54
  319. package/src/runtime/http-server.ts +6 -14
  320. package/src/runtime/http-types.ts +0 -1
  321. package/src/runtime/message-composer-types.ts +0 -9
  322. package/src/runtime/middleware/__tests__/rate-limiter.test.ts +63 -0
  323. package/src/runtime/middleware/auth.ts +27 -3
  324. package/src/runtime/middleware/rate-limiter.ts +28 -1
  325. package/src/runtime/migrations/vbundle-builder.ts +6 -5
  326. package/src/runtime/pending-interactions.ts +20 -1
  327. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +232 -173
  328. package/src/runtime/routes/__tests__/plugins-routes.test.ts +18 -0
  329. package/src/runtime/routes/__tests__/retrospective-routes.test.ts +436 -0
  330. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +11 -0
  331. package/src/runtime/routes/approval-routes.ts +35 -8
  332. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -1
  333. package/src/runtime/routes/btw-routes.ts +37 -1
  334. package/src/runtime/routes/channel-delivery-routes.ts +11 -7
  335. package/src/runtime/routes/channel-route-definitions.ts +3 -0
  336. package/src/runtime/routes/channel-route-shared.ts +3 -1
  337. package/src/runtime/routes/consolidation-routes.ts +17 -13
  338. package/src/runtime/routes/conversation-compaction-routes.ts +159 -119
  339. package/src/runtime/routes/conversation-list-routes.ts +41 -4
  340. package/src/runtime/routes/conversation-query-routes.ts +196 -11
  341. package/src/runtime/routes/conversation-routes.ts +15 -1
  342. package/src/runtime/routes/credential-prompt-routes.ts +39 -17
  343. package/src/runtime/routes/empty-state-greeting-cache.ts +65 -0
  344. package/src/runtime/routes/heartbeat-routes.ts +18 -13
  345. package/src/runtime/routes/image-generation-routes.ts +20 -2
  346. package/src/runtime/routes/inbound-message-handler.ts +13 -12
  347. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +32 -31
  348. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +7 -5
  349. package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -21
  350. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +5 -5
  351. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +6 -15
  352. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +1 -1
  353. package/src/runtime/routes/index.ts +6 -0
  354. package/src/runtime/routes/log-export/AGENTS.md +1 -1
  355. package/src/runtime/routes/log-export/workspace-allowlist.ts +1 -1
  356. package/src/runtime/routes/migration-routes.ts +5 -9
  357. package/src/runtime/routes/plugins-routes.ts +26 -0
  358. package/src/runtime/routes/ps-routes.ts +10 -8
  359. package/src/runtime/routes/retrospective-routes.ts +235 -0
  360. package/src/runtime/routes/runs-pagination.ts +75 -0
  361. package/src/runtime/routes/schedule-routes.ts +367 -59
  362. package/src/runtime/routes/sounds-config-routes.ts +239 -0
  363. package/src/runtime/routes/surface-action-routes.ts +84 -4
  364. package/src/runtime/routes/workflow-routes.test.ts +372 -0
  365. package/src/runtime/routes/workflow-routes.ts +363 -0
  366. package/src/runtime/routes/workspace-routes.test.ts +61 -1
  367. package/src/runtime/routes/workspace-routes.ts +26 -1
  368. package/src/runtime/services/__tests__/analyze-conversation.test.ts +38 -0
  369. package/src/runtime/services/analyze-conversation.ts +26 -13
  370. package/src/schedule/inference-profile.ts +28 -0
  371. package/src/schedule/schedule-store.ts +152 -4
  372. package/src/schedule/scheduler-types.ts +6 -0
  373. package/src/schedule/scheduler.ts +96 -0
  374. package/src/security/secret-allowlist.ts +1 -1
  375. package/src/skills/path-classifier.ts +1 -1
  376. package/src/tools/apps/executors.ts +23 -0
  377. package/src/tools/apps/resolve-app-id.ts +42 -0
  378. package/src/tools/browser/browser-execution.ts +9 -11
  379. package/src/tools/credentials/broker.ts +4 -4
  380. package/src/tools/credentials/vault.ts +26 -137
  381. package/src/tools/executor.ts +69 -0
  382. package/src/tools/flag-gated-tools.test.ts +76 -0
  383. package/src/tools/permission-checker.ts +8 -1
  384. package/src/tools/registry.ts +51 -0
  385. package/src/tools/schedule/create.ts +77 -1
  386. package/src/tools/schedule/list.ts +1 -0
  387. package/src/tools/schedule/update.ts +73 -1
  388. package/src/tools/skills/execute.ts +56 -0
  389. package/src/tools/terminal/shell.ts +1 -1
  390. package/src/tools/ui-surface/definitions.ts +91 -2
  391. package/src/tools/workflows/manage-workflows.ts +183 -0
  392. package/src/tools/workflows/run-workflow.test.ts +442 -0
  393. package/src/tools/workflows/run-workflow.ts +88 -0
  394. package/src/types/onboarding-context.ts +2 -0
  395. package/src/usage/attribution.ts +24 -0
  396. package/src/util/canonicalize-identity.ts +12 -3
  397. package/src/util/platform.ts +17 -17
  398. package/src/watcher/__tests__/engine.test.ts +24 -0
  399. package/src/watcher/__tests__/telemetry.test.ts +135 -0
  400. package/src/watcher/engine.ts +7 -0
  401. package/src/watcher/telemetry.ts +74 -0
  402. package/src/workflows/capabilities.test.ts +365 -0
  403. package/src/workflows/capabilities.ts +359 -0
  404. package/src/workflows/deterministic-stringify.ts +27 -0
  405. package/src/workflows/engine-integration.test.ts +656 -0
  406. package/src/workflows/engine.test.ts +1144 -0
  407. package/src/workflows/engine.ts +1078 -0
  408. package/src/workflows/fanout-load.test.ts +168 -0
  409. package/src/workflows/journal-store.test.ts +369 -0
  410. package/src/workflows/journal-store.ts +470 -0
  411. package/src/workflows/leaf-runner.test.ts +704 -0
  412. package/src/workflows/leaf-runner.ts +589 -0
  413. package/src/workflows/library.test.ts +134 -0
  414. package/src/workflows/library.ts +124 -0
  415. package/src/workflows/run-manager.test.ts +711 -0
  416. package/src/workflows/run-manager.ts +593 -0
  417. package/src/workflows/sandbox-escape.test.ts +339 -0
  418. package/src/workflows/sandbox.test.ts +251 -0
  419. package/src/workflows/sandbox.ts +447 -0
  420. package/src/workspace/adaptive-thinking-repair.ts +33 -11
  421. package/src/workspace/migrations/021-move-signals-to-workspace.ts +1 -1
  422. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +1 -1
  423. package/src/workspace/migrations/026-backfill-install-meta.ts +1 -1
  424. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +2 -1
  425. package/src/workspace/migrations/031-drop-user-md.ts +1 -4
  426. package/src/workspace/migrations/048-remove-workspace-hooks.ts +1 -1
  427. package/src/workspace/migrations/056-release-notes-inference-profile-reordering.ts +5 -2
  428. package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +1 -1
  429. package/src/workspace/migrations/082-backfill-managed-profile-labels.ts +8 -2
  430. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +41 -14
  431. package/src/workspace/migrations/102-preserve-heartbeat-enabled-for-existing-workspaces.ts +69 -0
  432. package/src/workspace/migrations/103-upgrade-quality-profile-to-opus-4-8.ts +83 -0
  433. package/src/workspace/migrations/104-recheck-adaptive-thinking-model-implied-anthropic.ts +133 -0
  434. package/src/workspace/migrations/registry.ts +6 -0
  435. package/src/workspace/migrations/runner.ts +1 -1
  436. package/tsconfig.json +1 -1
  437. package/src/__tests__/guardian-action-copy-generator.test.ts +0 -200
  438. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -579
  439. package/src/__tests__/guardian-action-store.test.ts +0 -106
  440. package/src/daemon/guardian-action-generators.ts +0 -71
  441. package/src/memory/guardian-action-store.ts +0 -484
  442. package/src/runtime/guardian-action-grant-minter.ts +0 -150
@@ -0,0 +1,209 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { buildToolApprovalSeedContentBlocks } from "../notifications/tool-approval-copy.js";
4
+
5
+ describe("buildToolApprovalSeedContentBlocks", () => {
6
+ const toolApprovalPayload: Record<string, unknown> = {
7
+ requestId: "req-tool-456",
8
+ requestCode: "XYZ789",
9
+ requestKind: "tool_approval",
10
+ toolName: "bash",
11
+ questionText:
12
+ 'Bob wants to use "bash": mkdir -p scratch && assistant credentials reveal --service slack',
13
+ sourceChannel: "slack",
14
+ requesterIdentifier: "Bob",
15
+ };
16
+
17
+ const toolGrantPayload: Record<string, unknown> = {
18
+ requestId: "req-grant-789",
19
+ requestCode: "GHI012",
20
+ requestKind: "tool_grant_request",
21
+ toolName: "web_search",
22
+ questionText: 'Alice wants to use "web_search": search for latest news',
23
+ sourceChannel: "telegram",
24
+ requesterIdentifier: "Alice",
25
+ };
26
+
27
+ const voiceToolApprovalPayload: Record<string, unknown> = {
28
+ requestId: "req-voice-101",
29
+ requestCode: "VOICE1",
30
+ requestKind: "pending_question",
31
+ toolName: "bash",
32
+ questionText: 'Bob wants to use "bash": echo hello',
33
+ sourceChannel: "phone",
34
+ requesterIdentifier: "Bob",
35
+ };
36
+
37
+ test("returns null for non-tool-approval request kinds", () => {
38
+ expect(
39
+ buildToolApprovalSeedContentBlocks({ requestKind: "pending_question" }),
40
+ ).toBeNull();
41
+ expect(
42
+ buildToolApprovalSeedContentBlocks({ requestKind: "access_request" }),
43
+ ).toBeNull();
44
+ expect(buildToolApprovalSeedContentBlocks({})).toBeNull();
45
+ });
46
+
47
+ test("produces blocks for pending_question with toolName (voice tool approval)", () => {
48
+ const blocks = buildToolApprovalSeedContentBlocks(
49
+ voiceToolApprovalPayload,
50
+ )!;
51
+ expect(blocks).toHaveLength(2);
52
+ expect((blocks[0] as Record<string, unknown>).type).toBe("ui_surface");
53
+ const surface = blocks[0] as Record<string, unknown>;
54
+ expect(surface.surfaceType).toBe("card");
55
+ expect(surface.surfaceId).toBe("tool-approval-req-voice-101");
56
+ const data = surface.data as Record<string, unknown>;
57
+ expect(data.subtitle).toBe("Requesting approval to run this tool");
58
+ const metadata = data.metadata as Array<{ label: string; value: string }>;
59
+ expect(metadata).toContainEqual({ label: "Tool", value: "bash" });
60
+ expect(metadata).toContainEqual({ label: "Source", value: "phone" });
61
+ });
62
+
63
+ test("returns null for pending_question without toolName", () => {
64
+ const payload = { ...voiceToolApprovalPayload, toolName: undefined };
65
+ expect(buildToolApprovalSeedContentBlocks(payload)).toBeNull();
66
+ });
67
+
68
+ test("produces a ui_surface block and a text fallback block for tool_approval", () => {
69
+ const blocks = buildToolApprovalSeedContentBlocks(toolApprovalPayload)!;
70
+ expect(blocks).toHaveLength(2);
71
+ expect((blocks[0] as Record<string, unknown>).type).toBe("ui_surface");
72
+ expect((blocks[1] as Record<string, unknown>).type).toBe("text");
73
+ });
74
+
75
+ test("produces a ui_surface block and a text fallback block for tool_grant_request", () => {
76
+ const blocks = buildToolApprovalSeedContentBlocks(toolGrantPayload)!;
77
+ expect(blocks).toHaveLength(2);
78
+ expect((blocks[0] as Record<string, unknown>).type).toBe("ui_surface");
79
+ expect((blocks[1] as Record<string, unknown>).type).toBe("text");
80
+ });
81
+
82
+ test("card surface has correct surfaceType and surfaceId for tool_approval", () => {
83
+ const blocks = buildToolApprovalSeedContentBlocks(toolApprovalPayload)!;
84
+ const surface = blocks[0] as Record<string, unknown>;
85
+ expect(surface.surfaceType).toBe("card");
86
+ expect(surface.surfaceId).toBe("tool-approval-req-tool-456");
87
+ expect(surface.title).toBe("Tool Approval");
88
+ });
89
+
90
+ test("card surface has correct title for tool_grant_request", () => {
91
+ const blocks = buildToolApprovalSeedContentBlocks(toolGrantPayload)!;
92
+ const surface = blocks[0] as Record<string, unknown>;
93
+ expect(surface.title).toBe("Tool Grant Request");
94
+ });
95
+
96
+ test("card data uses requesterIdentifier as title", () => {
97
+ const blocks = buildToolApprovalSeedContentBlocks(toolApprovalPayload)!;
98
+ const data = (blocks[0] as Record<string, unknown>).data as Record<
99
+ string,
100
+ unknown
101
+ >;
102
+ expect(data.title).toBe("Bob");
103
+ });
104
+
105
+ test("card data falls back to 'Someone' when no requesterIdentifier", () => {
106
+ const payload = { ...toolApprovalPayload, requesterIdentifier: undefined };
107
+ const blocks = buildToolApprovalSeedContentBlocks(payload)!;
108
+ const data = (blocks[0] as Record<string, unknown>).data as Record<
109
+ string,
110
+ unknown
111
+ >;
112
+ expect(data.title).toBe("Someone");
113
+ });
114
+
115
+ test("card subtitle differs for tool_approval vs tool_grant_request", () => {
116
+ const approvalBlocks =
117
+ buildToolApprovalSeedContentBlocks(toolApprovalPayload)!;
118
+ const grantBlocks = buildToolApprovalSeedContentBlocks(toolGrantPayload)!;
119
+ const approvalData = (approvalBlocks[0] as Record<string, unknown>)
120
+ .data as Record<string, unknown>;
121
+ const grantData = (grantBlocks[0] as Record<string, unknown>)
122
+ .data as Record<string, unknown>;
123
+ expect(approvalData.subtitle).toBe("Requesting approval to run this tool");
124
+ expect(grantData.subtitle).toBe("Requesting permission to use this tool");
125
+ });
126
+
127
+ test("includes tool name and source channel in metadata", () => {
128
+ const blocks = buildToolApprovalSeedContentBlocks(toolApprovalPayload)!;
129
+ const data = (blocks[0] as Record<string, unknown>).data as Record<
130
+ string,
131
+ unknown
132
+ >;
133
+ const metadata = data.metadata as Array<{ label: string; value: string }>;
134
+ expect(metadata).toContainEqual({ label: "Tool", value: "bash" });
135
+ expect(metadata).toContainEqual({ label: "Source", value: "slack" });
136
+ });
137
+
138
+ test("body includes questionText as blockquote", () => {
139
+ const blocks = buildToolApprovalSeedContentBlocks(toolApprovalPayload)!;
140
+ const data = (blocks[0] as Record<string, unknown>).data as Record<
141
+ string,
142
+ unknown
143
+ >;
144
+ expect(data.body).toContain("Bob wants to use");
145
+ expect(data.body).toContain("bash");
146
+ });
147
+
148
+ test("body shows fallback when no questionText", () => {
149
+ const payload = { ...toolApprovalPayload, questionText: undefined };
150
+ const blocks = buildToolApprovalSeedContentBlocks(payload)!;
151
+ const data = (blocks[0] as Record<string, unknown>).data as Record<
152
+ string,
153
+ unknown
154
+ >;
155
+ expect(data.body).toBe("No additional context available.");
156
+ });
157
+
158
+ test("surface block includes approve/reject actions when requestId present", () => {
159
+ const blocks = buildToolApprovalSeedContentBlocks(toolApprovalPayload)!;
160
+ const surface = blocks[0] as Record<string, unknown>;
161
+ const actions = surface.actions as Array<{
162
+ id: string;
163
+ label: string;
164
+ style: string;
165
+ }>;
166
+ expect(actions).toHaveLength(2);
167
+ expect(actions[0]).toEqual({
168
+ id: "apr:req-tool-456:approve_once",
169
+ label: "Approve",
170
+ style: "primary",
171
+ });
172
+ expect(actions[1]).toEqual({
173
+ id: "apr:req-tool-456:reject",
174
+ label: "Reject",
175
+ style: "destructive",
176
+ });
177
+ });
178
+
179
+ test("surface block omits actions when requestId is missing", () => {
180
+ const payload = { ...toolApprovalPayload, requestId: undefined };
181
+ const blocks = buildToolApprovalSeedContentBlocks(payload)!;
182
+ const surface = blocks[0] as Record<string, unknown>;
183
+ expect(surface.actions).toBeUndefined();
184
+ });
185
+
186
+ test("text fallback block contains questionText and request-code instruction", () => {
187
+ const blocks = buildToolApprovalSeedContentBlocks(toolApprovalPayload)!;
188
+ const textBlock = blocks[1] as Record<string, unknown>;
189
+ expect(textBlock.type).toBe("text");
190
+ expect(textBlock.text).toContain("Bob wants to use");
191
+ expect(textBlock.text).toContain("XYZ789");
192
+ expect(textBlock.text).toContain("approve");
193
+ });
194
+
195
+ test("text fallback block omits request-code instruction when no requestCode", () => {
196
+ const payload = { ...toolApprovalPayload, requestCode: undefined };
197
+ const blocks = buildToolApprovalSeedContentBlocks(payload)!;
198
+ const textBlock = blocks[1] as Record<string, unknown>;
199
+ expect(textBlock.text).toContain("Bob wants to use");
200
+ expect(textBlock.text).not.toContain("XYZ789");
201
+ });
202
+
203
+ test("text fallback block uses generic text when no questionText", () => {
204
+ const payload = { ...toolApprovalPayload, questionText: undefined };
205
+ const blocks = buildToolApprovalSeedContentBlocks(payload)!;
206
+ const textBlock = blocks[1] as Record<string, unknown>;
207
+ expect(textBlock.text).toContain("requesting approval to use bash");
208
+ });
209
+ });
@@ -110,12 +110,13 @@ describe("spoolAndStubOversizedToolResults", () => {
110
110
  expect(messages).toBe(history);
111
111
  });
112
112
 
113
- test("skips below-threshold, error, exempt, file_read, marked, and ax-tree results", () => {
113
+ test("skips below-threshold, error, exempt, file_read, host_file_read, marked, and ax-tree results", () => {
114
114
  const blocks = [
115
115
  makeToolResult(SHORT, "tu_short"),
116
116
  makeToolResult(LONG, "tu_err", true),
117
117
  makeToolResult(LONG, "tu_skill"),
118
118
  makeToolResult(LONG, "tu_read"),
119
+ makeToolResult(LONG, "tu_host_read"),
119
120
  makeToolResult(`${LONG}${TRUNCATION_MARKER}`, "tu_marked"),
120
121
  makeToolResult(`<ax-tree>${LONG}</ax-tree>`, "tu_ax"),
121
122
  { type: "text" as const, text: LONG },
@@ -127,6 +128,7 @@ describe("spoolAndStubOversizedToolResults", () => {
127
128
  toolNameById: (id) => {
128
129
  if (id === "tu_skill") return "skill_load";
129
130
  if (id === "tu_read") return "file_read";
131
+ if (id === "tu_host_read") return "host_file_read";
130
132
  return undefined;
131
133
  },
132
134
  });
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Tests for workspace migration `102-preserve-heartbeat-enabled-for-existing-workspaces`.
3
+ *
4
+ * The schema default for `heartbeat.enabled` flipped from true to false so
5
+ * heartbeats are opt-in for new users. The migration persists an explicit
6
+ * `enabled: true` for upgrading workspaces that never wrote the key (they
7
+ * relied on the old default-on behavior), creating `config.json` when it
8
+ * does not exist. It must skip fresh workspaces, leave explicit user values
9
+ * untouched, and be idempotent.
10
+ */
11
+
12
+ import {
13
+ existsSync,
14
+ mkdtempSync,
15
+ readFileSync,
16
+ rmSync,
17
+ writeFileSync,
18
+ } from "node:fs";
19
+ import { tmpdir } from "node:os";
20
+ import { join } from "node:path";
21
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
22
+
23
+ import { HeartbeatConfigSchema } from "../config/schemas/heartbeat.js";
24
+ import { preserveHeartbeatEnabledForExistingWorkspacesMigration } from "../workspace/migrations/102-preserve-heartbeat-enabled-for-existing-workspaces.js";
25
+ import type { MigrationRunContext } from "../workspace/migrations/types.js";
26
+
27
+ const NEW_WORKSPACE_CTX: MigrationRunContext = { isNewWorkspace: true };
28
+ const UPGRADE_CTX: MigrationRunContext = { isNewWorkspace: false };
29
+
30
+ let workspaceDir: string;
31
+ let configPath: string;
32
+
33
+ beforeEach(() => {
34
+ workspaceDir = mkdtempSync(join(tmpdir(), "vellum-migration-102-test-"));
35
+ configPath = join(workspaceDir, "config.json");
36
+ });
37
+
38
+ afterEach(() => {
39
+ if (existsSync(workspaceDir)) {
40
+ rmSync(workspaceDir, { recursive: true, force: true });
41
+ }
42
+ });
43
+
44
+ function readConfig(): Record<string, unknown> {
45
+ return JSON.parse(readFileSync(configPath, "utf-8"));
46
+ }
47
+
48
+ describe("102-preserve-heartbeat-enabled-for-existing-workspaces migration", () => {
49
+ test("has correct id and description", () => {
50
+ expect(preserveHeartbeatEnabledForExistingWorkspacesMigration.id).toBe(
51
+ "102-preserve-heartbeat-enabled-for-existing-workspaces",
52
+ );
53
+ expect(
54
+ preserveHeartbeatEnabledForExistingWorkspacesMigration.description,
55
+ ).toContain("heartbeat.enabled");
56
+ });
57
+
58
+ test("schema default is disabled (the flip this migration compensates for)", () => {
59
+ expect(HeartbeatConfigSchema.parse({}).enabled).toBe(false);
60
+ });
61
+
62
+ test("skips fresh workspaces so new users get the opt-in default", () => {
63
+ preserveHeartbeatEnabledForExistingWorkspacesMigration.run(
64
+ workspaceDir,
65
+ NEW_WORKSPACE_CTX,
66
+ );
67
+
68
+ expect(existsSync(configPath)).toBe(false);
69
+ });
70
+
71
+ test("creates config.json with enabled=true when an existing workspace has none", () => {
72
+ preserveHeartbeatEnabledForExistingWorkspacesMigration.run(
73
+ workspaceDir,
74
+ UPGRADE_CTX,
75
+ );
76
+
77
+ expect(readConfig()).toEqual({ heartbeat: { enabled: true } });
78
+ });
79
+
80
+ test("adds heartbeat block to an existing config without one", () => {
81
+ writeFileSync(configPath, JSON.stringify({ name: "Assistant" }), "utf-8");
82
+
83
+ preserveHeartbeatEnabledForExistingWorkspacesMigration.run(
84
+ workspaceDir,
85
+ UPGRADE_CTX,
86
+ );
87
+
88
+ expect(readConfig()).toEqual({
89
+ name: "Assistant",
90
+ heartbeat: { enabled: true },
91
+ });
92
+ });
93
+
94
+ test("adds enabled=true to a heartbeat block without the key, preserving siblings", () => {
95
+ writeFileSync(
96
+ configPath,
97
+ JSON.stringify({ heartbeat: { intervalMs: 1800000 } }),
98
+ "utf-8",
99
+ );
100
+
101
+ preserveHeartbeatEnabledForExistingWorkspacesMigration.run(
102
+ workspaceDir,
103
+ UPGRADE_CTX,
104
+ );
105
+
106
+ expect(readConfig()).toEqual({
107
+ heartbeat: { intervalMs: 1800000, enabled: true },
108
+ });
109
+ });
110
+
111
+ test.each([[true], [false]])(
112
+ "leaves an explicit enabled=%p untouched",
113
+ (enabled) => {
114
+ const original = JSON.stringify({ heartbeat: { enabled } });
115
+ writeFileSync(configPath, original, "utf-8");
116
+
117
+ preserveHeartbeatEnabledForExistingWorkspacesMigration.run(
118
+ workspaceDir,
119
+ UPGRADE_CTX,
120
+ );
121
+
122
+ expect(readFileSync(configPath, "utf-8")).toBe(original);
123
+ },
124
+ );
125
+
126
+ test("treats a missing context as an existing workspace", () => {
127
+ preserveHeartbeatEnabledForExistingWorkspacesMigration.run(workspaceDir);
128
+
129
+ expect(readConfig()).toEqual({ heartbeat: { enabled: true } });
130
+ });
131
+
132
+ test("leaves malformed config.json untouched", () => {
133
+ writeFileSync(configPath, "{ not json", "utf-8");
134
+
135
+ preserveHeartbeatEnabledForExistingWorkspacesMigration.run(
136
+ workspaceDir,
137
+ UPGRADE_CTX,
138
+ );
139
+
140
+ expect(readFileSync(configPath, "utf-8")).toBe("{ not json");
141
+ });
142
+
143
+ test("leaves a non-object heartbeat value untouched", () => {
144
+ const original = JSON.stringify({ heartbeat: "off" });
145
+ writeFileSync(configPath, original, "utf-8");
146
+
147
+ preserveHeartbeatEnabledForExistingWorkspacesMigration.run(
148
+ workspaceDir,
149
+ UPGRADE_CTX,
150
+ );
151
+
152
+ expect(readFileSync(configPath, "utf-8")).toBe(original);
153
+ });
154
+
155
+ test("is idempotent", () => {
156
+ preserveHeartbeatEnabledForExistingWorkspacesMigration.run(
157
+ workspaceDir,
158
+ UPGRADE_CTX,
159
+ );
160
+ const afterFirst = readFileSync(configPath, "utf-8");
161
+
162
+ preserveHeartbeatEnabledForExistingWorkspacesMigration.run(
163
+ workspaceDir,
164
+ UPGRADE_CTX,
165
+ );
166
+
167
+ expect(readFileSync(configPath, "utf-8")).toBe(afterFirst);
168
+ });
169
+
170
+ test("down is a no-op", () => {
171
+ preserveHeartbeatEnabledForExistingWorkspacesMigration.run(
172
+ workspaceDir,
173
+ UPGRADE_CTX,
174
+ );
175
+ const before = readFileSync(configPath, "utf-8");
176
+
177
+ preserveHeartbeatEnabledForExistingWorkspacesMigration.down(workspaceDir);
178
+
179
+ expect(readFileSync(configPath, "utf-8")).toBe(before);
180
+ });
181
+ });
@@ -0,0 +1,174 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
11
+
12
+ import { upgradeQualityProfileToOpus48Migration } from "../workspace/migrations/103-upgrade-quality-profile-to-opus-4-8.js";
13
+
14
+ let workspaceDir: string;
15
+
16
+ function writeConfig(data: Record<string, unknown>): void {
17
+ writeFileSync(
18
+ join(workspaceDir, "config.json"),
19
+ JSON.stringify(data, null, 2) + "\n",
20
+ );
21
+ }
22
+
23
+ function readConfig(): Record<string, unknown> {
24
+ return JSON.parse(readFileSync(join(workspaceDir, "config.json"), "utf-8"));
25
+ }
26
+
27
+ function readProfiles(): Record<string, Record<string, unknown>> {
28
+ const llm = readConfig().llm as Record<string, unknown>;
29
+ return llm.profiles as Record<string, Record<string, unknown>>;
30
+ }
31
+
32
+ beforeEach(() => {
33
+ workspaceDir = join(
34
+ tmpdir(),
35
+ `vellum-migration-103-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
36
+ );
37
+ mkdirSync(workspaceDir, { recursive: true });
38
+ });
39
+
40
+ afterEach(() => {
41
+ if (existsSync(workspaceDir)) {
42
+ rmSync(workspaceDir, { recursive: true, force: true });
43
+ }
44
+ });
45
+
46
+ describe("103-upgrade-quality-profile-to-opus-4-8 migration", () => {
47
+ test("no-op when config.json does not exist", () => {
48
+ upgradeQualityProfileToOpus48Migration.run(workspaceDir);
49
+ expect(existsSync(join(workspaceDir, "config.json"))).toBe(false);
50
+ });
51
+
52
+ test("no-op when config has no llm.profiles", () => {
53
+ const original = { llm: { default: { provider: "anthropic" } } };
54
+ writeConfig(original);
55
+ upgradeQualityProfileToOpus48Migration.run(workspaceDir);
56
+ expect(readConfig()).toEqual(original);
57
+ });
58
+
59
+ test("gracefully handles invalid JSON", () => {
60
+ writeFileSync(join(workspaceDir, "config.json"), "not-valid-json");
61
+ upgradeQualityProfileToOpus48Migration.run(workspaceDir);
62
+ expect(readFileSync(join(workspaceDir, "config.json"), "utf-8")).toBe(
63
+ "not-valid-json",
64
+ );
65
+ });
66
+
67
+ test("upgrades quality-optimized from claude-fable-5", () => {
68
+ writeConfig({
69
+ llm: {
70
+ profiles: {
71
+ "quality-optimized": {
72
+ provider: "anthropic",
73
+ model: "claude-fable-5",
74
+ label: "Quality",
75
+ },
76
+ },
77
+ },
78
+ });
79
+ upgradeQualityProfileToOpus48Migration.run(workspaceDir);
80
+ const profile = readProfiles()["quality-optimized"]!;
81
+ expect(profile.model).toBe("claude-opus-4-8");
82
+ expect(profile.label).toBe("Quality");
83
+ });
84
+
85
+ test("upgrades the managed profile but not the user-owned custom copy", () => {
86
+ writeConfig({
87
+ llm: {
88
+ profiles: {
89
+ "quality-optimized": {
90
+ provider: "anthropic",
91
+ model: "claude-fable-5",
92
+ },
93
+ "custom-quality-optimized": {
94
+ provider: "anthropic",
95
+ model: "claude-fable-5",
96
+ },
97
+ },
98
+ },
99
+ });
100
+ upgradeQualityProfileToOpus48Migration.run(workspaceDir);
101
+ const profiles = readProfiles();
102
+ expect(profiles["quality-optimized"]!.model).toBe("claude-opus-4-8");
103
+ expect(profiles["custom-quality-optimized"]!.model).toBe("claude-fable-5");
104
+ });
105
+
106
+ test("upgrades OpenRouter-prefixed model ids", () => {
107
+ writeConfig({
108
+ llm: {
109
+ profiles: {
110
+ "quality-optimized": {
111
+ provider: "openrouter",
112
+ model: "anthropic/claude-fable-5",
113
+ },
114
+ },
115
+ },
116
+ });
117
+ upgradeQualityProfileToOpus48Migration.run(workspaceDir);
118
+ expect(readProfiles()["quality-optimized"]!.model).toBe(
119
+ "anthropic/claude-opus-4.8",
120
+ );
121
+ });
122
+
123
+ test("leaves user-customized models untouched", () => {
124
+ const original = {
125
+ llm: {
126
+ profiles: {
127
+ "quality-optimized": { provider: "openai", model: "gpt-5.4" },
128
+ },
129
+ },
130
+ };
131
+ writeConfig(original);
132
+ upgradeQualityProfileToOpus48Migration.run(workspaceDir);
133
+ expect(readConfig()).toEqual(original);
134
+ });
135
+
136
+ test("leaves non-quality profiles untouched", () => {
137
+ writeConfig({
138
+ llm: {
139
+ profiles: {
140
+ balanced: { provider: "anthropic", model: "claude-fable-5" },
141
+ "quality-optimized": {
142
+ provider: "anthropic",
143
+ model: "claude-fable-5",
144
+ },
145
+ },
146
+ },
147
+ });
148
+ upgradeQualityProfileToOpus48Migration.run(workspaceDir);
149
+ const profiles = readProfiles();
150
+ expect(profiles["balanced"]!.model).toBe("claude-fable-5");
151
+ expect(profiles["quality-optimized"]!.model).toBe("claude-opus-4-8");
152
+ });
153
+
154
+ test("idempotency: no-op on already-upgraded config (no writes)", () => {
155
+ writeConfig({
156
+ llm: {
157
+ profiles: {
158
+ "quality-optimized": {
159
+ provider: "anthropic",
160
+ model: "claude-opus-4-8",
161
+ },
162
+ },
163
+ },
164
+ });
165
+ const beforeContent = readFileSync(
166
+ join(workspaceDir, "config.json"),
167
+ "utf-8",
168
+ );
169
+ upgradeQualityProfileToOpus48Migration.run(workspaceDir);
170
+ expect(readFileSync(join(workspaceDir, "config.json"), "utf-8")).toBe(
171
+ beforeContent,
172
+ );
173
+ });
174
+ });
@@ -27,6 +27,17 @@ export class CompactionCircuit {
27
27
  readonly conversationId: string;
28
28
  consecutiveCompactionFailures = 0;
29
29
  compactionCircuitOpenUntil: number | null = null;
30
+ /**
31
+ * Estimated input tokens immediately after the most recent compaction pass,
32
+ * or `null` before any pass this process. The budget gate's regrowth
33
+ * hysteresis reads it: if the history has not grown by at least
34
+ * `MIN_REGROWTH` tokens since this watermark, the previous pass already
35
+ * proved it cannot free more, so re-compacting would only thrash. Lives here
36
+ * because its lifetime must match the per-conversation circuit (the loop owns
37
+ * one circuit per conversation). Reset on process restart, the intended "one
38
+ * free retry after restart" behavior the failure counter already has.
39
+ */
40
+ lastPostCompactionEstimate: number | null = null;
30
41
 
31
42
  constructor(conversationId: string) {
32
43
  this.conversationId = conversationId;