@vellumai/assistant 0.8.12 → 0.9.0-staging.1

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 (414) 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/openapi.yaml +6935 -5545
  15. package/package.json +8 -4
  16. package/scripts/generate-openapi.ts +66 -114
  17. package/src/__tests__/access-request-seed-content-blocks.test.ts +213 -0
  18. package/src/__tests__/adaptive-thinking-repair.test.ts +32 -3
  19. package/src/__tests__/agent-loop-output-hooks.test.ts +183 -0
  20. package/src/__tests__/agent-loop-regrowth-guard.test.ts +506 -0
  21. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  22. package/src/__tests__/agent-wake-override-profile.test.ts +77 -0
  23. package/src/__tests__/app-dir-path-guard.test.ts +27 -3
  24. package/src/__tests__/approval-cascade.test.ts +0 -5
  25. package/src/__tests__/approval-routes-http.test.ts +91 -0
  26. package/src/__tests__/assistant-stream-state.test.ts +107 -0
  27. package/src/__tests__/browser-fill-credential.test.ts +3 -3
  28. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -1
  29. package/src/__tests__/compaction-events.test.ts +63 -7
  30. package/src/__tests__/compaction-trail-store.test.ts +74 -1
  31. package/src/__tests__/compaction.benchmark.test.ts +63 -41
  32. package/src/__tests__/compactor-low-watermark-cut.test.ts +349 -0
  33. package/src/__tests__/context-window-manager-compact-retry.test.ts +64 -0
  34. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -5
  35. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -5
  36. package/src/__tests__/conversation-history-web-search.test.ts +7 -0
  37. package/src/__tests__/conversation-process-callsite.test.ts +0 -5
  38. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -5
  39. package/src/__tests__/conversation-queue.test.ts +0 -5
  40. package/src/__tests__/conversation-slash-queue.test.ts +0 -5
  41. package/src/__tests__/conversation-slash-unknown.test.ts +0 -5
  42. package/src/__tests__/conversation-speed-override.test.ts +0 -5
  43. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  44. package/src/__tests__/conversation-usage.test.ts +2 -0
  45. package/src/__tests__/conversation-workspace-injection.test.ts +0 -5
  46. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -5
  47. package/src/__tests__/credential-broker-browser-fill.test.ts +2 -2
  48. package/src/__tests__/credential-broker-server-use.test.ts +2 -2
  49. package/src/__tests__/credential-broker.test.ts +1 -1
  50. package/src/__tests__/credential-prompt-route.test.ts +417 -0
  51. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  52. package/src/__tests__/credential-vault.test.ts +37 -0
  53. package/src/__tests__/db-schedule-syntax-migration.test.ts +24 -0
  54. package/src/__tests__/dynamic-page-surface.test.ts +125 -0
  55. package/src/__tests__/empty-state-greeting-cache.test.ts +94 -0
  56. package/src/__tests__/gateway-flag-listener.test.ts +24 -7
  57. package/src/__tests__/guardian-action-sweep.test.ts +56 -219
  58. package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
  59. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  60. package/src/__tests__/list-messages-hidden-metadata.test.ts +99 -0
  61. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +87 -1
  62. package/src/__tests__/llm-resolver.test.ts +115 -0
  63. package/src/__tests__/managed-profile-guard.test.ts +6 -5
  64. package/src/__tests__/max-tokens-continue-hook.test.ts +184 -0
  65. package/src/__tests__/media-generate-image.test.ts +20 -9
  66. package/src/__tests__/model-intents.test.ts +1 -1
  67. package/src/__tests__/normalize-onboarding.test.ts +26 -0
  68. package/src/__tests__/notification-decision-strategy.test.ts +4 -2
  69. package/src/__tests__/notification-telegram-adapter.test.ts +21 -3
  70. package/src/__tests__/pending-interactions-resolved-event.test.ts +62 -0
  71. package/src/__tests__/post-turn-tool-result-truncation.test.ts +72 -18
  72. package/src/__tests__/require-fresh-approval.test.ts +425 -1
  73. package/src/__tests__/runtime-events-sse-parity.test.ts +2 -0
  74. package/src/__tests__/schedule-routes-workflow-validation.test.ts +408 -0
  75. package/src/__tests__/schedule-routes.test.ts +257 -4
  76. package/src/__tests__/schedule-store.test.ts +60 -0
  77. package/src/__tests__/schedule-tools.test.ts +247 -2
  78. package/src/__tests__/skill-execute-input.test.ts +85 -0
  79. package/src/__tests__/skill-secret-handling-guard.test.ts +21 -20
  80. package/src/__tests__/skills.test.ts +3 -3
  81. package/src/__tests__/slack-app-setup-skill-regression.test.ts +1 -1
  82. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  83. package/src/__tests__/subagent-tool-gate-mode.test.ts +547 -0
  84. package/src/__tests__/system-prompt.test.ts +1 -1
  85. package/src/__tests__/task-progress-nudge-hook.test.ts +372 -0
  86. package/src/__tests__/task-scheduler.test.ts +299 -0
  87. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +209 -0
  88. package/src/__tests__/tool-result-spool.test.ts +3 -1
  89. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +181 -0
  90. package/src/__tests__/workspace-migration-103-upgrade-quality-profile-to-opus-4-8.test.ts +174 -0
  91. package/src/agent/compaction-circuit.ts +11 -0
  92. package/src/agent/loop.ts +181 -12
  93. package/src/api/constants/call-sites.ts +12 -0
  94. package/src/api/events/assistant-thinking-delta.ts +10 -0
  95. package/src/api/events/usage-update.ts +7 -0
  96. package/src/api/index.ts +4 -1
  97. package/src/api/responses/memory-v3-selection-log.ts +18 -11
  98. package/src/approvals/approval-primitive.ts +2 -2
  99. package/src/background-wake/background-wake-routes.test.ts +5 -2
  100. package/src/bundler/compiler-tools.ts +1 -1
  101. package/src/calls/call-domain.ts +1 -1
  102. package/src/calls/guardian-action-sweep.ts +16 -93
  103. package/src/cli/AGENTS.md +4 -0
  104. package/src/cli/commands/__tests__/schedules.test.ts +430 -1
  105. package/src/cli/commands/credentials.ts +28 -24
  106. package/src/cli/commands/image-generation.ts +23 -9
  107. package/src/cli/commands/notifications.ts +1 -1
  108. package/src/cli/commands/plugins.ts +89 -46
  109. package/src/cli/commands/schedules.ts +384 -11
  110. package/src/cli/lib/__tests__/inspect-plugin.test.ts +69 -5
  111. package/src/cli/lib/__tests__/install-from-github.test.ts +15 -0
  112. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +81 -4
  113. package/src/cli/lib/inspect-plugin.ts +62 -1
  114. package/src/cli/lib/install-from-github.ts +52 -5
  115. package/src/cli/lib/upgrade-plugin.ts +18 -0
  116. package/src/config/__tests__/workflows-schema.test.ts +60 -0
  117. package/src/config/bundled-skills/acp/SKILL.md +2 -2
  118. package/src/config/bundled-skills/image-studio/SKILL.md +66 -19
  119. package/src/config/bundled-skills/image-studio/TOOLS.json +1 -6
  120. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +22 -3
  121. package/src/config/bundled-skills/personal-page/SKILL.md +57 -0
  122. package/src/config/bundled-skills/personal-page/TOOLS.json +27 -0
  123. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +17 -0
  124. package/src/config/bundled-skills/schedule/SKILL.md +7 -2
  125. package/src/config/bundled-skills/schedule/TOOLS.json +48 -4
  126. package/src/config/bundled-skills/workflows/SKILL.md +214 -0
  127. package/src/config/bundled-skills/workflows/TOOLS.json +84 -0
  128. package/src/config/bundled-skills/workflows/tools/manage-workflows.ts +12 -0
  129. package/src/config/bundled-skills/workflows/tools/run-workflow.ts +12 -0
  130. package/src/config/bundled-tool-registry.ts +14 -2
  131. package/src/config/call-site-defaults.ts +5 -0
  132. package/src/config/feature-flag-registry.json +12 -4
  133. package/src/config/llm-context-resolution.ts +8 -0
  134. package/src/config/llm-resolver.ts +30 -0
  135. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +22 -0
  136. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +16 -0
  137. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +77 -0
  138. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +57 -0
  139. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +28 -0
  140. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +234 -0
  141. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +48 -0
  142. package/src/config/preloaded-apps/personal-page/src/components/media.ts +16 -0
  143. package/src/config/preloaded-apps/personal-page/src/index.html +20 -0
  144. package/src/config/preloaded-apps/personal-page/src/main.tsx +7 -0
  145. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +82 -0
  146. package/src/config/preloaded-apps/personal-page/src/styles.css +759 -0
  147. package/src/config/schema.ts +2 -0
  148. package/src/config/schemas/call-site-catalog.ts +7 -0
  149. package/src/config/schemas/heartbeat.ts +4 -1
  150. package/src/config/schemas/llm.ts +33 -26
  151. package/src/config/schemas/memory-retrospective.ts +19 -0
  152. package/src/config/schemas/platform.ts +8 -0
  153. package/src/config/schemas/services.ts +5 -2
  154. package/src/config/schemas/workflows.ts +42 -0
  155. package/src/config/skills.ts +3 -3
  156. package/src/context/compactor.ts +273 -39
  157. package/src/context/post-turn-tool-result-truncation.ts +23 -6
  158. package/src/context/tool-result-spool.ts +12 -17
  159. package/src/credential-execution/executable-discovery.ts +1 -1
  160. package/src/credential-execution/process-manager.ts +37 -3
  161. package/src/credential-execution/prompted-credential.ts +205 -0
  162. package/src/daemon/conversation-agent-loop-handlers.ts +14 -0
  163. package/src/daemon/conversation-process.ts +11 -2
  164. package/src/daemon/conversation-surfaces.ts +75 -0
  165. package/src/daemon/conversation-tool-setup.ts +103 -26
  166. package/src/daemon/conversation-usage.ts +2 -0
  167. package/src/daemon/conversation.ts +115 -11
  168. package/src/daemon/handlers/shared.ts +26 -14
  169. package/src/daemon/host-cu-proxy.ts +15 -12
  170. package/src/daemon/host-file-proxy.ts +15 -12
  171. package/src/daemon/host-transfer-proxy.ts +30 -24
  172. package/src/daemon/lifecycle.ts +40 -3
  173. package/src/daemon/message-protocol.ts +3 -0
  174. package/src/daemon/message-types/messages.ts +2 -10
  175. package/src/daemon/message-types/workflows.ts +49 -0
  176. package/src/daemon/parse-actual-tokens-from-error.test.ts +62 -1
  177. package/src/daemon/parse-actual-tokens-from-error.ts +43 -4
  178. package/src/daemon/process-message.ts +6 -0
  179. package/src/daemon/tool-setup-types.ts +57 -0
  180. package/src/daemon/wake-conversation-ops.ts +18 -0
  181. package/src/heartbeat/heartbeat-run-store.ts +8 -2
  182. package/src/home/feed-types.ts +1 -1
  183. package/src/ipc/gateway-flag-listener.ts +28 -6
  184. package/src/mcp/mcp-auth-state.ts +8 -20
  185. package/src/media/__tests__/image-models.test.ts +57 -0
  186. package/src/media/image-models.ts +66 -0
  187. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +38 -0
  188. package/src/memory/__tests__/find-most-recent-retrospective-for.test.ts +12 -2
  189. package/src/memory/__tests__/memory-retrospective-job.test.ts +911 -34
  190. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +227 -5
  191. package/src/memory/__tests__/memory-retrospective-state.test.ts +195 -0
  192. package/src/memory/__tests__/preloaded-apps.test.ts +85 -0
  193. package/src/memory/auto-analysis-enqueue.ts +14 -1
  194. package/src/memory/compaction-log-store-clickhouse.ts +6 -4
  195. package/src/memory/conversation-crud.ts +9 -2
  196. package/src/memory/conversation-disk-view.ts +1 -1
  197. package/src/memory/conversation-queries.ts +22 -7
  198. package/src/memory/db-init.ts +20 -0
  199. package/src/memory/db-maintenance.ts +16 -0
  200. package/src/memory/embedding-runtime-manager.ts +1 -1
  201. package/src/memory/llm-request-log-source-clickhouse.ts +112 -14
  202. package/src/memory/llm-request-log-source-local.ts +19 -1
  203. package/src/memory/llm-request-log-source.ts +35 -6
  204. package/src/memory/llm-request-log-store.ts +90 -2
  205. package/src/memory/memory-retrospective-constants.ts +9 -0
  206. package/src/memory/memory-retrospective-enqueue.ts +3 -6
  207. package/src/memory/memory-retrospective-fork-boundary.ts +94 -0
  208. package/src/memory/memory-retrospective-job.ts +500 -208
  209. package/src/memory/memory-retrospective-startup-cleanup.ts +97 -19
  210. package/src/memory/memory-retrospective-state.ts +85 -2
  211. package/src/memory/migrations/281-memory-retrospective-remembered-log.ts +40 -0
  212. package/src/memory/migrations/282-schedule-inference-profile.test.ts +77 -0
  213. package/src/memory/migrations/282-schedule-inference-profile.ts +26 -0
  214. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.test.ts +102 -0
  215. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.ts +53 -0
  216. package/src/memory/migrations/284-workflow-runs.ts +51 -0
  217. package/src/memory/migrations/285-schedule-workflow-mode.ts +26 -0
  218. package/src/memory/migrations/286-workflow-run-trust.ts +27 -0
  219. package/src/memory/migrations/287-conversation-origin-channel-index.ts +15 -0
  220. package/src/memory/migrations/288-backfill-origin-channel-from-bindings.ts +43 -0
  221. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +115 -0
  222. package/src/memory/migrations/290-schedule-capabilities.test.ts +77 -0
  223. package/src/memory/migrations/290-schedule-capabilities.ts +25 -0
  224. package/src/memory/migrations/__tests__/281-memory-retrospective-remembered-log.test.ts +96 -0
  225. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +571 -0
  226. package/src/memory/migrations/index.ts +10 -0
  227. package/src/memory/preloaded-apps.ts +116 -0
  228. package/src/memory/schema/infrastructure.ts +4 -0
  229. package/src/memory/schema/memory-core.ts +4 -0
  230. package/src/memory/v2/__tests__/concept-page-frontmatter-schema.test.ts +45 -0
  231. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +11 -7
  232. package/src/memory/v2/__tests__/page-store.test.ts +13 -2
  233. package/src/memory/v2/__tests__/qdrant.test.ts +24 -0
  234. package/src/memory/v2/frontmatter-sweep.ts +7 -6
  235. package/src/memory/v2/page-store.ts +4 -3
  236. package/src/memory/v2/qdrant.ts +42 -3
  237. package/src/memory/v2/types.ts +16 -10
  238. package/src/messaging/draft-store.ts +1 -1
  239. package/src/notifications/access-request-copy.ts +200 -113
  240. package/src/notifications/adapters/slack.ts +250 -111
  241. package/src/notifications/adapters/telegram.ts +7 -44
  242. package/src/notifications/approval-card-builder.ts +93 -0
  243. package/src/notifications/broadcaster.ts +74 -0
  244. package/src/notifications/conversation-pairing.ts +8 -6
  245. package/src/notifications/copy-composer.ts +32 -26
  246. package/src/notifications/decision-engine.ts +59 -7
  247. package/src/notifications/guardian-question-mode.ts +145 -155
  248. package/src/notifications/home-feed-side-effect.ts +1 -1
  249. package/src/notifications/notification-utils.ts +66 -0
  250. package/src/notifications/signal.ts +6 -0
  251. package/src/notifications/tool-approval-copy.ts +142 -0
  252. package/src/notifications/types.ts +19 -0
  253. package/src/permissions/threshold.ts +11 -0
  254. package/src/plugin-api/types.ts +16 -4
  255. package/src/plugins/defaults/compaction/window-manager.ts +44 -0
  256. package/src/plugins/defaults/index.ts +46 -0
  257. package/src/plugins/defaults/max-tokens-continue/continue-state-store.ts +53 -0
  258. package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +80 -0
  259. package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +20 -0
  260. package/src/plugins/defaults/max-tokens-continue/package.json +14 -0
  261. package/src/plugins/defaults/memory-retrieval/hooks/__tests__/user-prompt-submit.test.ts +37 -0
  262. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +29 -1
  263. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +8 -3
  264. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +4 -2
  265. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +28 -18
  266. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +67 -0
  267. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +122 -22
  268. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +2 -0
  269. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +63 -1
  270. package/src/plugins/defaults/memory-v3-shadow/injector.ts +61 -18
  271. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +1 -1
  272. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +39 -10
  273. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +34 -1
  274. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +112 -47
  275. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +78 -15
  276. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +206 -0
  277. package/src/plugins/defaults/task-progress-nudge/package.json +15 -0
  278. package/src/prompts/__tests__/system-prompt.test.ts +100 -1
  279. package/src/prompts/__tests__/task-progress-hint-section.test.ts +5 -7
  280. package/src/prompts/normalize-onboarding.ts +2 -0
  281. package/src/prompts/persona-resolver.ts +3 -0
  282. package/src/prompts/system-prompt.ts +51 -2
  283. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +3 -1
  284. package/src/prompts/templates/system-sections.ts +8 -3
  285. package/src/providers/call-site-routing.ts +6 -3
  286. package/src/providers/fireworks/client.ts +3 -0
  287. package/src/providers/inference/auth.ts +52 -46
  288. package/src/providers/model-intents.ts +2 -2
  289. package/src/providers/openai/__tests__/coerce-object-args.test.ts +105 -0
  290. package/src/providers/openai/chat-completions-provider.ts +47 -9
  291. package/src/providers/openai/coerce-object-args.ts +104 -0
  292. package/src/providers/retry.ts +8 -5
  293. package/src/providers/types.ts +10 -0
  294. package/src/runtime/__tests__/agent-wake.test.ts +629 -7
  295. package/src/runtime/access-request-helper.ts +16 -9
  296. package/src/runtime/agent-wake.ts +302 -51
  297. package/src/runtime/assistant-stream-state.ts +141 -8
  298. package/src/runtime/background-job-runner.ts +9 -0
  299. package/src/runtime/channel-approval-types.ts +1 -0
  300. package/src/runtime/guardian-action-message-composer.ts +0 -54
  301. package/src/runtime/http-server.ts +6 -14
  302. package/src/runtime/http-types.ts +0 -1
  303. package/src/runtime/message-composer-types.ts +0 -9
  304. package/src/runtime/middleware/__tests__/rate-limiter.test.ts +63 -0
  305. package/src/runtime/middleware/auth.ts +27 -3
  306. package/src/runtime/middleware/rate-limiter.ts +28 -1
  307. package/src/runtime/migrations/vbundle-builder.ts +6 -5
  308. package/src/runtime/pending-interactions.ts +20 -1
  309. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +232 -173
  310. package/src/runtime/routes/__tests__/plugins-routes.test.ts +18 -0
  311. package/src/runtime/routes/__tests__/retrospective-routes.test.ts +436 -0
  312. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +11 -0
  313. package/src/runtime/routes/approval-routes.ts +35 -8
  314. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -1
  315. package/src/runtime/routes/btw-routes.ts +37 -1
  316. package/src/runtime/routes/channel-route-shared.ts +3 -1
  317. package/src/runtime/routes/consolidation-routes.ts +17 -13
  318. package/src/runtime/routes/conversation-compaction-routes.ts +159 -119
  319. package/src/runtime/routes/conversation-list-routes.ts +41 -4
  320. package/src/runtime/routes/conversation-query-routes.ts +196 -11
  321. package/src/runtime/routes/conversation-routes.ts +15 -1
  322. package/src/runtime/routes/credential-prompt-routes.ts +39 -17
  323. package/src/runtime/routes/empty-state-greeting-cache.ts +65 -0
  324. package/src/runtime/routes/heartbeat-routes.ts +18 -13
  325. package/src/runtime/routes/image-generation-routes.ts +20 -2
  326. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +26 -1
  327. package/src/runtime/routes/index.ts +6 -0
  328. package/src/runtime/routes/log-export/AGENTS.md +1 -1
  329. package/src/runtime/routes/log-export/workspace-allowlist.ts +1 -1
  330. package/src/runtime/routes/migration-routes.ts +5 -9
  331. package/src/runtime/routes/plugins-routes.ts +26 -0
  332. package/src/runtime/routes/ps-routes.ts +10 -8
  333. package/src/runtime/routes/retrospective-routes.ts +235 -0
  334. package/src/runtime/routes/runs-pagination.ts +75 -0
  335. package/src/runtime/routes/schedule-routes.ts +367 -59
  336. package/src/runtime/routes/sounds-config-routes.ts +239 -0
  337. package/src/runtime/routes/surface-action-routes.ts +84 -4
  338. package/src/runtime/routes/workflow-routes.test.ts +372 -0
  339. package/src/runtime/routes/workflow-routes.ts +363 -0
  340. package/src/runtime/routes/workspace-routes.test.ts +61 -1
  341. package/src/runtime/routes/workspace-routes.ts +26 -1
  342. package/src/runtime/services/__tests__/analyze-conversation.test.ts +38 -0
  343. package/src/runtime/services/analyze-conversation.ts +26 -13
  344. package/src/schedule/inference-profile.ts +28 -0
  345. package/src/schedule/schedule-store.ts +122 -4
  346. package/src/schedule/scheduler-types.ts +6 -0
  347. package/src/schedule/scheduler.ts +96 -0
  348. package/src/security/secret-allowlist.ts +1 -1
  349. package/src/skills/path-classifier.ts +1 -1
  350. package/src/tools/browser/browser-execution.ts +9 -11
  351. package/src/tools/credentials/broker.ts +4 -4
  352. package/src/tools/credentials/vault.ts +26 -137
  353. package/src/tools/executor.ts +69 -0
  354. package/src/tools/flag-gated-tools.test.ts +76 -0
  355. package/src/tools/permission-checker.ts +8 -1
  356. package/src/tools/registry.ts +51 -0
  357. package/src/tools/schedule/create.ts +77 -1
  358. package/src/tools/schedule/list.ts +1 -0
  359. package/src/tools/schedule/update.ts +73 -1
  360. package/src/tools/skills/execute.ts +56 -0
  361. package/src/tools/terminal/shell.ts +1 -1
  362. package/src/tools/ui-surface/definitions.ts +48 -2
  363. package/src/tools/workflows/manage-workflows.ts +183 -0
  364. package/src/tools/workflows/run-workflow.test.ts +442 -0
  365. package/src/tools/workflows/run-workflow.ts +88 -0
  366. package/src/types/onboarding-context.ts +2 -0
  367. package/src/usage/attribution.ts +24 -0
  368. package/src/util/canonicalize-identity.ts +12 -3
  369. package/src/util/platform.ts +17 -17
  370. package/src/watcher/__tests__/engine.test.ts +24 -0
  371. package/src/watcher/__tests__/telemetry.test.ts +135 -0
  372. package/src/watcher/engine.ts +7 -0
  373. package/src/watcher/telemetry.ts +74 -0
  374. package/src/workflows/capabilities.test.ts +365 -0
  375. package/src/workflows/capabilities.ts +359 -0
  376. package/src/workflows/deterministic-stringify.ts +27 -0
  377. package/src/workflows/engine-integration.test.ts +656 -0
  378. package/src/workflows/engine.test.ts +1144 -0
  379. package/src/workflows/engine.ts +1078 -0
  380. package/src/workflows/fanout-load.test.ts +168 -0
  381. package/src/workflows/journal-store.test.ts +369 -0
  382. package/src/workflows/journal-store.ts +470 -0
  383. package/src/workflows/leaf-runner.test.ts +704 -0
  384. package/src/workflows/leaf-runner.ts +589 -0
  385. package/src/workflows/library.test.ts +134 -0
  386. package/src/workflows/library.ts +124 -0
  387. package/src/workflows/run-manager.test.ts +711 -0
  388. package/src/workflows/run-manager.ts +593 -0
  389. package/src/workflows/sandbox-escape.test.ts +339 -0
  390. package/src/workflows/sandbox.test.ts +251 -0
  391. package/src/workflows/sandbox.ts +447 -0
  392. package/src/workspace/adaptive-thinking-repair.ts +33 -11
  393. package/src/workspace/migrations/021-move-signals-to-workspace.ts +1 -1
  394. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +1 -1
  395. package/src/workspace/migrations/026-backfill-install-meta.ts +1 -1
  396. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +2 -1
  397. package/src/workspace/migrations/031-drop-user-md.ts +1 -4
  398. package/src/workspace/migrations/048-remove-workspace-hooks.ts +1 -1
  399. package/src/workspace/migrations/056-release-notes-inference-profile-reordering.ts +5 -2
  400. package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +1 -1
  401. package/src/workspace/migrations/082-backfill-managed-profile-labels.ts +8 -2
  402. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +41 -14
  403. package/src/workspace/migrations/102-preserve-heartbeat-enabled-for-existing-workspaces.ts +69 -0
  404. package/src/workspace/migrations/103-upgrade-quality-profile-to-opus-4-8.ts +83 -0
  405. package/src/workspace/migrations/104-recheck-adaptive-thinking-model-implied-anthropic.ts +133 -0
  406. package/src/workspace/migrations/registry.ts +6 -0
  407. package/src/workspace/migrations/runner.ts +1 -1
  408. package/tsconfig.json +1 -1
  409. package/src/__tests__/guardian-action-copy-generator.test.ts +0 -200
  410. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -579
  411. package/src/__tests__/guardian-action-store.test.ts +0 -106
  412. package/src/daemon/guardian-action-generators.ts +0 -71
  413. package/src/memory/guardian-action-store.ts +0 -484
  414. package/src/runtime/guardian-action-grant-minter.ts +0 -150
