@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
@@ -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 () => {
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Tests for the default `max-tokens-continue` plugin's hooks.
3
+ *
4
+ * Covers:
5
+ * - The `post-model-call` hook's decision for the canonical cases: a
6
+ * main-agent turn truncated at the output token limit → continue, with the
7
+ * truncated turn and the canonical continuation nudge appended to
8
+ * `messages`; any other stop reason → stop; non-main-agent call sites →
9
+ * stop; a provider rejection → stop; an entirely-stripped (empty) truncated
10
+ * turn → stop.
11
+ * - The per-run budget is split across the two hooks: `post-model-call`
12
+ * consumes one unit per continue (up to `MAX_TOKENS_AUTO_CONTINUES`) and
13
+ * the `stop` hook clears the counter on the definitive terminal so the next
14
+ * run starts with a full budget.
15
+ *
16
+ * The loop's actual side-effects (keeping the partial turn, per-run backstop,
17
+ * the continuation card on a terminal stop) live in `agent/loop.ts`. This
18
+ * file isolates the hook.
19
+ */
20
+
21
+ import { beforeEach, describe, expect, test } from "bun:test";
22
+
23
+ import type {
24
+ PluginLogger,
25
+ PostModelCallContext,
26
+ StopContext,
27
+ } from "../plugin-api/types.js";
28
+ import {
29
+ hasMaxTokensContinueBudget,
30
+ MAX_TOKENS_AUTO_CONTINUES,
31
+ resetMaxTokensContinueStoreForTests,
32
+ } from "../plugins/defaults/max-tokens-continue/continue-state-store.js";
33
+ import postModelCall, {
34
+ MAX_TOKENS_CONTINUE_NUDGE_TEXT,
35
+ } from "../plugins/defaults/max-tokens-continue/hooks/post-model-call.js";
36
+ import stop from "../plugins/defaults/max-tokens-continue/hooks/stop.js";
37
+ import type { ContentBlock } from "../providers/types.js";
38
+
39
+ // ─── Fixtures ────────────────────────────────────────────────────────────────
40
+
41
+ const noopLogger: PluginLogger = {
42
+ info: () => {},
43
+ warn: () => {},
44
+ error: () => {},
45
+ debug: () => {},
46
+ };
47
+
48
+ const truncatedText: ContentBlock = {
49
+ type: "text",
50
+ text: "Here is the start of a very long answer that got cut o",
51
+ };
52
+
53
+ function makeCtx(
54
+ overrides: Partial<PostModelCallContext> = {},
55
+ ): PostModelCallContext {
56
+ return {
57
+ conversationId: "conv-mtc",
58
+ callSite: "mainAgent",
59
+ content: [truncatedText],
60
+ messages: [{ role: "user", content: [{ type: "text", text: "build it" }] }],
61
+ stopReason: "max_tokens",
62
+ decision: "stop",
63
+ logger: noopLogger,
64
+ ...overrides,
65
+ };
66
+ }
67
+
68
+ beforeEach(() => {
69
+ resetMaxTokensContinueStoreForTests();
70
+ });
71
+
72
+ // ─── Decisions ───────────────────────────────────────────────────────────────
73
+
74
+ describe("max-tokens-continue post-model-call hook", () => {
75
+ test("truncated main-agent turn → continue with the turn and nudge appended", async () => {
76
+ /**
77
+ * Tests that a max_tokens stop on a main-agent turn auto-continues.
78
+ */
79
+ // GIVEN a main-agent turn that stopped at the output token limit.
80
+ const ctx = makeCtx();
81
+ const priorLength = ctx.messages.length;
82
+
83
+ // WHEN the hook runs.
84
+ await postModelCall(ctx);
85
+
86
+ // THEN it asks the loop to continue.
87
+ expect(ctx.decision).toBe("continue");
88
+ // AND it appends the truncated assistant turn followed by the nudge.
89
+ expect(ctx.messages).toHaveLength(priorLength + 2);
90
+ expect(ctx.messages[priorLength]).toEqual({
91
+ role: "assistant",
92
+ content: [truncatedText],
93
+ });
94
+ expect(ctx.messages[priorLength + 1]).toEqual({
95
+ role: "user",
96
+ content: [{ type: "text", text: MAX_TOKENS_CONTINUE_NUDGE_TEXT }],
97
+ });
98
+ });
99
+
100
+ test("non-max-tokens stop reason → stop", async () => {
101
+ /**
102
+ * Tests that ordinary stop reasons are left alone.
103
+ */
104
+ // GIVEN a turn that ended normally.
105
+ const ctx = makeCtx({ stopReason: "end_turn" });
106
+
107
+ // WHEN the hook runs.
108
+ await postModelCall(ctx);
109
+
110
+ // THEN the decision stays stop and messages are untouched.
111
+ expect(ctx.decision).toBe("stop");
112
+ expect(ctx.messages).toHaveLength(1);
113
+ });
114
+
115
+ test("non-mainAgent call site → stop", async () => {
116
+ /**
117
+ * Tests that background/subagent calls are never auto-continued.
118
+ */
119
+ // GIVEN a truncated turn on a non-main-agent call site.
120
+ const ctx = makeCtx({ callSite: "recall" });
121
+
122
+ // WHEN the hook runs.
123
+ await postModelCall(ctx);
124
+
125
+ // THEN the decision stays stop.
126
+ expect(ctx.decision).toBe("stop");
127
+ expect(ctx.messages).toHaveLength(1);
128
+ });
129
+
130
+ test("provider rejection → stop", async () => {
131
+ /**
132
+ * Tests that a rejection outcome (no reply content) is ignored.
133
+ */
134
+ // GIVEN a provider rejection carrying an error.
135
+ const ctx = makeCtx({ error: new Error("boom"), content: [] });
136
+
137
+ // WHEN the hook runs.
138
+ await postModelCall(ctx);
139
+
140
+ // THEN the decision stays stop.
141
+ expect(ctx.decision).toBe("stop");
142
+ });
143
+
144
+ test("empty truncated content → stop", async () => {
145
+ /**
146
+ * Tests that a turn with nothing left after truncation-block stripping
147
+ * ends terminally rather than pushing an empty assistant message.
148
+ */
149
+ // GIVEN a max_tokens stop whose safe content is empty.
150
+ const ctx = makeCtx({ content: [] });
151
+
152
+ // WHEN the hook runs.
153
+ await postModelCall(ctx);
154
+
155
+ // THEN the decision stays stop and nothing is appended.
156
+ expect(ctx.decision).toBe("stop");
157
+ expect(ctx.messages).toHaveLength(1);
158
+ });
159
+
160
+ test("budget exhausts after MAX_TOKENS_AUTO_CONTINUES and stop hook resets it", async () => {
161
+ /**
162
+ * Tests the per-run continue budget and its reset on the terminal stop.
163
+ */
164
+ // GIVEN the budget is consumed by repeated truncated turns.
165
+ for (let i = 0; i < MAX_TOKENS_AUTO_CONTINUES; i++) {
166
+ const ctx = makeCtx();
167
+ await postModelCall(ctx);
168
+ expect(ctx.decision).toBe("continue");
169
+ }
170
+ expect(hasMaxTokensContinueBudget("conv-mtc")).toBe(false);
171
+
172
+ // WHEN one more truncated turn arrives.
173
+ const exhausted = makeCtx();
174
+ await postModelCall(exhausted);
175
+
176
+ // THEN the hook lets the turn end.
177
+ expect(exhausted.decision).toBe("stop");
178
+ expect(exhausted.messages).toHaveLength(1);
179
+
180
+ // AND the stop hook clears the budget so the next run continues afresh.
181
+ await stop({ conversationId: "conv-mtc" } as StopContext);
182
+ expect(hasMaxTokensContinueBudget("conv-mtc")).toBe(true);
183
+ });
184
+ });
@@ -322,7 +322,13 @@ describe("image-studio skill script wrapper", () => {
322
322
  expect(result.isError).toBe(true);
323
323
  expect(result.content).toContain("Mock Gemini error: API failure");
324
324
  expect(result.content).toContain(
325
- "Do not change service configuration (mode, provider, or model) to try to fix it",
325
+ "Failed model: gemini-3.1-flash-image-preview",
326
+ );
327
+ expect(result.content).toContain(
328
+ "Do not change service configuration (managed/your-own mode or default provider/model settings)",
329
+ );
330
+ expect(result.content).toContain(
331
+ "Retrying this call once with a different model parameter is allowed",
326
332
  );
327
333
  });
