@vellumai/assistant 0.11.1 → 0.11.2-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 (318) hide show
  1. package/Dockerfile +1 -3
  2. package/README.md +1 -1
  3. package/eslint.config.mjs +28 -8
  4. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  10. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  12. package/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  13. package/node_modules/@vellumai/slack-text/src/index.test.ts +48 -0
  14. package/node_modules/@vellumai/slack-text/src/index.ts +25 -6
  15. package/openapi.yaml +292 -5
  16. package/package.json +1 -1
  17. package/src/__tests__/agent-loop-resume-interrupted.test.ts +223 -0
  18. package/src/__tests__/byok-default-profile-ensure.test.ts +43 -0
  19. package/src/__tests__/cli-logger-boundary-guard.test.ts +87 -0
  20. package/src/__tests__/compaction-events.test.ts +139 -2
  21. package/src/__tests__/config-loader-backfill.test.ts +35 -3
  22. package/src/__tests__/config-schema.test.ts +1 -0
  23. package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +30 -0
  24. package/src/__tests__/conversation-agent-loop.test.ts +180 -7
  25. package/src/__tests__/conversation-error.test.ts +211 -4
  26. package/src/__tests__/conversation-options-turn-scoped-transport.test.ts +64 -0
  27. package/src/__tests__/conversation-queue.test.ts +126 -0
  28. package/src/__tests__/conversation-retry-route.test.ts +46 -7
  29. package/src/__tests__/conversation-slash.test.ts +4 -2
  30. package/src/__tests__/conversation-summarize-route.test.ts +7 -1
  31. package/src/__tests__/conversation-summarize-up-to.test.ts +71 -5
  32. package/src/__tests__/document-create-dedupe.test.ts +2 -1
  33. package/src/__tests__/document-find-replace.test.ts +2 -1
  34. package/src/__tests__/document-tool-security.test.ts +2 -1
  35. package/src/__tests__/document-update-default-surface.test.ts +2 -1
  36. package/src/__tests__/document-workspace-file.test.ts +467 -0
  37. package/src/__tests__/emit-signal-routing-intent.test.ts +290 -49
  38. package/src/__tests__/guardian-card-withdrawal.test.ts +91 -3
  39. package/src/__tests__/http-user-message-parity.test.ts +66 -0
  40. package/src/__tests__/list-messages-attachments.test.ts +87 -0
  41. package/src/__tests__/list-messages-provider-error.test.ts +143 -0
  42. package/src/__tests__/list-messages-system-card.test.ts +100 -0
  43. package/src/__tests__/llm-resolver.test.ts +19 -9
  44. package/src/__tests__/managed-profile-guard.test.ts +23 -0
  45. package/src/__tests__/notification-platform-adapter.test.ts +130 -2
  46. package/src/__tests__/notification-telegram-adapter.test.ts +6 -0
  47. package/src/__tests__/notification-vellum-adapter.test.ts +45 -0
  48. package/src/__tests__/plugin-api-model-profiles.test.ts +10 -1
  49. package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
  50. package/src/__tests__/provider-error-scenarios.test.ts +140 -0
  51. package/src/__tests__/provider-send-message-override-profile.test.ts +95 -0
  52. package/src/__tests__/run-conversation-turn-persistence.test.ts +66 -3
  53. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +209 -0
  54. package/src/__tests__/skills.test.ts +27 -0
  55. package/src/__tests__/slack-channels-routes.test.ts +0 -2
  56. package/src/__tests__/slack-share-routes.test.ts +0 -3
  57. package/src/__tests__/slack-users-routes.test.ts +0 -2
  58. package/src/__tests__/subagent-tools.test.ts +11 -0
  59. package/src/__tests__/tool-preview-lifecycle.test.ts +58 -0
  60. package/src/__tests__/tool-result-spool.test.ts +5 -4
  61. package/src/__tests__/turn-boundary-resolution.test.ts +53 -0
  62. package/src/__tests__/turn-events-store.test.ts +26 -0
  63. package/src/__tests__/ui-visual-surface.test.ts +695 -0
  64. package/src/__tests__/unified-turn-context-visible-app.test.ts +99 -0
  65. package/src/__tests__/visible-app-context.test.ts +189 -0
  66. package/src/__tests__/workspace-git-service.test.ts +173 -33
  67. package/src/__tests__/workspace-migration-137-repair-retired-fireworks-minimax-model-id.test.ts +157 -0
  68. package/src/__tests__/workspace-migration-138-backfill-home-feed-titles.test.ts +373 -0
  69. package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +137 -0
  70. package/src/agent/loop.ts +36 -7
  71. package/src/api/events/context-window-usage.ts +31 -0
  72. package/src/api/events/notification-intent.ts +8 -0
  73. package/src/api/events/ui-surface-pending.ts +35 -0
  74. package/src/api/index.ts +14 -0
  75. package/src/api/responses/conversation-message.ts +25 -4
  76. package/src/api/surfaces.ts +90 -1
  77. package/src/approvals/guardian-card-withdrawal.ts +66 -31
  78. package/src/approvals/guardian-decision-primitive.ts +4 -0
  79. package/src/calls/__tests__/call-setup-router.test.ts +156 -26
  80. package/src/calls/__tests__/voice-session-bridge.test.ts +201 -12
  81. package/src/calls/call-setup-router.ts +80 -36
  82. package/src/calls/voice-session-bridge.ts +173 -26
  83. package/src/cli/commands/inference.help.ts +3 -3
  84. package/src/cli/commands/notifications.help.ts +3 -2
  85. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +42 -128
  86. package/src/cli/commands/platform/__tests__/credits.test.ts +9 -78
  87. package/src/cli/commands/platform/__tests__/helpers.ts +90 -0
  88. package/src/cli/commands/platform/__tests__/invoices.test.ts +238 -0
  89. package/src/cli/commands/platform/__tests__/plans.test.ts +9 -88
  90. package/src/cli/commands/platform/__tests__/status.test.ts +12 -87
  91. package/src/cli/commands/platform/__tests__/subscription.test.ts +9 -86
  92. package/src/cli/commands/platform/index.help.ts +92 -0
  93. package/src/cli/commands/platform/index.ts +7 -0
  94. package/src/cli/commands/platform/invoices.ts +132 -0
  95. package/src/cli/commands/usage.help.ts +1 -1
  96. package/src/cli/lib/list-installed-plugins.ts +2 -1
  97. package/src/config/__tests__/default-profile-catalog.test.ts +7 -10
  98. package/src/config/__tests__/deployment-context-defaults.test.ts +27 -5
  99. package/src/config/assistant-feature-flags.ts +7 -2
  100. package/src/config/bundled-skills/app-builder/SKILL.md +3 -2
  101. package/src/config/bundled-skills/subagent/SKILL.md +3 -1
  102. package/src/config/bundled-skills/subagent/TOOLS.json +3 -3
  103. package/src/config/bundled-skills/visualize/SKILL.md +163 -0
  104. package/src/config/call-site-defaults.ts +3 -2
  105. package/src/config/default-profile-catalog.ts +75 -70
  106. package/src/config/default-profile-names.ts +13 -28
  107. package/src/config/env-registry.ts +1 -0
  108. package/src/config/feature-flag-registry.json +16 -0
  109. package/src/config/llm-resolver.ts +21 -5
  110. package/src/config/loader.ts +18 -11
  111. package/src/config/schemas/llm.ts +1 -9
  112. package/src/config/schemas/memory-retrospective.ts +9 -0
  113. package/src/config/schemas/monitoring.ts +28 -2
  114. package/src/config/schemas/workspace-git.ts +15 -0
  115. package/src/config/seed-inference-profiles.ts +19 -5
  116. package/src/context/post-turn-tool-result-truncation.ts +2 -2
  117. package/src/conversations/__tests__/message-consolidation.test.ts +54 -0
  118. package/src/conversations/message-consolidation.ts +17 -15
  119. package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +182 -0
  120. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +181 -0
  121. package/src/daemon/conversation-agent-loop-handlers.ts +90 -7
  122. package/src/daemon/conversation-agent-loop.ts +107 -22
  123. package/src/daemon/conversation-error.ts +169 -48
  124. package/src/daemon/conversation-messaging.ts +87 -4
  125. package/src/daemon/conversation-process.ts +57 -45
  126. package/src/daemon/conversation-runtime-assembly.ts +57 -0
  127. package/src/daemon/conversation-store.ts +36 -1
  128. package/src/daemon/conversation-surfaces.ts +68 -6
  129. package/src/daemon/conversation-turn-finalize.ts +76 -18
  130. package/src/daemon/conversation.ts +122 -27
  131. package/src/daemon/lifecycle.ts +9 -0
  132. package/src/daemon/message-types/conversations.ts +8 -0
  133. package/src/daemon/message-types/surfaces.ts +3 -0
  134. package/src/documents/document-store.ts +247 -10
  135. package/src/home/__tests__/feed-types.test.ts +43 -8
  136. package/src/home/__tests__/feed-writer.test.ts +59 -0
  137. package/src/home/feed-types.ts +34 -7
  138. package/src/home/feed-writer.ts +15 -6
  139. package/src/live-voice/__tests__/activity-label.test.ts +95 -0
  140. package/src/live-voice/__tests__/live-activity-reporter.test.ts +86 -5
  141. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +310 -25
  142. package/src/live-voice/__tests__/live-voice-events.test.ts +4 -4
  143. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +8 -4
  144. package/src/live-voice/__tests__/live-voice-vad.test.ts +9 -1
  145. package/src/live-voice/activity-label.ts +169 -0
  146. package/src/live-voice/live-activity-reporter.ts +35 -6
  147. package/src/live-voice/live-voice-session.ts +282 -41
  148. package/src/live-voice/protocol.ts +35 -0
  149. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +84 -2
  150. package/src/messaging/providers/slack/adapter.ts +86 -19
  151. package/src/messaging/providers/slack/api.test.ts +85 -1
  152. package/src/messaging/providers/slack/api.ts +121 -288
  153. package/src/messaging/providers/slack/client.ts +20 -251
  154. package/src/messaging/providers/slack/send.test.ts +4 -9
  155. package/src/messaging/providers/slack/send.ts +1 -1
  156. package/src/messaging/providers/slack/types.ts +5 -0
  157. package/src/messaging/providers/slack/web-api-transport.test.ts +181 -0
  158. package/src/messaging/providers/slack/web-api-transport.ts +367 -0
  159. package/src/messaging/providers/slack/withdraw.ts +6 -14
  160. package/src/messaging/providers/telegram-bot/send.test.ts +31 -1
  161. package/src/messaging/providers/telegram-bot/send.ts +23 -2
  162. package/src/messaging/providers/telegram-bot/withdraw.test.ts +152 -0
  163. package/src/messaging/providers/telegram-bot/withdraw.ts +166 -0
  164. package/src/monitoring/__tests__/db-integrity-sample.test.ts +18 -4
  165. package/src/monitoring/__tests__/file-descriptors.test.ts +144 -0
  166. package/src/monitoring/file-descriptors.ts +262 -0
  167. package/src/monitoring/process-memory.ts +4 -11
  168. package/src/monitoring/resource-sampler.ts +5 -0
  169. package/src/monitoring/worker.ts +12 -0
  170. package/src/notifications/__tests__/assistant-reply-producer.test.ts +740 -0
  171. package/src/notifications/__tests__/broadcaster.test.ts +347 -7
  172. package/src/notifications/__tests__/copy-composer.test.ts +53 -3
  173. package/src/notifications/__tests__/decision-engine.test.ts +349 -29
  174. package/src/notifications/__tests__/deterministic-checks.test.ts +21 -0
  175. package/src/notifications/__tests__/edit-notification.test.ts +330 -0
  176. package/src/notifications/__tests__/guardian-delivery-recorder.test.ts +71 -0
  177. package/src/notifications/__tests__/home-feed-side-effect.test.ts +170 -12
  178. package/src/notifications/adapters/macos.ts +3 -0
  179. package/src/notifications/adapters/platform.ts +90 -14
  180. package/src/notifications/adapters/telegram.ts +6 -4
  181. package/src/notifications/assistant-reply-producer.ts +219 -0
  182. package/src/notifications/broadcaster.ts +523 -317
  183. package/src/notifications/copy-composer.ts +27 -6
  184. package/src/notifications/decision-engine.ts +154 -77
  185. package/src/notifications/deterministic-checks.ts +6 -7
  186. package/src/notifications/edit-notification.ts +8 -4
  187. package/src/notifications/emit-signal.ts +49 -16
  188. package/src/notifications/guardian-delivery-recorder.ts +8 -2
  189. package/src/notifications/home-feed-side-effect.ts +43 -6
  190. package/src/notifications/notification-utils.ts +39 -4
  191. package/src/notifications/signal.ts +10 -0
  192. package/src/notifications/types.ts +17 -0
  193. package/src/persistence/bookmark-crud.ts +18 -9
  194. package/src/persistence/conversation-attention-store.ts +26 -0
  195. package/src/persistence/conversation-crud.ts +79 -32
  196. package/src/persistence/conversation-queries.ts +30 -15
  197. package/src/persistence/conversation-title-service.ts +5 -170
  198. package/src/persistence/conversation-types.ts +192 -1
  199. package/src/persistence/db-init.ts +14 -1
  200. package/src/persistence/db-maintenance.ts +5 -4
  201. package/src/persistence/embeddings/__tests__/plugin-index-qdrant-init.test.ts +219 -0
  202. package/src/persistence/embeddings/__tests__/plugin-index.test.ts +22 -0
  203. package/src/persistence/embeddings/__tests__/worker-script-version.test.ts +76 -0
  204. package/src/persistence/embeddings/embedding-local.ts +2 -1
  205. package/src/persistence/embeddings/embedding-runtime-manager.ts +29 -5
  206. package/src/persistence/embeddings/plugin-index.ts +84 -12
  207. package/src/persistence/migrations/360-add-document-workspace-path.test.ts +110 -0
  208. package/src/persistence/migrations/360-add-document-workspace-path.ts +39 -0
  209. package/src/persistence/planner-statistics.ts +14 -0
  210. package/src/persistence/schema/documents.ts +26 -11
  211. package/src/persistence/steps.ts +2 -0
  212. package/src/platform/client.test.ts +172 -2
  213. package/src/platform/client.ts +178 -55
  214. package/src/plugin-api/conversation-turn.ts +13 -0
  215. package/src/plugin-api/model-profiles.test.ts +6 -2
  216. package/src/plugins/defaults/memory/__tests__/bookmark-crud.test.ts +34 -0
  217. package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +22 -0
  218. package/src/plugins/defaults/memory/__tests__/jobs-store-enqueue-gate.test.ts +11 -0
  219. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +199 -0
  220. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +98 -1
  221. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +105 -1
  222. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +24 -2
  223. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +524 -0
  224. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +1 -1
  225. package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +9 -4
  226. package/src/plugins/defaults/memory/host-utils.ts +5 -0
  227. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +105 -1
  228. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +64 -5
  229. package/src/plugins/defaults/memory/memory-retrospective-job.ts +36 -1
  230. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +12 -3
  231. package/src/plugins/defaults/memory/src/__tests__/memory-v2-simulate-route.test.ts +1 -0
  232. package/src/plugins/defaults/memory/substrate/__tests__/skill-store.test.ts +195 -1
  233. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +1 -1
  234. package/src/plugins/defaults/memory/substrate/skill-store.ts +167 -29
  235. package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +131 -0
  236. package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +1 -0
  237. package/src/plugins/defaults/memory/v2/activation-log-store.ts +4 -0
  238. package/src/plugins/defaults/memory/v2/injection.ts +48 -3
  239. package/src/plugins/defaults/memory/v2/rerank-local.ts +6 -2
  240. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -1
  241. package/src/plugins/defaults/platform-hosted/routes/reengage.ts +1 -1
  242. package/src/plugins/defaults/turn-context/injectors.ts +1 -0
  243. package/src/plugins/defaults/turn-context/unified-turn-context.ts +26 -0
  244. package/src/plugins/types.ts +17 -0
  245. package/src/prompts/templates/system-sections.ts +2 -2
  246. package/src/providers/__tests__/dispatch-connection-routing.test.ts +43 -2
  247. package/src/providers/__tests__/vellum-mismatch-routing.test.ts +8 -0
  248. package/src/providers/call-site-routing.ts +77 -14
  249. package/src/providers/connection-resolution.ts +42 -2
  250. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +127 -1
  251. package/src/providers/inference/adapter-factory.ts +70 -12
  252. package/src/providers/model-catalog.ts +0 -11
  253. package/src/providers/model-intents.ts +11 -0
  254. package/src/providers/registry.ts +8 -0
  255. package/src/providers/retry.ts +85 -17
  256. package/src/providers/types.ts +8 -1
  257. package/src/runtime/__tests__/agent-wake.test.ts +181 -0
  258. package/src/runtime/agent-wake.ts +132 -9
  259. package/src/runtime/channel-approval-types.ts +25 -0
  260. package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +2 -2
  261. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +11 -0
  262. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +121 -9
  263. package/src/runtime/routes/__tests__/platform-invoice-routes.test.ts +392 -0
  264. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +7 -0
  265. package/src/runtime/routes/__tests__/workspace-commit-routes.test.ts +63 -0
  266. package/src/runtime/routes/consolidation-routes.ts +1 -1
  267. package/src/runtime/routes/conversation-management-routes.ts +18 -6
  268. package/src/runtime/routes/conversation-query-routes.ts +14 -23
  269. package/src/runtime/routes/conversation-routes.ts +113 -8
  270. package/src/runtime/routes/credential-routes.ts +1 -5
  271. package/src/runtime/routes/documents-routes.ts +207 -12
  272. package/src/runtime/routes/identity-routes.ts +3 -93
  273. package/src/runtime/routes/inbound-stages/admission-policy.test.ts +31 -1
  274. package/src/runtime/routes/inbound-stages/admission-policy.ts +18 -0
  275. package/src/runtime/routes/inference-provider-connection-routes.ts +52 -3
  276. package/src/runtime/routes/notification-routes.ts +4 -1
  277. package/src/runtime/routes/platform-routes.ts +238 -3
  278. package/src/runtime/routes/playground/guard.ts +1 -2
  279. package/src/runtime/routes/slack-channel-routes.ts +2 -4
  280. package/src/runtime/routes/workspace-commit-routes.ts +49 -2
  281. package/src/runtime/routes/workspace-routes.ts +11 -12
  282. package/src/runtime/routes/workspace-utils.ts +49 -2
  283. package/src/runtime/services/conversation-serializer.ts +2 -4
  284. package/src/subagent/__tests__/consult-context-gating.test.ts +98 -0
  285. package/src/subagent/__tests__/consult-context-skills.test.ts +82 -0
  286. package/src/subagent/__tests__/consult-context.test.ts +84 -0
  287. package/src/subagent/__tests__/consult-prompt.test.ts +36 -0
  288. package/src/subagent/consult-context.ts +410 -0
  289. package/src/subagent/consult-prompt.ts +38 -5
  290. package/src/subagent/manager.ts +9 -4
  291. package/src/subagent/types.ts +8 -0
  292. package/src/telemetry/telemetry-event-sources.ts +7 -0
  293. package/src/telemetry/telemetry-wire-source.json +1 -1
  294. package/src/telemetry/telemetry-wire.generated.ts +1 -0
  295. package/src/telemetry/turn-events-store.ts +30 -1
  296. package/src/telemetry/types.ts +24 -0
  297. package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
  298. package/src/tools/browser/pinned-tabs.ts +3 -1
  299. package/src/tools/subagent/spawn.ts +23 -1
  300. package/src/tools/terminal/safe-env.ts +1 -0
  301. package/src/tools/ui-surface/definitions.ts +39 -1
  302. package/src/tools/ui-surface/surface-shape-docs.ts +38 -4
  303. package/src/tools/ui-surface/visual-validation.ts +787 -0
  304. package/src/tools/workflows/run-workflow.test.ts +1 -0
  305. package/src/util/__tests__/short-title.test.ts +229 -0
  306. package/src/util/__tests__/worker-compute.test.ts +65 -0
  307. package/src/util/cgroup-cpu.ts +93 -0
  308. package/src/util/errors.ts +27 -0
  309. package/src/util/process-tree.ts +19 -0
  310. package/src/util/short-title.ts +189 -0
  311. package/src/util/worker-compute.ts +83 -0
  312. package/src/workspace/byok-default-profile-ensure.ts +25 -15
  313. package/src/workspace/git-service.ts +79 -30
  314. package/src/workspace/migrations/137-repair-retired-fireworks-minimax-model-id.ts +131 -0
  315. package/src/workspace/migrations/138-backfill-home-feed-titles.ts +179 -0
  316. package/src/workspace/migrations/139-clear-renamed-cost-profile-label.ts +92 -0
  317. package/src/workspace/migrations/registry.ts +6 -0
  318. package/src/plugins/defaults/memory/substrate/constants.ts +0 -8