@@ -45,12 +45,14 @@ import {
45
45
  } from "../memory/canonical-guardian-store.js";
46
46
  import { getDb } from "../memory/db-connection.js";
47
47
  import { initializeDb } from "../memory/db-init.js";
48
+ import { processGuardianDecision } from "../runtime/guardian-action-service.js";
48
49
  import { GUARDIAN_DECISION_ACTIONS } from "../runtime/guardian-decision-types.js";
49
50
  import {
50
51
  type GuardianReplyContext,
51
52
  routeGuardianReply,
52
53
  } from "../runtime/guardian-reply-router.js";
53
54
  import * as pendingInteractions from "../runtime/pending-interactions.js";
55
+ import { parseCallbackData } from "../runtime/routes/channel-route-shared.js";
54
56
  import { listGuardianDecisionPrompts } from "../runtime/routes/guardian-action-routes.js";
55
57
 
56
58
  initializeDb();
@@ -168,6 +170,12 @@ describe("routing invariant: all decision paths reference applyCanonicalGuardian
168
170
  path: "runtime/routes/guardian-action-routes.ts",
169
171
  symbols: ["processGuardianDecision"],
170
172
  },
173
+ // Surface action route handler (Vellum in-app card buttons) — intercepts
174
+ // apr:* action IDs and routes through processGuardianDecision
175
+ {
176
+ path: "runtime/routes/surface-action-routes.ts",
177
+ symbols: ["processGuardianDecision"],
178
+ },
171
179
  // Shared service where processGuardianDecision is defined — must route