328
334
 
@@ -335,7 +341,10 @@ describe("image-studio skill script wrapper", () => {
335
341
  expect(result.isError).toBe(true);
336
342
  expect(result.content).toContain("Mock OpenAI error: openai failure");
337
343
  expect(result.content).toContain(
338
- "Do not change service configuration (mode, provider, or model) to try to fix it",
344
+ "Do not change service configuration (managed/your-own mode or default provider/model settings)",
345
+ );
346
+ expect(result.content).toContain(
347
+ "Retrying this call once with a different model parameter is allowed",
339
348
  );
340
349
  });
341
350
 
@@ -346,8 +355,9 @@ describe("image-studio skill script wrapper", () => {
346
355
 
347
356
  expect(result.isError).toBe(true);
348
357
  expect(result.content).toContain("No Gemini API key");
358
+ expect(result.content).toContain("Report this error to the user as-is");
349
359
  expect(result.content).toContain(
350
- "Do not change service configuration (mode, provider, or model) to try to fix it",
360
+ "Do not change service configuration (managed/your-own mode or default provider/model settings)",
351
361
  );
352
362
  });
353
363
 
@@ -359,8 +369,9 @@ describe("image-studio skill script wrapper", () => {
359
369
 
360
370
  expect(result.isError).toBe(true);
361
371
  expect(result.content).toContain("Managed proxy is not available");
372
+ expect(result.content).toContain("Report this error to the user as-is");
362
373
  expect(result.content).toContain(
363
- "Do not change service configuration (mode, provider, or model) to try to fix it",
374
+ "Do not change service configuration (managed/your-own mode or default provider/model settings)",
364
375
  );
365
376
  });