@@ -170,6 +170,112 @@ describe("classifyConversationError", () => {
170
170
  expect(result.userMessage).toContain("AI provider");
171
171
  expect(result.errorCategory).toBe("rate_limit");
172
172
  });
173
+
174
+ it("uses the ChatGPT OAuth route instead of the OpenAI registry default", () => {
175
+ providerRoutingSources.openai = "managed-proxy";
176
+ const err = new ProviderError(
177
+ "OpenAI API error (429): Too many requests",
178
+ "openai",
179
+ 429,
180
+ { reason: "rate_limited" },
181
+ );
182
+
183
+ const result = classifyConversationError(err, {
184
+ ...baseCtx,
185
+ connectionName: "chatgpt-subscription",
186
+ isManagedRoute: false,
187
+ });
188
+
189
+ expect(result.code).toBe("PROVIDER_RATE_LIMIT");
190
+ expect(result.userMessage).toContain("AI provider");
191
+ expect(result.errorCategory).toBe("rate_limit");
192
+ });
193
+
194
+ it("prefers the failed call's direct route over stale managed turn attribution", () => {
195
+ providerRoutingSources.openai = "managed-proxy";
196
+ const err = new ProviderError(
197
+ "OpenAI API error (429): Too many requests",
198
+ "openai",
199
+ 429,
200
+ { reason: "rate_limited" },
201
+ );
202
+ err.attachRouteAttribution({
203
+ connectionName: "chatgpt-subscription",
204
+ profileName: "chatgpt",
205
+ isManagedRoute: false,
206
+ });
207
+
208
+ const result = classifyConversationError(err, {
209
+ ...baseCtx,
210
+ connectionName: "vellum",
211
+ profileName: "managed",
212
+ isManagedRoute: true,
213
+ });
214
+
215
+ expect(result.code).toBe("PROVIDER_RATE_LIMIT");
216
+ expect(result.userMessage).toContain("AI provider");
217
+ expect(result.errorCategory).toBe("rate_limit");
218
+ });
219
+
220
+ it("prefers the failed call's managed fallback over stale direct turn attribution", () => {
221
+ const err = new ProviderError(
222
+ "Anthropic API error (429): Too many requests",
223
+ "anthropic",
224
+ 429,
225
+ { reason: "rate_limited" },
226
+ );
227
+ err.attachRouteAttribution({
228
+ profileName: "direct",
229
+ isManagedRoute: true,
230
+ });
231
+
232
+ const result = classifyConversationError(err, {
233
+ ...baseCtx,
234
+ connectionName: "anthropic-key",
235
+ profileName: "direct",
236
+ isManagedRoute: false,
237
+ });
238
+
239
+ expect(result.code).toBe("MANAGED_USAGE_LIMIT");
240
+ expect(result.userMessage).toContain("Vellum managed inference");
241
+ expect(result.errorCategory).toBe("managed_usage_limit");
242
+ });
243
+
244
+ it("does not apply a managed LLM route to a plain rate-limit error", () => {
245
+ const result = classifyConversationError(
246
+ new Error("429 Too Many Requests from a tool API"),
247
+ {
248
+ ...baseCtx,
249
+ isManagedRoute: true,
250
+ },
251
+ );
252
+
253
+ expect(result.code).toBe("PROVIDER_RATE_LIMIT");
254
+ expect(result.errorCategory).toBe("rate_limit");
255
+ });
256
+
257
+ it("falls back to context for fields the failed call's route omits", () => {
258
+ const err = new ProviderError("Unauthorized", "anthropic", 401);
259
+ err.attachRouteAttribution({ profileName: "direct" });
260
+
261
+ const result = classifyConversationError(err, {
262
+ ...baseCtx,
263
+ connectionName: "anthropic-key",
264
+ });
265
+
266
+ expect(result.connectionName).toBe("anthropic-key");
267
+ expect(result.profileName).toBe("direct");
268
+ });
269
+
270
+ it("still recognizes a rewrapped Vellum quota body", () => {
271
+ const result = classifyConversationError(
272
+ new Error('429 {"code":"daily_quota_exceeded"}'),
273
+ baseCtx,
274
+ );
275
+
276
+ expect(result.code).toBe("MANAGED_USAGE_LIMIT");
277
+ expect(result.errorCategory).toBe("managed_usage_limit");
278
+ });
173
279
  });