172
180
  // through the canonical primitive to complete the chain:
173
181
  // entrypoint → processGuardianDecision → applyCanonicalGuardianDecision
@@ -1941,3 +1949,133 @@ describe("routing invariant: kind-specific action sets in prompt mapping", () =>
1941
1949
  expect(actionIds).not.toContain("approve_conversation");
1942
1950
  });
1943
1951
  });
1952
+
1953
+ // ===========================================================================
1954
+ // SECTION 11: Surface action → access request approval routing
1955
+ // ===========================================================================
1956
+
1957
+ describe("routing invariant: surface action apr:* buttons route through canonical primitive", () => {
1958
+ beforeEach(() => resetTables());
1959
+
1960
+ test("parseCallbackData extracts requestId and action from apr: action IDs", () => {
1961
+ const result = parseCallbackData(
1962
+ "apr:access-req-test-slack-U123-1700000000:approve_once",
1963
+ "vellum",
1964
+ );
1965
+ expect(result).not.toBeNull();
1966
+ expect(result!.requestId).toBe("access-req-test-slack-U123-1700000000");
1967
+ expect(result!.action).toBe("approve_once");
1968
+ expect(result!.source).toBe("vellum_surface");
1969
+ });
1970
+
1971
+ test("parseCallbackData returns vellum_surface source for vellum channel", () => {
1972
+ const result = parseCallbackData("apr:req-1:reject", "vellum");
1973
+ expect(result).not.toBeNull();
1974
+ expect(result!.source).toBe("vellum_surface");
1975
+ });
1976
+
1977
+ test("processGuardianDecision approves access_request from vellum surface", async () => {
1978
+ const requestId = `access-req-test-slack-U123-${Date.now()}`;
1979
+ createCanonicalGuardianRequest({
1980
+ id: requestId,
1981
+ kind: "access_request",
1982
+ sourceType: "channel",
1983
+ sourceChannel: "slack",
1984
+ conversationId: "conv-surface-test",
1985
+ requesterExternalUserId: "U123",
1986
+ requesterChatId: "D456",
1987
+ guardianExternalUserId: "guardian-1",
1988
+ guardianPrincipalId: TEST_PRINCIPAL_ID,
1989
+ toolName: "ingress_access_request",
1990
+ expiresAt: Date.now() + 60_000,
1991
+ });
1992
+
1993
+ const result = await processGuardianDecision({
1994
+ requestId,
1995
+ action: "approve_once",
1996
+ channel: "vellum",
1997
+ actorContext: {
1998
+ actorPrincipalId: TEST_PRINCIPAL_ID,
1999
+ guardianPrincipalId: TEST_PRINCIPAL_ID,
2000
+ },
2001
+ });
2002
+
2003
+ expect(result.ok).toBe(true);
2004
+ if (result.ok && "applied" in result) {
2005
+ expect(result.applied).toBe(true);
2006
+ }
2007
+
2008
+ const resolved = getCanonicalGuardianRequest(requestId);
2009
+ expect(resolved!.status).toBe("approved");
2010
+ });
2011
+
2012
+ test("processGuardianDecision denies access_request from vellum surface", async () => {
2013
+ const requestId = `access-req-test-slack-U789-${Date.now()}`;
2014
+ createCanonicalGuardianRequest({
2015
+ id: requestId,
2016
+ kind: "access_request",
2017
+ sourceType: "channel",
2018
+ sourceChannel: "slack",
2019
+ conversationId: "conv-surface-deny",
2020
+ requesterExternalUserId: "U789",
2021
+ requesterChatId: "D012",
2022
+ guardianExternalUserId: "guardian-1",
2023
+ guardianPrincipalId: TEST_PRINCIPAL_ID,
2024
+ toolName: "ingress_access_request",
2025
+ expiresAt: Date.now() + 60_000,
2026
+ });
2027
+
2028
+ const result = await processGuardianDecision({
2029
+ requestId,
2030
+ action: "reject",
2031
+ channel: "vellum",
2032
+ actorContext: {
2033
+ actorPrincipalId: TEST_PRINCIPAL_ID,
2034
+ guardianPrincipalId: TEST_PRINCIPAL_ID,
2035
+ },
2036
+ });
2037
+
2038
+ expect(result.ok).toBe(true);
2039
+ if (result.ok && "applied" in result) {
2040
+ expect(result.applied).toBe(true);
2041
+ }
2042
+
2043
+ const resolved = getCanonicalGuardianRequest(requestId);
2044
+ expect(resolved!.status).toBe("denied");
2045
+ });
2046
+
2047
+ test("principal mismatch rejects surface action decision", async () => {
2048
+ const requestId = `access-req-test-slack-U999-${Date.now()}`;
2049
+ createCanonicalGuardianRequest({
2050
+ id: requestId,
2051
+ kind: "access_request",
2052
+ sourceType: "channel",
2053
+ sourceChannel: "slack",
2054
+ conversationId: "conv-surface-mismatch",
2055
+ requesterExternalUserId: "U999",
2056
+ requesterChatId: "D345",
2057
+ guardianExternalUserId: "guardian-1",
2058
+ guardianPrincipalId: TEST_PRINCIPAL_ID,
2059
+ toolName: "ingress_access_request",
2060
+ expiresAt: Date.now() + 60_000,
2061
+ });
2062
+
2063
+ const result = await processGuardianDecision({
2064
+ requestId,
2065
+ action: "approve_once",
2066
+ channel: "vellum",
2067
+ actorContext: {
2068
+ actorPrincipalId: "wrong-principal",
2069
+ guardianPrincipalId: "wrong-principal",
2070
+ },
2071
+ });
2072
+
2073
+ expect(result.ok).toBe(true);
2074
+ if (result.ok && "applied" in result) {
2075
+ expect(result.applied).toBe(false);
2076
+ }
2077
+
2078
+ const req = getCanonicalGuardianRequest(requestId);
2079
+ expect(req!.status).toBe("pending");
2080
+ });
2081
+ });
@@ -64,7 +64,6 @@ mock.module("../../daemon/approval-generators.js", () => ({
64
64
  import type {
65
65
  ApprovalConversationGenerator,
66
66
  ApprovalCopyGenerator,
67
- GuardianActionCopyGenerator,
68
67
  MessageProcessor,
69
68
  } from "../../runtime/http-types.js";
70
69
  import {
@@ -111,7 +110,6 @@ export async function handleChannelInbound(
111
110
  _assistantId?: string,
112
111
  _approvalCopyGenerator?: ApprovalCopyGenerator,
113
112
  _approvalConversationGenerator?: ApprovalConversationGenerator,
114
- _guardianActionCopyGenerator?: GuardianActionCopyGenerator,
115
113
  ): Promise<Response> {
116
114
  const body = await req.json();
117
115
  return wrapHandler(() => _handleChannelInbound({ body }));
@@ -33,6 +33,10 @@ import {
33
33
  } from "../memory/conversation-crud.js";
34
34
  import { getDb } from "../memory/db-connection.js";
35
35
  import { initializeDb } from "../memory/db-init.js";
36
+ import {
37
+ MEMORY_RETROSPECTIVE_FORK_SOURCE,
38
+ MEMORY_RETROSPECTIVE_INSTRUCTION_KIND,
39
+ } from "../memory/memory-retrospective-constants.js";
36
40
  import { messages } from "../memory/schema.js";
37
41
  import { handleListMessages } from "../runtime/routes/conversation-routes.js";
38
42
 
@@ -334,4 +338,99 @@ describe("handleListMessages metadata.hidden filtering", () => {
334
338
  expect(latest.hasMore).toBe(false);
335
339
  expect(latest.oldestTimestamp).not.toBeNull();
336
340
  });
341
+
342
+ test("fork retrospective conversations expose the hidden instruction and keep the review as its own turn", async () => {
343
+ const conv = createConversation({
344
+ source: MEMORY_RETROSPECTIVE_FORK_SOURCE,
345
+ });
346
+ await addMessage(
347
+ conv.id,
348
+ "user",
349
+ JSON.stringify([{ type: "text", text: "source question" }]),
350
+ );
351
+ await addMessage(
352
+ conv.id,
353
+ "assistant",
354
+ JSON.stringify([{ type: "text", text: "source answer" }]),
355
+ );
356
+ await addMessage(
357
+ conv.id,
358
+ "user",
359
+ JSON.stringify([
360
+ { type: "text", text: "background memory pass instruction" },
361
+ ]),
362
+ {
363
+ metadata: { kind: MEMORY_RETROSPECTIVE_INSTRUCTION_KIND, hidden: true },
364
+ },
365
+ );
366
+ await addMessage(
367
+ conv.id,
368
+ "assistant",
369
+ JSON.stringify([{ type: "text", text: "retrospective review" }]),
370
+ );
371
+
372
+ const response = handleListMessages({
373
+ queryParams: { conversationId: conv.id },
374
+ });
375
+ const body = response as { messages: MessagePayload[] };
376
+
377
+ // The hidden instruction is shown, and because it sits between the two
378
+ // assistant rows they do NOT merge — the review stands as its own turn.
379
+ expect(body.messages.map((m) => m.role)).toEqual([
380
+ "user",
381
+ "assistant",
382
+ "user",
383
+ "assistant",
384
+ ]);
385
+ expect(plainText(body.messages[1])).toBe("source answer");
386
+ expect(plainText(body.messages[2])).toContain(
387
+ "background memory pass instruction",
388
+ );
389
+ expect(plainText(body.messages[3])).toBe("retrospective review");
390
+ });
391
+
392
+ test("non-fork conversations still hide the retrospective instruction (relaxation is fork-scoped)", async () => {
393
+ const conv = createConversation();
394
+ await addMessage(
395
+ conv.id,
396
+ "user",
397
+ JSON.stringify([{ type: "text", text: "source question" }]),
398
+ );
399
+ await addMessage(
400
+ conv.id,
401
+ "assistant",
402
+ JSON.stringify([{ type: "text", text: "source answer" }]),
403
+ );
404
+ await addMessage(
405
+ conv.id,
406
+ "user",
407
+ JSON.stringify([
408
+ { type: "text", text: "background memory pass instruction" },
409
+ ]),
410
+ {
411
+ metadata: { kind: MEMORY_RETROSPECTIVE_INSTRUCTION_KIND, hidden: true },
412
+ },
413
+ );
414
+ await addMessage(
415
+ conv.id,
416
+ "assistant",
417
+ JSON.stringify([{ type: "text", text: "retrospective review" }]),
418
+ );
419
+
420
+ const response = handleListMessages({
421
+ queryParams: { conversationId: conv.id },
422
+ });
423
+ const body = response as { messages: MessagePayload[] };
424
+
425
+ // Instruction stays hidden for non-retrospective conversations; the review
426
+ // (a normal assistant message) is still returned.
427
+ expect(
428
+ body.messages.some((m) =>
429
+ plainText(m).includes("background memory pass instruction"),
430
+ ),
431
+ ).toBe(false);
432
+ expect(
433
+ body.messages.some((m) => plainText(m).includes("retrospective review")),
434
+ ).toBe(true);
435
+ });
337
436
  });
@@ -134,7 +134,9 @@ describe("ClickHouseLlmRequestLogSource", () => {
134
134
  const call = recorder[0]!;
135
135
  const parsed = new URL(call.url);
136
136
  expect(parsed.searchParams.get("database")).toBe("default");
137
- expect(parsed.searchParams.get("param_assistant_id")).toBe("asst-fixture-001");
137
+ expect(parsed.searchParams.get("param_assistant_id")).toBe(
138
+ "asst-fixture-001",
139
+ );
138
140
  expect(parsed.searchParams.get("param_log_id")).toBe("log-1");
139
141
  expect(call.init?.method).toBe("POST");
140
142
  const auth = (call.init?.headers as Record<string, string>).Authorization;
@@ -224,6 +226,90 @@ describe("ClickHouseLlmRequestLogSource", () => {
224
226
  expect(body).not.toContain(`'msg-b'`);
225
227
  });
226
228
 
229
+ test("getRequestLogMetaById returns metadata without selecting either payload column", async () => {
230
+ // GIVEN a stored row in ClickHouse
231
+ const recorder: FakeFetchCall[] = [];
232
+ const metaRow = {
233
+ id: "log-1",
234
+ conversation_id: "conv-1",
235
+ message_id: "msg-1",
236
+ provider: "anthropic",
237
+ created_at: "1778465138786",
238
+ agent_loop_exit_reason: "no_tool_calls",
239
+ call_site: "compactionAgent",
240
+ };
241
+ const src = makeSource({
242
+ body: JSON.stringify(metaRow) + "\n",
243
+ recorder,
244
+ });
245
+
246
+ // WHEN looking it up by id
247
+ const row = await src.getRequestLogMetaById("log-1");
248
+
249
+ // THEN the metadata comes back without payload fields
250
+ expect(row).toEqual({
251
+ id: "log-1",
252
+ conversationId: "conv-1",
253
+ messageId: "msg-1",
254
+ provider: "anthropic",
255
+ createdAt: 1778465138786,
256
+ agentLoopExitReason: "no_tool_calls",
257
+ callSite: "compactionAgent",
258
+ });
259
+ // AND the query never selects the payload columns (a single request
260
+ // payload can be a full context window)
261
+ const body = String(recorder[0]!.init?.body ?? "");
262
+ expect(body).not.toContain("request_payload");
263
+ expect(body).not.toContain("response_payload");
264
+ });
265
+
266
+ test("getCompactionLogsBetween computes message count in SQL and skips the request payload", async () => {
267
+ // GIVEN compaction rows whose message count ClickHouse computed via
268
+ // JSONLength (Int64 arrives as a quoted string under JSONEachRow)
269
+ const recorder: FakeFetchCall[] = [];
270
+ const compactionRow = {
271
+ id: "log-1",
272
+ conversation_id: "conv-1",
273
+ message_id: "",
274
+ provider: "anthropic",
275
+ response_payload: '{"bar":2}',
276
+ request_message_count: "42",
277
+ created_at: "1778465138786",
278
+ agent_loop_exit_reason: "",
279
+ call_site: "compactionAgent",
280
+ };
281
+ const src = makeSource({
282
+ body:
283
+ JSON.stringify(compactionRow) +
284
+ "\n" +
285
+ JSON.stringify({
286
+ ...compactionRow,
287
+ id: "log-2",
288
+ request_message_count: null,
289
+ }) +
290
+ "\n",
291
+ recorder,
292
+ });
293
+
294
+ // WHEN querying the trail window
295
+ const rows = await src.getCompactionLogsBetween("conv-1", 100, 500);
296
+
297
+ // THEN each row carries the numeric message count (null when the
298
+ // payload had no known message array) and no request payload
299
+ expect(rows).toHaveLength(2);
300
+ expect(rows[0]!.requestMessageCount).toBe(42);
301
+ expect(rows[1]!.requestMessageCount).toBeNull();
302
+ expect(rows[0]!.responsePayload).toBe('{"bar":2}');
303
+ expect("requestPayload" in rows[0]!).toBe(false);
304
+ // AND the SQL only touches request_payload inside JSONLength —
305
+ // it is never selected as a column
306
+ const body = String(recorder[0]!.init?.body ?? "");
307
+ expect(body).toContain("JSONLength(request_payload");
308
+ expect(body.match(/request_payload/g)).toHaveLength(
309
+ body.match(/JSONLength\(request_payload/g)!.length,
310
+ );
311
+ });
312
+
227
313
  test("missing clickhouse:url credential surfaces a clear error", async () => {
228
314
  const src = new ClickHouseLlmRequestLogSource(DEFAULT_CONFIG, {
229
315
  resolveUrl: async () => null,
@@ -341,6 +341,97 @@ describe("resolveCallSiteConfig", () => {
341
341
  expect(resolved.verbosity).toBe("low");
342
342
  });
343
343
 
344
+ test("forceOverrideProfile floats the override profile above site profile and callsite fields for non-main call sites", () => {
345
+ const llm = LLMSchema.parse({
346
+ default: fullDefault,
347
+ profiles: {
348
+ active: { verbosity: "low" },
349
+ sitep: { effort: "none", speed: "fast" },
350
+ forced: {
351
+ model: "claude-haiku-4-5",
352
+ effort: "high",
353
+ thinking: { enabled: false },
354
+ },
355
+ },
356
+ callSites: {
357
+ memoryExtraction: { profile: "sitep", maxTokens: 1000 },
358
+ },
359
+ activeProfile: "active",
360
+ });
361
+ const resolved = resolveCallSiteConfig("memoryExtraction", llm, {
362
+ overrideProfile: "forced",
363
+ forceOverrideProfile: true,
364
+ });
365
+ // Forced override wins over both the site profile and the call-site
366
+ // override fields it touches.
367
+ expect(resolved.model).toBe("claude-haiku-4-5");
368
+ expect(resolved.effort).toBe("high");
369
+ expect(resolved.thinking.enabled).toBe(false);
370
+ // Call-site layers still win where the forced profile is silent.
371
+ expect(resolved.maxTokens).toBe(1000);
372
+ expect(resolved.speed).toBe("fast");
373
+ // Active profile still applies under everything.
374
+ expect(resolved.verbosity).toBe("low");
375
+ });
376
+
377
+ test("forceOverrideProfile absent leaves the override below callsite fields (unchanged precedence)", () => {
378
+ const llm = LLMSchema.parse({
379
+ default: fullDefault,
380
+ profiles: {
381
+ forced: { effort: "high" },
382
+ },
383
+ callSites: {
384
+ memoryExtraction: { effort: "none" },
385
+ },
386
+ });
387
+ const resolved = resolveCallSiteConfig("memoryExtraction", llm, {
388
+ overrideProfile: "forced",
389
+ });
390
+ expect(resolved.effort).toBe("none");
391
+ });
392
+
393
+ test("forceOverrideProfile with a missing profile reference falls through to unchanged precedence", () => {
394
+ const llm = LLMSchema.parse({
395
+ default: fullDefault,
396
+ profiles: {
397
+ sitep: { effort: "low", model: "claude-haiku-4-5" },
398
+ },
399
+ callSites: {
400
+ memoryExtraction: { profile: "sitep" },
401
+ },
402
+ });
403
+ const resolved = resolveCallSiteConfig("memoryExtraction", llm, {
404
+ overrideProfile: "nonexistent",
405
+ forceOverrideProfile: true,
406
+ });
407
+ // The missing reference is inert: site profile still wins.
408
+ expect(resolved.effort).toBe("low");
409
+ expect(resolved.model).toBe("claude-haiku-4-5");
410
+ });
411
+
412
+ test("forceOverrideProfile is a no-op for mainAgent (override already resolves on top)", () => {
413
+ const llm = LLMSchema.parse({
414
+ default: fullDefault,
415
+ profiles: {
416
+ active: { effort: "low" },
417
+ override: { effort: "high" },
418
+ },
419
+ callSites: {
420
+ mainAgent: { effort: "none" },
421
+ },
422
+ activeProfile: "active",
423
+ });
424
+ const withForce = resolveCallSiteConfig("mainAgent", llm, {
425
+ overrideProfile: "override",
426
+ forceOverrideProfile: true,
427
+ });
428
+ const withoutForce = resolveCallSiteConfig("mainAgent", llm, {
429
+ overrideProfile: "override",
430
+ });
431
+ expect(withForce).toEqual(withoutForce);
432
+ expect(withForce.effort).toBe("high");
433
+ });
434
+
344
435
  test("overrideProfile absent leaves prior behavior intact", () => {
345
436
  // No `opts` argument at all — the resolver must behave exactly as it did
346
437
  // before this PR for configs without activeProfile/overrideProfile.
@@ -579,6 +670,30 @@ describe("resolveCallSiteConfig", () => {
579
670
  expect(resolved.effort).toBe("medium");
580
671
  });
581
672
 
673
+ test("workflowLeaf defaults to the cost-optimized profile", () => {
674
+ const llm = LLMSchema.parse({
675
+ default: fullDefault,
676
+ profiles: {
677
+ balanced: {
678
+ model: "claude-sonnet-4-7",
679
+ effort: "medium",
680
+ },
681
+ "cost-optimized": {
682
+ model: "claude-haiku-4-5-20251001",
683
+ effort: "low",
684
+ },
685
+ },
686
+ });
687
+
688
+ expect(resolveDefaultProfileKey("workflowLeaf", llm)).toBe(
689
+ "cost-optimized",
690
+ );
691
+ const resolved = resolveCallSiteConfig("workflowLeaf", llm);
692
+ expect(resolved.model).toBe("claude-haiku-4-5-20251001");
693
+ expect(resolved.effort).toBe("low");
694
+ expect(resolved.thinking.enabled).toBe(false);
695
+ });
696
+
582
697
  test("explicit callSites config overrides CALL_SITE_DEFAULTS", () => {
583
698
  const llm = LLMSchema.parse({
584
699
  default: fullDefault,
@@ -66,6 +66,7 @@ mock.module("../config/loader.js", () => ({
66
66
  deepMergeForTest(target, overrides);
67
67
  },
68
68
  getConfig: () => rawConfig,
69
+ getDeploymentContextDefaults: () => ({}),
69
70
  invalidateConfigCache: () => {},
70
71
  withSuppressedConfigDiskWrites: async (fn: () => unknown) => fn(),
71
72
  withSuppressedConfigDiskWritesSync: (fn: () => unknown) => fn(),
@@ -321,7 +322,7 @@ describe("PATCH /v1/config — managed profile deletion guard", () => {
321
322
  const result = await patchRoute.handler({
322
323
  body: { llm: { profiles: { "custom-balanced": null } } },
323
324
  });
324
- expect(result).toEqual({ ok: true });
325
+ expect(result).toHaveProperty("llm");
325
326
  });
326
327
 
327
328
  test("allows deletion of a user-defined profile via null", async () => {
@@ -329,14 +330,14 @@ describe("PATCH /v1/config — managed profile deletion guard", () => {
329
330
  const result = await patchRoute.handler({
330
331
  body: { llm: { profiles: { "my-custom": null } } },
331
332
  });
332
- expect(result).toEqual({ ok: true });
333
+ expect(result).toHaveProperty("llm");
333
334
  });
334
335
 
335
336
  test("allows non-profile config patches", async () => {
336
337
  const result = await patchRoute.handler({
337
338
  body: { someOtherKey: "value" },
338
339
  });
339
- expect(result).toEqual({ ok: true });
340
+ expect(result).toHaveProperty("llm");
340
341
  });
341
342
 
342
343
  test("clears stale Velay ownership when manually patching public base URL", async () => {
@@ -353,7 +354,7 @@ describe("PATCH /v1/config — managed profile deletion guard", () => {
353
354
  },
354
355
  });
355
356
 
356
- expect(result).toEqual({ ok: true });
357
+ expect(result).toHaveProperty("ingress");
357
358
  expect(savedRaw).toEqual({
358
359
  ingress: {
359
360
  publicBaseUrl: "https://manual.example.test",
@@ -371,7 +372,7 @@ describe("PATCH /v1/config — managed profile deletion guard", () => {
371
372
  },
372
373
  },
373
374
  });
374
- expect(result).toEqual({ ok: true });
375
+ expect(result).toHaveProperty("llm");
375
376
  });
376
377
 
377
378
  test("rejects nulling the entire profiles map", async () => {