366
377
 
@@ -463,11 +474,11 @@ describe("image-studio TOOLS.json manifest", () => {
463
474
  expect(props.mode.enum).toEqual(["generate", "edit"]);
464
475
  expect(props.source_paths.type).toBe("array");
465
476
  expect(props.attachment_ids).toBeUndefined();
466
- expect(props.model.enum).toEqual([
467
- "gemini-3.1-flash-image-preview",
468
- "gemini-3-pro-image-preview",
469
- "gpt-image-2",
470
- ]);
477
+ // No enum by design: model accepts tier aliases or concrete IDs, and is
478
+ // validated at runtime against the registry so the schema never goes
479
+ // stale when models change.
480
+ expect(props.model.type).toBe("string");
481
+ expect(props.model.enum).toBeUndefined();
471
482
  expect(props.variants.type).toBe("number");
472
483
  });
473
484
  });
@@ -82,6 +82,28 @@ const GET_FEATURE_FLAGS_DEFAULT: Record<string, boolean> = {
82
82
  __test_default__: false,
83
83
  };
84
84
 
85
+ class FakeIpcCallError extends Error {
86
+ readonly statusCode?: number;
87
+ readonly errorCode?: string;
88
+ readonly errorDetails?: unknown;
89
+
90
+ constructor(
91
+ message: string,
92
+ fields: {
93
+ statusCode?: number;
94
+ errorCode?: string;
95
+ errorDetails?: unknown;
96
+ } = {},
97
+ ) {
98
+ super(message);
99
+ this.name = "IpcCallError";
100
+ if (fields.statusCode !== undefined) this.statusCode = fields.statusCode;
101
+ if (fields.errorCode !== undefined) this.errorCode = fields.errorCode;
102
+ if (fields.errorDetails !== undefined)
103
+ this.errorDetails = fields.errorDetails;
104
+ }
105
+ }
106
+
85
107
  export function installGatewayIpcMock(): void {
86
108
  mock.module("@vellumai/gateway-client/ipc-client", () => ({
87
109
  ipcCall: async (
@@ -97,6 +119,7 @@ export function installGatewayIpcMock(): void {
97
119
  if (method === "get_feature_flags") return GET_FEATURE_FLAGS_DEFAULT;
98
120
  return undefined;
99
121
  },
122
+ IpcCallError: FakeIpcCallError,
100
123
  PersistentIpcClient: FakePersistentIpcClient,
101
124
  }));
102
125
  }
@@ -20,7 +20,7 @@ describe("model intents", () => {
20
20
  "claude-haiku-4-5-20251001",
21
21
  );
22
22
  expect(resolveModelIntent("anthropic", "quality-optimized")).toBe(
23
- "claude-fable-5",
23
+ "claude-opus-4-8",
24
24
  );
25
25
  expect(resolveModelIntent("anthropic", "vision-optimized")).toBe(
26
26
  "claude-opus-4-6",
@@ -139,6 +139,32 @@ describe("normalizeOnboardingContext", () => {
139
139
  expect(result.preferredName).toBeUndefined();
140
140
  });
141
141
 
142
+ test("maps occupation through, trimmed", () => {
143
+ const ctx: OnboardingContext = {
144
+ tools: [],
145
+ tasks: [],
146
+ tone: "friendly",
147
+ occupation: " Software Engineer ",
148
+ };
149
+ const result = normalizeOnboardingContext(ctx);
150
+ expect(result.occupation).toBe("Software Engineer");
151
+ });
152
+
153
+ test("absent or blank occupation yields undefined", () => {
154
+ expect(
155
+ normalizeOnboardingContext({ tools: [], tasks: [], tone: "friendly" })
156
+ .occupation,
157
+ ).toBeUndefined();
158
+ expect(
159
+ normalizeOnboardingContext({
160
+ tools: [],
161
+ tasks: [],
162
+ tone: "friendly",
163
+ occupation: " ",
164
+ }).occupation,
165
+ ).toBeUndefined();
166
+ });
167
+
142
168
  test("tone passes through", () => {
143
169
  const ctx: OnboardingContext = {
144
170
  tools: [],
@@ -15,8 +15,6 @@ import {
15
15
  hasAccessRequestInstructions,
16
16
  hasInviteFlowDirective,
17
17
  normalizeForDirectiveMatching,
18
- sanitizeIdentityField,
19
- sanitizeMessagePreview,
20
18
  } from "../notifications/access-request-copy.js";
21
19
  import type { ConversationCandidateSet } from "../notifications/conversation-candidates.js";
22
20
  import { composeFallbackCopy } from "../notifications/copy-composer.js";
@@ -24,6 +22,10 @@ import {
24
22
  enforceGuardianCallConversationAffinity,
25
23
  validateConversationActions,
26
24
  } from "../notifications/decision-engine.js";
25
+ import {
26
+ sanitizeIdentityField,
27
+ sanitizeMessagePreview,
28
+ } from "../notifications/notification-utils.js";
27
29
  import type { NotificationSignal } from "../notifications/signal.js";
28
30
  import type {
29
31
  NotificationChannel,
@@ -186,9 +186,18 @@ describe("TelegramAdapter", () => {
186
186
  contextPayload: {
187
187
  requestId: "req-abc-123",
188
188
  requestCode: "XYZW",
189
- senderIdentifier: "Marina",
189
+ senderIdentifier: "TestUser",
190
190
  sourceChannel: "telegram",
191
191
  },
192
+ approvalContext: {
193
+ requestId: "req-abc-123",
194
+ actions: [
195
+ { id: "approve_once", label: "Approve once" },
196
+ { id: "reject", label: "Reject" },
197
+ ],
198
+ plainTextFallback:
199
+ 'TestUser is requesting access to the assistant.\nReply "XYZW approve" to grant access or "XYZW reject" to deny.\nReply "open invite flow" to start Trusted Contacts invite flow.',
200
+ },
192
201
  });
193
202
 
194
203
  const result = await adapter.send(payload, makeDestination());
@@ -237,7 +246,7 @@ describe("TelegramAdapter", () => {
237
246
  body: "Someone is requesting access.",
238
247
  },
239
248
  contextPayload: {
240
- senderIdentifier: "Marina",
249
+ senderIdentifier: "TestUser",
241
250
  sourceChannel: "telegram",
242
251
  // no requestId
243
252
  },
@@ -264,9 +273,18 @@ describe("TelegramAdapter", () => {
264
273
  contextPayload: {
265
274
  requestId: "req-abc-123",
266
275
  requestCode: "XYZW",
267
- senderIdentifier: "Marina",
276
+ senderIdentifier: "TestUser",
268
277
  sourceChannel: "telegram",
269
278
  },
279
+ approvalContext: {
280
+ requestId: "req-abc-123",
281
+ actions: [
282
+ { id: "approve_once", label: "Approve once" },
283
+ { id: "reject", label: "Reject" },
284
+ ],
285
+ plainTextFallback:
286
+ 'TestUser is requesting access to the assistant.\nReply "XYZW approve" to grant access or "XYZW reject" to deny.\nReply "open invite flow" to start Trusted Contacts invite flow.',
287
+ },
270
288
  });
271
289
 
272
290
  const result = await adapter.send(payload, makeDestination());