174
280
 
175
281
  describe("provider overloaded errors", () => {
@@ -671,6 +777,9 @@ describe("classifyConversationError", () => {
671
777
  expect(result.code).toBe("PROVIDER_INVALID_KEY");
672
778
  expect(result.retryable).toBe(false);
673
779
  expect(result.errorCategory).toBe("provider_invalid_key");
780
+ expect(result.userMessage).toBe(
781
+ "Your personal Anthropic API key was rejected by Anthropic. Update that key in Settings → Models & Services.",
782
+ );
674
783
  });
675
784
 
676
785
  it("classifies managed-proxy auth failures as managed credential refresh failures", () => {
@@ -685,12 +794,30 @@ describe("classifyConversationError", () => {
685
794
 
686
795
  expect(result.code).toBe("MANAGED_KEY_INVALID");
687
796
  expect(result.userMessage).toBe(
688
- "Couldn't refresh assistant credentials.",
797
+ "Vellum's managed inference credential was rejected. This isn't a personal provider API key — Vellum provisions this one, so there's nothing to update in Settings.",
689
798
  );
690
799
  expect(result.retryable).toBe(false);
691
800
  expect(result.errorCategory).toBe("managed_key_invalid");
692
801
  });
693
802
 
803
+ it("keeps credential-shaped 4xx failures on the request's managed route", () => {
804
+ providerRoutingSources.anthropic = "user-key";
805
+ const err = new ProviderError(
806
+ "Authentication error: invalid API key",
807
+ "anthropic",
808
+ 400,
809
+ );
810
+ err.attachRouteAttribution({ credentialSource: "vellum-managed" });
811
+
812
+ const result = classifyConversationError(err, baseCtx);
813
+
814
+ expect(result.code).toBe("MANAGED_KEY_INVALID");
815
+ expect(result.errorCategory).toBe("managed_key_invalid");
816
+ expect(result.userMessage).toBe(
817
+ "Vellum's managed inference credential was rejected. This isn't a personal provider API key — Vellum provisions this one, so there's nothing to update in Settings.",
818
+ );
819
+ });
820
+
694
821
  it("classifies ProviderError 401 with 'invalid x-api-key' message as PROVIDER_INVALID_KEY", () => {
695
822
  // Regex-match branch — Anthropic's standard 401 wording.
696
823
  const err = new ProviderError(
@@ -708,6 +835,8 @@ describe("classifyConversationError", () => {
708
835
  const result = classifyConversationError(err, baseCtx);
709
836
  expect(result.code).toBe("PROVIDER_INVALID_KEY");
710
837
  expect(result.errorCategory).toBe("provider_invalid_key");
838
+ expect(result.userMessage).toContain("personal OpenAI API key");
839
+ expect(result.userMessage).toContain("rejected by OpenAI");
711
840
  });
712
841
 
713
842
  it("includes connection/profile attribution in PROVIDER_INVALID_KEY when provided", () => {
@@ -720,7 +849,84 @@ describe("classifyConversationError", () => {
720
849
  expect(result.code).toBe("PROVIDER_INVALID_KEY");
721
850
  expect(result.connectionName).toBe("my-anthropic");
722
851
  expect(result.profileName).toBe("personal");
723
- expect(result.userMessage).toContain("personal");
852
+ expect(result.userMessage).toBe(
853
+ 'Your personal Anthropic API key for profile "personal" (connection "my-anthropic") was rejected by Anthropic. Update that key in Settings → Models & Services.',
854
+ );
855
+ });
856
+
857
+ it("uses the request's BYOK attribution when the same provider also has a managed route", () => {
858
+ providerRoutingSources.anthropic = "managed-proxy";
859
+ const err = new ProviderError("Unauthorized", "anthropic", 401);
860
+ err.attachRouteAttribution({
861
+ credentialSource: "byok",
862
+ connectionName: "anthropic-personal",
863
+ });
864
+
865
+ const result = classifyConversationError(err, baseCtx);
866
+
867
+ expect(result.code).toBe("PROVIDER_INVALID_KEY");
868
+ expect(result.connectionName).toBe("anthropic-personal");
869
+ expect(result.userMessage).toBe(
870
+ 'Your personal Anthropic API key for connection "anthropic-personal" was rejected by Anthropic. Update that key in Settings → Models & Services.',
871
+ );
872
+ });
873
+
874
+ it("uses the request's managed attribution when the same provider also has a BYOK route", () => {
875
+ providerRoutingSources.anthropic = "user-key";
876
+ const err = new ProviderError("Unauthorized", "anthropic", 401);
877
+ err.attachRouteAttribution({
878
+ credentialSource: "vellum-managed",
879
+ connectionName: "vellum",
880
+ });
881
+
882
+ const result = classifyConversationError(err, baseCtx);
883
+
884
+ expect(result.code).toBe("MANAGED_KEY_INVALID");
885
+ expect(result.userMessage).toBe(
886
+ "Vellum's managed inference credential was rejected. This isn't a personal provider API key — Vellum provisions this one, so there's nothing to update in Settings.",
887
+ );
888
+ });
889
+
890
+ it("uses subscription-specific recovery when the same provider also has a managed route", () => {
891
+ providerRoutingSources.openai = "managed-proxy";
892
+ const err = new ProviderError("Unauthorized", "openai", 401, {
893
+ reason: "invalid_credentials",
894
+ });
895
+ err.attachRouteAttribution({
896
+ credentialSource: "oauth-subscription",
897
+ connectionName: "chatgpt-subscription",
898
+ });
899
+
900
+ const result = classifyConversationError(err, baseCtx);
901
+
902
+ expect(result.code).toBe("PROVIDER_API");
903
+ expect(result.errorCategory).toBe("provider_subscription_auth");
904
+ expect(result.retryable).toBe(false);
905
+ expect(result.connectionName).toBe("chatgpt-subscription");
906
+ expect(result.userMessage).toBe(
907
+ 'Your OpenAI subscription login for connection "chatgpt-subscription" was rejected by OpenAI. Reconnect that account in Settings → Models & Services.',
908
+ );
909
+ });
910
+
911
+ it("explains when a no-auth endpoint rejects an unauthenticated request", () => {
912
+ providerRoutingSources["openai-compatible"] = "managed-proxy";
913
+ const err = new ProviderError("Unauthorized", "openai-compatible", 401, {
914
+ reason: "invalid_credentials",
915
+ });
916
+ err.attachRouteAttribution({
917
+ credentialSource: "no-auth",
918
+ connectionName: "local-model-server",
919
+ });
920
+
921
+ const result = classifyConversationError(err, baseCtx);
922
+
923
+ expect(result.code).toBe("PROVIDER_API");
924
+ expect(result.errorCategory).toBe("provider_endpoint_auth_required");
925
+ expect(result.retryable).toBe(false);
926
+ expect(result.connectionName).toBe("local-model-server");
927
+ expect(result.userMessage).toBe(
928
+ 'The OpenAI-compatible endpoint for connection "local-model-server" requires authentication, but that connection is configured without credentials. Configure authentication for that endpoint in Settings → Models & Services.',
929
+ );
724
930
  });
725
931
 
726
932
  it("classifies direct ProviderError with 402 as provider_billing (non-retryable)", () => {
@@ -788,8 +994,9 @@ describe("classifyConversationError", () => {
788
994
  expect(result.code).toBe("PROVIDER_BILLING");
789
995
  expect(result.errorCategory).toBe("credits_exhausted");
790
996
  expect(result.retryable).toBe(false);
791
- expect(result.userMessage).toContain("Add funds");
792
- expect(result.userMessage).toContain("assistant");
997
+ expect(result.userMessage).toBe(
998
+ "You're out of credits. Add credits in Settings → Billing to continue.",
999
+ );
793
1000
  });
794
1001
 
795
1002
  it("classifies direct Anthropic, OpenAI, and OpenRouter 402 responses as provider_billing", () => {
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Tests for `withoutTurnScopedTransport`, the guard that keeps per-message view
3
+ * state out of the durable `conversationOptions` map.
4
+ *
5
+ * `getOrCreateConversation` re-applies the stored transport whenever it rebuilds
6
+ * an evicted or stale conversation. Anything describing what the client had on
7
+ * screen for one message must be stripped before it is stored, or a rebuild
8
+ * hours later (a scheduled wake, a background follow-up) would resurrect an app
9
+ * the user has since closed and tell the model it is on screen.
10
+ */
11
+
12
+ import { describe, expect, test } from "bun:test";
13
+
14
+ import { withoutTurnScopedTransport } from "../daemon/conversation-store.js";
15
+ import type { ConversationCreateOptions } from "../daemon/handlers/shared.js";
16
+
17
+ describe("withoutTurnScopedTransport", () => {
18
+ test("drops the visible app while keeping durable transport fields", () => {
19
+ const options: ConversationCreateOptions = {
20
+ transport: {
21
+ channelId: "vellum",
22
+ interfaceId: "web",
23
+ clientOs: "macos",
24
+ clientTimezone: "America/Denver",
25
+ visibleAppId: "app-on-screen",
26
+ },
27
+ };
28
+
29
+ const stored = withoutTurnScopedTransport(options);
30
+
31
+ expect(stored.transport?.visibleAppId).toBeUndefined();
32
+ expect(stored.transport?.clientOs).toBe("macos");
33
+ expect(stored.transport?.clientTimezone).toBe("America/Denver");
34
+ expect(stored.transport?.interfaceId).toBe("web");
35
+ });
36
+
37
+ test("does not mutate the caller's options, which still drive this turn", () => {
38
+ const options: ConversationCreateOptions = {
39
+ transport: {
40
+ channelId: "vellum",
41
+ interfaceId: "web",
42
+ visibleAppId: "app-on-screen",
43
+ },
44
+ };
45
+
46
+ withoutTurnScopedTransport(options);
47
+
48
+ expect(options.transport?.visibleAppId).toBe("app-on-screen");
49
+ });
50
+
51
+ test("passes options through untouched when there is no app in view", () => {
52
+ const options: ConversationCreateOptions = {
53
+ transport: { channelId: "vellum", interfaceId: "web", clientOs: "ios" },
54
+ };
55
+
56
+ expect(withoutTurnScopedTransport(options)).toBe(options);
57
+ });
58
+
59
+ test("passes options through untouched when there is no transport", () => {
60
+ const options: ConversationCreateOptions = {};
61
+
62
+ expect(withoutTurnScopedTransport(options)).toBe(options);
63
+ });
64
+ });
@@ -138,6 +138,12 @@ mock.module("../telemetry/turn-outcome.js", () => ({
138
138
  stampTurnOutcome: mockStampTurnOutcome,
139
139
  }));
140
140
 
141
+ const emitAssistantReplyNotificationMock = mock(async () => {});
142
+
143
+ mock.module("../notifications/assistant-reply-producer.js", () => ({
144
+ emitAssistantReplyNotification: emitAssistantReplyNotificationMock,
145
+ }));
146
+
141
147
  let linkAttachmentShouldThrow = false;
142
148
  let mockAttachmentIdCounter = 0;
143
149
 
@@ -669,6 +675,52 @@ describe("Conversation message queue", () => {
669
675
  expect(pendingRuns.length).toBe(3);
670
676
  });
671
677
 
678
+ test("[experimental] queued passthrough siblings viewing different apps do NOT batch", async () => {
679
+ // A batched turn applies only the head's `visibleAppId`, which drives the
680
+ // `visible_app:` context line. Coalescing messages sent while different
681
+ // apps were on screen would point "the app" at the head's app for both.
682
+ const conversation = makeConversation();
683
+ await conversation.loadFromDb();
684
+
685
+ const p1 = conversation.processMessage({
686
+ content: "msg-1",
687
+ attachments: [],
688
+ onEvent: () => {},
689
+ requestId: "req-1",
690
+ });
691
+ await waitForPendingRun(1);
692
+
693
+ conversation.enqueueMessage({
694
+ content: "msg-2",
695
+ onEvent: () => {},
696
+ requestId: "req-2",
697
+ transport: {
698
+ channelId: "vellum",
699
+ interfaceId: "web",
700
+ visibleAppId: "app-a",
701
+ },
702
+ });
703
+ conversation.enqueueMessage({
704
+ content: "msg-3",
705
+ onEvent: () => {},
706
+ requestId: "req-3",
707
+ transport: {
708
+ channelId: "vellum",
709
+ interfaceId: "web",
710
+ visibleAppId: "app-b",
711
+ },
712
+ });
713
+ expect(conversation.getQueueDepth()).toBe(2);
714
+
715
+ await resolveRun(0);
716
+ await p1;
717
+ await waitForPendingRun(2);
718
+ await resolveRun(1);
719
+ await waitForPendingRun(3);
720
+
721
+ expect(pendingRuns.length).toBe(3);
722
+ });
723
+
672
724
  test("message_queued and message_dequeued events are emitted", async () => {
673
725
  const conversation = makeConversation();
674
726
  await conversation.loadFromDb();
@@ -1793,6 +1845,80 @@ describe("Batched drain correctness fixes", () => {
1793
1845
  await new Promise((r) => setTimeout(r, 10));
1794
1846
  });
1795
1847
 
1848
+ // The batch's shared reply answers the person's prompt; the trailing row
1849
+ // only rode along behind it. A channel send gets its reply delivered back to
1850
+ // Slack and a hidden marker is nobody's prompt, so pointing the producer at
1851
+ // either would suppress a push the user is waiting on.
1852
+ // expectEcho pins the deliberate asymmetry between the two gates: the
1853
+ // echo broadcast uses the narrower echo-suppression predicate, so a
1854
+ // channel send is push-ineligible yet must still echo to passive devices.
1855
+ const TRAILING_INELIGIBLE_ROW_CASES: Array<{
1856
+ name: string;
1857
+ metadata: Record<string, unknown>;
1858
+ expectEcho: boolean;
1859
+ }> = [
1860
+ { name: "hidden marker", metadata: { hidden: true }, expectEcho: false },
1861
+ {
1862
+ name: "channel send",
1863
+ metadata: { userMessageChannel: "slack" },
1864
+ expectEcho: true,
1865
+ },
1866
+ ];
1867
+
1868
+ for (const { name, metadata, expectEcho } of TRAILING_INELIGIBLE_ROW_CASES) {
1869
+ test(`drainBatch notifies about the genuine prompt, not a trailing ${name}`, async () => {
1870
+ emitAssistantReplyNotificationMock.mockClear();
1871
+ const conversation = makeConversation();
1872
+ await conversation.loadFromDb();
1873
+
1874
+ const p1 = conversation.processMessage({
1875
+ content: "msg-1",
1876
+ attachments: [],
1877
+ requestId: "req-1",
1878
+ });
1879
+ await waitForPendingRun(1);
1880
+
1881
+ conversation.enqueueMessage({
1882
+ content: "batch-prompt-genuine",
1883
+ requestId: "req-prompt",
1884
+ });
1885
+ const trailingEvents: AssistantEvent[] = [];
1886
+ conversation.enqueueMessage({
1887
+ content: "batch-prompt-trailing",
1888
+ requestId: "req-trailing",
1889
+ metadata,
1890
+ onEvent: (e) => {
1891
+ trailingEvents.push(e);
1892
+ },
1893
+ });
1894
+
1895
+ await resolveRun(0);
1896
+ await p1;
1897
+ await waitForPendingRun(2);
1898
+ await resolveRun(1);
1899
+ // Two turns finish here: msg-1's own run, then the batch's shared run.
1900
+ await waitForCondition(
1901
+ () => emitAssistantReplyNotificationMock.mock.calls.length >= 2,
1902
+ );
1903
+
1904
+ const genuineRow = capturedAddMessages.find((m) =>
1905
+ m.content.includes("batch-prompt-genuine"),
1906
+ );
1907
+ expect(genuineRow?.id).toBeDefined();
1908
+
1909
+ const notifyCalls = emitAssistantReplyNotificationMock.mock
1910
+ .calls as unknown as Array<[{ userMessageId: string | undefined }]>;
1911
+ expect(notifyCalls.at(-1)?.[0].userMessageId).toBe(genuineRow!.id);
1912
+
1913
+ const trailingEchoes = trailingEvents.filter(
1914
+ (e) => e.type === "user_message_echo",
1915
+ );
1916
+ expect(trailingEchoes.length).toBe(expectEcho ? 1 : 0);
1917
+
1918
+ await new Promise((r) => setTimeout(r, 10));
1919
+ });
1920
+ }
1921
+
1796
1922
  // Defensive recovery path: buildPassthroughBatch is designed to make
1797
1923
  // the invariant throw unreachable in practice, so neither the head
1798
1924
  // branch (re-dispatch batch.slice(1) to drainBatch/drainSingleMessage/
@@ -17,6 +17,15 @@ import { beforeEach, describe, expect, mock, test } from "bun:test";
17
17
 
18
18
  import { z } from "zod";
19
19
 
20
+ // The route's hidden-prompt branch is exactly what these pure predicates
21
+ // decide, so the crud mock below hands back the real ones rather than a
22
+ // restatement that can drift. Imported from the `conversation-types` leaf so
23
+ // naming them here pulls in no DB graph.
24
+ import {
25
+ isBackgroundEventMetadata,
26
+ isEchoSuppressedUserMessage,
27
+ } from "../persistence/conversation-types.js";
28
+
20
29
  mock.module("../config/env.js", () => ({
21
30
  isHttpAuthDisabled: () => true,
22
31
  hasUngatedHttpAuthDisabled: () => false,
@@ -24,13 +33,6 @@ mock.module("../config/env.js", () => ({
24
33
 
25
34
  mock.module("../daemon/conversation-process.js", () => ({
26
35
  formatSummarizeUpToResult: () => "",
27
- isEchoSuppressedUserMessage: (
28
- metadata: Record<string, unknown> | undefined,
29
- ) =>
30
- metadata?.hidden === true ||
31
- typeof metadata?.backgroundEventSource === "string",
32
- isBackgroundEventMetadata: (metadata: Record<string, unknown> | undefined) =>
33
- typeof metadata?.backgroundEventSource === "string",
34
36
  }));
35
37
 
36
38
  mock.module("../daemon/handlers/conversations.js", () => ({
@@ -77,6 +79,8 @@ mock.module("../persistence/conversation-crud.js", () => ({
77
79
  extractImageSourcePaths: () => undefined,
78
80
  forkConversation: () => ({ id: "forked" }),
79
81
  getConversation: getConversationMock,
82
+ isBackgroundEventMetadata,
83
+ isEchoSuppressedUserMessage,
80
84
  provenanceFromTrustContext: () => ({ provenanceTrustClass: "unknown" }),
81
85
  setConversationSurfaced: () => null,
82
86
  unarchiveConversation: () => true,
@@ -316,6 +320,41 @@ describe("POST /v1/conversations/:id/retry", () => {
316
320
  expect(typeof (options as { onEvent?: unknown }).onEvent).toBe("function");
317
321
  });
318
322
 
323
+ // The re-run carries none of the anchor's original delivery orchestration,
324
+ // so a channel or voice anchor's regenerated reply reaches SSE subscribers
325
+ // alone and the reply push must not read the anchor's origin markers.
326
+ const OFF_APP_ANCHOR_CASES: Array<{ name: string; metadata: unknown }> = [
327
+ { name: "Slack", metadata: { userMessageChannel: "slack" } },
328
+ {
329
+ name: "voice-session",
330
+ metadata: { voiceSessionTurn: true, userMessageChannel: "phone" },
331
+ },
332
+ { name: "plain in-app", metadata: { userMessageChannel: "vellum" } },
333
+ ];
334
+
335
+ for (const { name, metadata } of OFF_APP_ANCHOR_CASES) {
336
+ test(`marks the re-run of a ${name} anchor as app-only delivery`, async () => {
337
+ discardResult = {
338
+ anchor: anchorRow({ metadata: JSON.stringify(metadata) }),
339
+ deletedMessageIds: ["assistant-msg-1"],
340
+ };
341
+ const ctx = makeConversation();
342
+ activeConversation = ctx.conversation;
343
+
344
+ const res = await callHandler(
345
+ retryHandler,
346
+ makeRequest(),
347
+ { id: "conv-retry-test" },
348
+ 202,
349
+ );
350
+ expect(res.status).toBe(202);
351
+ await settle();
352
+
353
+ const [, , options] = ctx.runAgentLoop.mock.calls[0];
354
+ expect(options).toMatchObject({ replyDeliveredInAppOnly: true });
355
+ });
356
+ }
357
+
319
358
  test("legacy background-event anchor (no recorded mode) re-runs hidden AND non-interactive", async () => {
320
359
  // Anchors persisted before `backgroundEventInteractive` existed carry only
321
360
  // `backgroundEventSource`. Fall back to non-interactive so a retried
@@ -51,7 +51,8 @@ describe("/model list", () => {
51
51
  const message = ((await resolveSlash("/model")) as { message: string })
52
52
  .message;
53
53
  expect(message).toContain("Best results with the most capable model");
54
- expect(message).toContain("Fastest responses at lower cost");
54
+ expect(message).toContain("Cheapest responses, for high-volume work");
55
+ expect(message).toContain("Fastest responses, with reasoning turned off");
55
56
  expect(message).not.toContain("(DeepSeek V4 Flash)");
56
57
  });
57
58
 
@@ -63,7 +64,8 @@ describe("/model list", () => {
63
64
  expect(message).toContain(
64
65
  "High-quality results with the most capable model",
65
66
  );
66
- expect(message).toContain("Fastest responses at lower cost");
67
+ expect(message).toContain("Cheapest responses, for high-volume work");
68
+ expect(message).toContain("Fastest responses, with reasoning turned off");
67
69
  expect(message).not.toContain("(DeepSeek V4 Flash)");
68
70
  });
69
71
  });
@@ -257,7 +257,13 @@ describe("POST /v1/conversations/summarize", () => {
257
257
 
258
258
  await settle();
259
259
 
260
- expect(ctx.summarizeUpToMessage).toHaveBeenCalledWith("msg-42");
260
+ // The second arg is the sink the context-window usage push rides. This
261
+ // route's conversation may hold the store's no-op sender, so it hands in
262
+ // the broadcast path its result card goes out on.
263
+ expect(ctx.summarizeUpToMessage).toHaveBeenCalledWith(
264
+ "msg-42",
265
+ expect.any(Function),
266
+ );
261
267
  expect(ctx.emitActivityState).toHaveBeenCalledWith(
262
268
  "thinking",
263
269
  "context_compacting",