@vellumai/assistant 0.12.3-staging.1 → 0.12.3-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 (560) hide show
  1. package/ARCHITECTURE.md +43 -29
  2. package/Dockerfile +63 -0
  3. package/docs/architecture/security.md +10 -5
  4. package/docs/architecture/turn-actor.md +2 -0
  5. package/docs/desktop-browser-cli.md +45 -4
  6. package/docs/live-voice-task-outcomes.md +100 -0
  7. package/docs/mode-sessions.md +183 -0
  8. package/docs/voice-input-diagnostics.md +58 -0
  9. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  10. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  11. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/integration-categories.ts +36 -0
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/integration-categories.ts +36 -0
  15. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +20 -0
  16. package/node_modules/@vellumai/gateway-client/src/index.ts +4 -1
  17. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +38 -7
  18. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +5 -6
  19. package/node_modules/@vellumai/service-contracts/package.json +2 -1
  20. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  21. package/node_modules/@vellumai/service-contracts/src/integration-categories.ts +36 -0
  22. package/node_modules/@vellumai/slack-text/src/index.ts +4 -8
  23. package/openapi.yaml +665 -14
  24. package/package.json +1 -1
  25. package/scripts/smoke-desktop-browser-cli.ts +10 -26
  26. package/scripts/smoke-desktop-window-theme.ts +2 -3
  27. package/src/__tests__/agent-loop-output-hooks.test.ts +150 -1
  28. package/src/__tests__/agent-loop-provider-error-recording.test.ts +87 -19
  29. package/src/__tests__/annotate-activity-metadata.test.ts +3 -1
  30. package/src/__tests__/annotate-risk-options.test.ts +2 -0
  31. package/src/__tests__/answered-question-persistence.test.ts +2 -0
  32. package/src/__tests__/anthropic-gateway-shared.test.ts +22 -0
  33. package/src/__tests__/approval-message-composer.test.ts +1 -118
  34. package/src/__tests__/approval-routes-http.test.ts +3 -0
  35. package/src/__tests__/assistant-event-hub-connection-history.test.ts +111 -0
  36. package/src/__tests__/assistant-event-hub-targeted.test.ts +50 -0
  37. package/src/__tests__/assistant-event-targeting.test.ts +186 -0
  38. package/src/__tests__/assistant-stream-state.test.ts +29 -9
  39. package/src/__tests__/browser-fill-credential.test.ts +11 -22
  40. package/src/__tests__/btw-routes.test.ts +4 -1
  41. package/src/__tests__/build-persisted-content.test.ts +1 -1
  42. package/src/__tests__/call-conversation-messages.test.ts +5 -0
  43. package/src/__tests__/call-recovery.test.ts +7 -2
  44. package/src/__tests__/call-routes-http.test.ts +12 -0
  45. package/src/__tests__/call-store.test.ts +7 -2
  46. package/src/__tests__/channel-retry-sweep.test.ts +9 -0
  47. package/src/__tests__/chat-reveal-guard-priming.test.ts +2 -0
  48. package/src/__tests__/checker.test.ts +11 -3
  49. package/src/__tests__/client-os-metadata-persistence.test.ts +35 -1
  50. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +0 -25
  51. package/src/__tests__/computer-use-tools.test.ts +1 -233
  52. package/src/__tests__/config-model-image-provider.test.ts +39 -0
  53. package/src/__tests__/config-schema.test.ts +16 -0
  54. package/src/__tests__/config-watcher.test.ts +24 -0
  55. package/src/__tests__/context-search-agent-runner.test.ts +260 -1
  56. package/src/__tests__/context-search-fanout.test.ts +1 -1
  57. package/src/__tests__/context-search-types.test.ts +7 -7
  58. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +8 -2
  59. package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +2 -0
  60. package/src/__tests__/conversation-agent-loop.test.ts +546 -16
  61. package/src/__tests__/conversation-app-control-lifecycle.test.ts +53 -1
  62. package/src/__tests__/conversation-evictor.test.ts +62 -37
  63. package/src/__tests__/conversation-history-boundary-repair-failure.test.ts +174 -0
  64. package/src/__tests__/conversation-loop-db-lock-resilience.test.ts +2 -0
  65. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
  66. package/src/__tests__/conversation-retry-route.test.ts +22 -0
  67. package/src/__tests__/conversation-routes-disk-view.test.ts +2 -0
  68. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +2 -0
  69. package/src/__tests__/conversation-routes-guardian-reply.test.ts +10 -0
  70. package/src/__tests__/conversation-routes-hidden-queue.test.ts +2 -0
  71. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -0
  72. package/src/__tests__/conversation-runtime-assembly.test.ts +29 -0
  73. package/src/__tests__/conversation-surface-snapshots.test.ts +543 -0
  74. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +151 -0
  75. package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +63 -1
  76. package/src/__tests__/conversation-tool-setup-attribution.test.ts +23 -0
  77. package/src/__tests__/conversation-undo.test.ts +61 -0
  78. package/src/__tests__/credential-execution-client.test.ts +6 -6
  79. package/src/__tests__/credential-prompt-route.test.ts +35 -3
  80. package/src/__tests__/cu-unified-flow.test.ts +82 -1
  81. package/src/__tests__/db-mode-sessions-migration.test.ts +175 -0
  82. package/src/__tests__/deterministic-verification-control-plane.test.ts +2 -0
  83. package/src/__tests__/dm-backfill.test.ts +91 -1
  84. package/src/__tests__/document-create-route.test.ts +88 -0
  85. package/src/__tests__/document-sync-tags.test.ts +27 -0
  86. package/src/__tests__/emit-signal-routing-intent.test.ts +142 -2
  87. package/src/__tests__/events-tail-route.test.ts +25 -0
  88. package/src/__tests__/evict-conversations-for-reload.test.ts +57 -8
  89. package/src/__tests__/guardian-action-sweep.test.ts +16 -0
  90. package/src/__tests__/guardian-dispatch.test.ts +13 -0
  91. package/src/__tests__/headless-browser-interactions.test.ts +64 -20
  92. package/src/__tests__/heartbeat-service.test.ts +2 -59
  93. package/src/__tests__/helpers/mock-conversation.ts +21 -0
  94. package/src/__tests__/helpers/native-web-search-harness.ts +2 -0
  95. package/src/__tests__/helpers/verification-sessions-ipc-sim.ts +5 -0
  96. package/src/__tests__/host-cu-proxy.test.ts +46 -2
  97. package/src/__tests__/http-user-message-parity.test.ts +2 -0
  98. package/src/__tests__/image-credentials.test.ts +49 -0
  99. package/src/__tests__/image-service-dispatcher.test.ts +45 -0
  100. package/src/__tests__/inference-profile-session-handler.test.ts +1 -1
  101. package/src/__tests__/interrupt-on-send.test.ts +57 -43
  102. package/src/__tests__/interrupt-turn-note.test.ts +5 -6
  103. package/src/__tests__/llm-catalog-parity.test.ts +2 -2
  104. package/src/__tests__/media-generate-image.test.ts +49 -1
  105. package/src/__tests__/message-provenance-contact-id.test.ts +69 -0
  106. package/src/__tests__/migration-export-to-gcs.test.ts +142 -0
  107. package/src/__tests__/mode-session-history.test.ts +245 -0
  108. package/src/__tests__/mode-session-recovery.test.ts +222 -0
  109. package/src/__tests__/mode-session-wire.test.ts +141 -0
  110. package/src/__tests__/notification-guardian-path.test.ts +7 -0
  111. package/src/__tests__/notification-telegram-adapter.test.ts +2 -2
  112. package/src/__tests__/notification-vellum-adapter.test.ts +34 -0
  113. package/src/__tests__/oauth-provider-serializer.test.ts +12 -0
  114. package/src/__tests__/oauth-providers-routes.test.ts +1 -0
  115. package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
  116. package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
  117. package/src/__tests__/plugin-api-provider.test.ts +5 -0
  118. package/src/__tests__/plugin-api-resolve-credential.test.ts +97 -0
  119. package/src/__tests__/plugin-import-boundary-guard.test.ts +9 -0
  120. package/src/__tests__/process-message-display-content.test.ts +56 -0
  121. package/src/__tests__/reaction-persistence.test.ts +5 -0
  122. package/src/__tests__/request-secret-standalone-channel-gate.test.ts +25 -9
  123. package/src/__tests__/runtime-events-sse-reconnect.test.ts +1 -1
  124. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +73 -0
  125. package/src/__tests__/secret-ingress-http.test.ts +2 -0
  126. package/src/__tests__/secure-keys.test.ts +43 -1
  127. package/src/__tests__/send-endpoint-busy.test.ts +163 -1
  128. package/src/__tests__/server-history-render.test.ts +91 -1
  129. package/src/__tests__/settings-routes.test.ts +50 -1
  130. package/src/__tests__/skill-script-runner-sandbox.test.ts +32 -0
  131. package/src/__tests__/subagent-fork-notifications.test.ts +1 -2
  132. package/src/__tests__/subagent-manager-notify.test.ts +1 -2
  133. package/src/__tests__/subagent-notify-parent.test.ts +146 -39
  134. package/src/__tests__/subagent-notify-stale-rebuild.test.ts +2 -4
  135. package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +2 -0
  136. package/src/__tests__/thread-backfill.test.ts +53 -0
  137. package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
  138. package/src/__tests__/tool-result-metadata-plumbing.test.ts +3 -1
  139. package/src/__tests__/tool-start-timestamp.test.ts +2 -0
  140. package/src/__tests__/twilio-routes.test.ts +1 -0
  141. package/src/__tests__/web-search-backend-failure.test.ts +1 -1
  142. package/src/activation/progress-store.test.ts +12 -7
  143. package/src/agent/loop.ts +99 -41
  144. package/src/api/events/assistant-turn-start.ts +3 -0
  145. package/src/api/events/generation-handoff.ts +2 -0
  146. package/src/api/events/message-complete.ts +2 -0
  147. package/src/api/events/notification-conversation-created.ts +14 -16
  148. package/src/api/events/notification-intent.ts +4 -2
  149. package/src/api/events/tool-result.ts +85 -0
  150. package/src/api/events/tool-use-start.ts +3 -0
  151. package/src/api/events/user-message-echo.ts +3 -0
  152. package/src/api/index.ts +31 -6
  153. package/src/api/mode-session.test.ts +131 -0
  154. package/src/api/mode-session.ts +148 -0
  155. package/src/api/remember-tool.ts +15 -0
  156. package/src/api/responses/conversation-message.ts +6 -0
  157. package/src/approvals/AGENTS.md +4 -2
  158. package/src/browser/__tests__/operations.test.ts +29 -0
  159. package/src/browser/operation-meta.ts +2 -1
  160. package/src/browser/operations.ts +29 -0
  161. package/src/browser/virtual-desktop-target.ts +2 -8
  162. package/src/bundler/bundle-signer.ts +1 -3
  163. package/src/calls/__tests__/barge-in-guard.test.ts +3 -0
  164. package/src/calls/__tests__/call-controller.test.ts +94 -0
  165. package/src/calls/__tests__/call-funnel-telemetry.test.ts +204 -0
  166. package/src/calls/__tests__/voice-control-protocol.test.ts +15 -0
  167. package/src/calls/__tests__/voice-escalation-judge.test.ts +209 -0
  168. package/src/calls/__tests__/voice-leg-coordinator.test.ts +168 -0
  169. package/src/{live-voice/__tests__/live-voice-metrics.test.ts → calls/__tests__/voice-metrics.test.ts} +32 -38
  170. package/src/calls/__tests__/voice-session-bridge.test.ts +576 -122
  171. package/src/calls/barge-in-guard.ts +5 -0
  172. package/src/calls/call-controller.ts +196 -1
  173. package/src/calls/call-domain.ts +4 -0
  174. package/src/calls/call-store.ts +87 -2
  175. package/src/calls/guardian-action-sweep.ts +22 -8
  176. package/src/calls/media-stream-audio-transcode.ts +1 -3
  177. package/src/calls/typesafe-noul.ts +156 -0
  178. package/src/calls/voice-control-protocol.ts +6 -0
  179. package/src/calls/voice-escalation-judge.ts +145 -0
  180. package/src/calls/voice-escalation-target.ts +10 -0
  181. package/src/calls/voice-leg-coordinator.ts +121 -3
  182. package/src/{live-voice/live-voice-metrics.ts → calls/voice-metrics.ts} +94 -87
  183. package/src/calls/voice-session-bridge.ts +256 -112
  184. package/src/calls/voice-triage-escalate.ts +7 -0
  185. package/src/cli/__tests__/roadmap-cli.test.ts +4 -41
  186. package/src/cli/commands/__tests__/notifications.test.ts +35 -10
  187. package/src/cli/commands/browser.help.ts +4 -5
  188. package/src/cli/commands/browser.ts +0 -1
  189. package/src/cli/commands/clients.help.ts +46 -0
  190. package/src/cli/commands/clients.ts +116 -0
  191. package/src/cli/commands/credentials.help.ts +6 -1
  192. package/src/cli/commands/notifications.help.ts +16 -2
  193. package/src/cli/commands/notifications.ts +30 -8
  194. package/src/cli/commands/roadmap.help.ts +18 -21
  195. package/src/cli/commands/roadmap.ts +4 -21
  196. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +5 -2
  197. package/src/cli/lib/bundled-marketplace.json +84 -34
  198. package/src/cli/lib/plugin-marketplace.ts +8 -0
  199. package/src/cli/lib/publish-plugin.ts +1 -2
  200. package/src/config/__tests__/default-provider.test.ts +3 -1
  201. package/src/config/__tests__/memory-retrospective-schema.test.ts +14 -1
  202. package/src/config/bundled-skills/computer-use/SKILL.md +1 -1
  203. package/src/config/bundled-skills/computer-use/TOOLS.json +89 -1
  204. package/src/config/bundled-skills/image-studio/SKILL.md +17 -2
  205. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +7 -11
  206. package/src/config/bundled-skills/sequences/TOOLS.json +1 -5
  207. package/src/config/call-site-defaults.ts +9 -0
  208. package/src/config/feature-flag-registry.json +16 -0
  209. package/src/config/profile-text-generation.test.ts +2 -2
  210. package/src/config/schemas/__tests__/tools.test.ts +31 -0
  211. package/src/config/schemas/call-site-catalog.ts +14 -0
  212. package/src/config/schemas/channels.ts +0 -18
  213. package/src/config/schemas/llm.ts +3 -1
  214. package/src/config/schemas/mcp.ts +2 -1
  215. package/src/config/schemas/memory-retrospective.ts +11 -2
  216. package/src/config/schemas/services.ts +15 -6
  217. package/src/config/schemas/tools.ts +10 -0
  218. package/src/config/session-groups-gate.ts +21 -0
  219. package/src/config/skills.ts +2 -6
  220. package/src/conversations/__tests__/message-consolidation.test.ts +149 -0
  221. package/src/conversations/message-consolidation.ts +102 -5
  222. package/src/credential-execution/ces-connect.test.ts +69 -0
  223. package/src/credential-execution/ces-connect.ts +115 -0
  224. package/src/credential-execution/ces-runtime.ts +55 -93
  225. package/src/credential-execution/executable-discovery.ts +1 -11
  226. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +10 -1
  227. package/src/daemon/__tests__/inference-profile-notification.test.ts +2 -0
  228. package/src/daemon/browser-mode-session.test.ts +402 -0
  229. package/src/daemon/browser-mode-session.ts +186 -0
  230. package/src/daemon/computer-use-mode-session.test.ts +210 -0
  231. package/src/daemon/computer-use-mode-session.ts +105 -0
  232. package/src/daemon/config-watcher.ts +10 -0
  233. package/src/daemon/conversation-agent-loop-handlers.ts +58 -10
  234. package/src/daemon/conversation-agent-loop.ts +219 -25
  235. package/src/daemon/conversation-evictor.ts +5 -24
  236. package/src/daemon/conversation-history.ts +10 -0
  237. package/src/daemon/conversation-interrupt-repair.ts +4 -43
  238. package/src/daemon/conversation-interrupt.ts +7 -11
  239. package/src/daemon/conversation-messaging.ts +58 -5
  240. package/src/daemon/conversation-mode-session.test.ts +1667 -0
  241. package/src/daemon/conversation-mode-session.ts +1008 -0
  242. package/src/daemon/conversation-process.ts +5 -0
  243. package/src/daemon/conversation-runtime-assembly.ts +10 -20
  244. package/src/daemon/conversation-skill-tools.ts +1 -0
  245. package/src/daemon/conversation-store.ts +4 -4
  246. package/src/daemon/conversation-surface-snapshots.ts +135 -0
  247. package/src/daemon/conversation-surfaces.ts +113 -5
  248. package/src/daemon/conversation-tool-setup.ts +8 -3
  249. package/src/daemon/conversation.ts +143 -41
  250. package/src/daemon/handlers/config-model.ts +7 -5
  251. package/src/daemon/handlers/shared.ts +55 -16
  252. package/src/daemon/host-cu-proxy.ts +51 -22
  253. package/src/daemon/lifecycle.ts +30 -13
  254. package/src/daemon/message-protocol.ts +0 -1
  255. package/src/daemon/message-provenance.ts +28 -4
  256. package/src/daemon/mode-session-startup-recovery.ts +23 -0
  257. package/src/daemon/mode-session-tracking.ts +54 -0
  258. package/src/daemon/process-message.ts +13 -4
  259. package/src/daemon/virtual-desktop-context.ts +21 -0
  260. package/src/desktop/__tests__/fake-desktop.ts +1 -0
  261. package/src/desktop/desktop-accessibility-script.ts +88 -0
  262. package/src/desktop/desktop-accessibility.test.ts +160 -0
  263. package/src/desktop/desktop-accessibility.ts +169 -0
  264. package/src/desktop/desktop-automation-lease.test.ts +76 -68
  265. package/src/desktop/desktop-automation-lease.ts +41 -50
  266. package/src/desktop/desktop-browser-endpoint.ts +4 -0
  267. package/src/desktop/desktop-computer-use-routing.test.ts +330 -0
  268. package/src/desktop/desktop-computer-use.test.ts +449 -0
  269. package/src/desktop/desktop-computer-use.ts +502 -0
  270. package/src/desktop/desktop-dependencies.ts +47 -293
  271. package/src/desktop/desktop-display.ts +2 -2
  272. package/src/desktop/desktop-panel-config.test.ts +13 -3
  273. package/src/desktop/desktop-panel-config.ts +109 -15
  274. package/src/desktop/desktop-session-manager.test.ts +69 -21
  275. package/src/desktop/desktop-session-manager.ts +76 -22
  276. package/src/desktop/desktop-stream-bridge.test.ts +11 -122
  277. package/src/desktop/desktop-stream-bridge.ts +6 -8
  278. package/src/desktop/desktop-window-theme.ts +5 -25
  279. package/src/desktop/virtual-desktop-feature.ts +52 -2
  280. package/src/desktop/virtual-desktop-platform.test.ts +5 -9
  281. package/src/documents/document-store.ts +23 -0
  282. package/src/heartbeat/__tests__/heartbeat-service.test.ts +22 -26
  283. package/src/heartbeat/heartbeat-service.ts +5 -33
  284. package/src/ipc/__tests__/browser-ipc.test.ts +215 -6
  285. package/src/ipc/assistant-server.ts +2 -0
  286. package/src/ipc/routes/__tests__/channel-reply-ipc-routes.test.ts +111 -0
  287. package/src/ipc/routes/channel-reply-ipc-routes.ts +73 -0
  288. package/src/live-voice/__tests__/continuation-judge.test.ts +151 -0
  289. package/src/live-voice/__tests__/live-activity-reporter.test.ts +154 -15
  290. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +743 -31
  291. package/src/live-voice/__tests__/live-voice-look-follow-up.test.ts +191 -10
  292. package/src/live-voice/__tests__/live-voice-session-groups-gate.test.ts +87 -0
  293. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +5 -1
  294. package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +41 -0
  295. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +1017 -9
  296. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +224 -1
  297. package/src/live-voice/__tests__/live-voice-tts-session.test.ts +101 -0
  298. package/src/live-voice/__tests__/live-voice-tts.test.ts +44 -0
  299. package/src/live-voice/__tests__/live-voice-vad.test.ts +939 -75
  300. package/src/live-voice/__tests__/microphone-idle-gate.test.ts +87 -0
  301. package/src/live-voice/__tests__/session-controls.test.ts +23 -0
  302. package/src/live-voice/__tests__/task-outcomes.test.ts +117 -0
  303. package/src/live-voice/__tests__/voice-input-diagnostics.test.ts +117 -0
  304. package/src/live-voice/camera-mode-session.test.ts +265 -0
  305. package/src/live-voice/camera-mode-session.ts +277 -0
  306. package/src/live-voice/continuation-judge.ts +123 -0
  307. package/src/live-voice/live-activity-reporter.ts +115 -6
  308. package/src/live-voice/live-voice-manager.ts +36 -1
  309. package/src/live-voice/live-voice-photo.ts +88 -7
  310. package/src/live-voice/live-voice-session-manager.ts +15 -0
  311. package/src/live-voice/live-voice-session.ts +1443 -747
  312. package/src/live-voice/live-voice-tts.ts +22 -4
  313. package/src/live-voice/microphone-idle-gate.ts +56 -0
  314. package/src/live-voice/protocol.ts +171 -4
  315. package/src/live-voice/session-controls.ts +11 -3
  316. package/src/live-voice/task-outcomes.ts +131 -0
  317. package/src/live-voice/voice-input-diagnostics.ts +117 -0
  318. package/src/mcp/__tests__/manager-tool-caps.test.ts +63 -18
  319. package/src/mcp/__tests__/tool-caps.test.ts +28 -1
  320. package/src/mcp/manager.ts +3 -0
  321. package/src/mcp/tool-caps.ts +12 -4
  322. package/src/media/__tests__/image-models.test.ts +58 -0
  323. package/src/media/image-credentials.ts +5 -0
  324. package/src/media/image-models.ts +52 -0
  325. package/src/media/image-service.ts +8 -0
  326. package/src/media/openrouter-image-service.test.ts +133 -0
  327. package/src/media/openrouter-image-service.ts +153 -0
  328. package/src/media/types.ts +1 -1
  329. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +65 -11
  330. package/src/messaging/providers/discord/send.ts +4 -17
  331. package/src/messaging/providers/discord/transport.ts +2 -2
  332. package/src/messaging/providers/outlook/client.ts +16 -22
  333. package/src/messaging/providers/telegram-bot/send.test.ts +236 -1
  334. package/src/messaging/providers/telegram-bot/send.ts +10 -7
  335. package/src/messaging/providers/telegram-bot/transport.ts +3 -10
  336. package/src/messaging/providers/whatsapp/send.test.ts +169 -4
  337. package/src/monitoring/__tests__/control.test.ts +17 -5
  338. package/src/monitoring/control.ts +15 -5
  339. package/src/notifications/AGENTS.md +1 -1
  340. package/src/notifications/README.md +83 -99
  341. package/src/notifications/__tests__/broadcaster.test.ts +1 -1
  342. package/src/notifications/__tests__/channel-allowlist.test.ts +39 -0
  343. package/src/notifications/__tests__/copy-composer.test.ts +1 -1
  344. package/src/notifications/__tests__/decision-engine.test.ts +79 -2
  345. package/src/notifications/__tests__/deterministic-checks.test.ts +10 -10
  346. package/src/notifications/__tests__/home-feed-side-effect.test.ts +93 -4
  347. package/src/notifications/__tests__/notification-utils.test.ts +18 -0
  348. package/src/notifications/adapters/macos.ts +37 -46
  349. package/src/notifications/broadcaster.ts +5 -6
  350. package/src/notifications/channel-allowlist.ts +44 -0
  351. package/src/notifications/copy-composer.ts +0 -23
  352. package/src/notifications/decision-engine.ts +24 -14
  353. package/src/notifications/deterministic-checks.ts +1 -1
  354. package/src/notifications/emit-signal.ts +123 -37
  355. package/src/notifications/home-feed-side-effect.ts +9 -7
  356. package/src/notifications/notification-utils.ts +19 -0
  357. package/src/notifications/signal.ts +4 -19
  358. package/src/notifications/types.ts +15 -0
  359. package/src/oauth/connection-resolver.ts +1 -2
  360. package/src/oauth/oauth-store.ts +1 -3
  361. package/src/oauth/provider-serializer.ts +7 -0
  362. package/src/oauth/seed-providers.ts +30 -0
  363. package/src/onboarding/onboarding-events-store.ts +99 -12
  364. package/src/persistence/__tests__/client-connection-events-store.test.ts +322 -0
  365. package/src/persistence/client-connection-events-store.ts +377 -0
  366. package/src/persistence/conversation-crud.ts +55 -6
  367. package/src/persistence/conversation-mode-sessions.test.ts +424 -0
  368. package/src/persistence/conversation-mode-sessions.ts +460 -0
  369. package/src/persistence/conversation-queries.ts +6 -19
  370. package/src/persistence/llm-usage-store.ts +3 -9
  371. package/src/persistence/message-metadata.ts +25 -0
  372. package/src/persistence/migrations/381-create-conversation-mode-sessions.ts +41 -0
  373. package/src/persistence/migrations/382-create-client-connection-events.ts +40 -0
  374. package/src/persistence/migrations/__tests__/382-create-client-connection-events.test.ts +56 -0
  375. package/src/persistence/schema/conversations.ts +67 -0
  376. package/src/persistence/schema/infrastructure.ts +29 -0
  377. package/src/persistence/steps.ts +17 -0
  378. package/src/plugin-api/__tests__/plugin-name-env.test.ts +249 -0
  379. package/src/plugin-api/effective-context-window.ts +33 -0
  380. package/src/plugin-api/index.ts +24 -5
  381. package/src/plugin-api/plugin-name-env.ts +125 -0
  382. package/src/plugin-api/resolve-credential.ts +79 -20
  383. package/src/plugin-api/vision-support.test.ts +22 -0
  384. package/src/plugin-api/vision-support.ts +17 -5
  385. package/src/plugins/__tests__/active-plugin-env.test.ts +136 -0
  386. package/src/plugins/active-plugin-env.ts +51 -0
  387. package/src/plugins/defaults/index.ts +24 -0
  388. package/src/plugins/defaults/memory/__tests__/archive-removal-framing.test.ts +64 -0
  389. package/src/plugins/defaults/memory/__tests__/worker-control.test.ts +19 -6
  390. package/src/plugins/defaults/memory/context-search/agent-protocol.ts +9 -12
  391. package/src/plugins/defaults/memory/context-search/agent-runner.ts +78 -22
  392. package/src/plugins/defaults/memory/context-search/format.ts +51 -7
  393. package/src/plugins/defaults/memory/context-search/limits.ts +10 -12
  394. package/src/plugins/defaults/memory/context-search/recall-input.ts +38 -0
  395. package/src/plugins/defaults/memory/context-search/search.ts +8 -14
  396. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +1 -0
  397. package/src/plugins/defaults/memory/context-search/types.ts +1 -11
  398. package/src/plugins/defaults/memory/graph/__tests__/handle-remember-v2.test.ts +11 -4
  399. package/src/plugins/defaults/memory/graph/tool-handlers.ts +12 -21
  400. package/src/plugins/defaults/memory/graph/tools.ts +35 -64
  401. package/src/plugins/defaults/memory/src/memory-eval-routes.ts +2 -3
  402. package/src/plugins/defaults/memory/src/memory-item-routes.test.ts +5 -0
  403. package/src/plugins/defaults/memory/src/memory-item-routes.ts +2 -1
  404. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +2 -2
  405. package/src/plugins/defaults/memory/tools.test.ts +124 -9
  406. package/src/plugins/defaults/memory/tools.ts +38 -11
  407. package/src/plugins/defaults/memory/v3/__tests__/pool-log-store.test.ts +165 -27
  408. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +626 -27
  409. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +46 -27
  410. package/src/plugins/defaults/memory/v3/orchestrate.ts +78 -38
  411. package/src/plugins/defaults/memory/v3/pool-log-store.ts +65 -79
  412. package/src/plugins/defaults/memory/v3/pool-select.test.ts +14 -5
  413. package/src/plugins/defaults/memory/v3/pool-select.ts +683 -44
  414. package/src/plugins/defaults/memory/worker-control.ts +15 -5
  415. package/src/plugins/defaults/task-progress/__tests__/registration.test.ts +70 -0
  416. package/src/plugins/defaults/task-progress/__tests__/task-progress-context.test.ts +380 -0
  417. package/src/plugins/defaults/task-progress/hooks/post-compact.ts +19 -0
  418. package/src/plugins/defaults/task-progress/hooks/user-prompt-submit.ts +24 -0
  419. package/src/plugins/defaults/task-progress/package.json +14 -0
  420. package/src/plugins/defaults/task-progress/src/apply-task-progress-context.ts +93 -0
  421. package/src/plugins/defaults/task-progress/src/format-task-progress-context.ts +67 -0
  422. package/src/providers/__tests__/context-overflow-error.test.ts +10 -0
  423. package/src/providers/__tests__/search-provider-catalog.test.ts +18 -0
  424. package/src/providers/anthropic/client.ts +10 -1
  425. package/src/providers/anthropic-gateway-shared.ts +6 -0
  426. package/src/providers/fetch-provider-catalog.ts +11 -0
  427. package/src/providers/gemini/client.ts +25 -8
  428. package/src/providers/inference/adapter-factory.ts +1 -1
  429. package/src/providers/jev/client.test.ts +10 -0
  430. package/src/providers/jev/client.ts +20 -4
  431. package/src/providers/model-catalog.ts +4 -3
  432. package/src/providers/openai/__tests__/chat-completions-extra-body.test.ts +94 -1
  433. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +106 -1
  434. package/src/providers/openai/__tests__/chat-template-flatten.test.ts +24 -0
  435. package/src/providers/openai/__tests__/google-thought-signature.test.ts +1 -2
  436. package/src/providers/openai/chat-completions-provider.ts +100 -29
  437. package/src/providers/openai/google-thought-signature.ts +0 -6
  438. package/src/providers/openai/responses-provider.ts +23 -1
  439. package/src/providers/ratelimit.ts +1 -3
  440. package/src/providers/search-provider-catalog.ts +24 -0
  441. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +24 -0
  442. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +7 -1
  443. package/src/providers/speech-to-text/__tests__/vellum-managed-realtime.test.ts +50 -1
  444. package/src/providers/speech-to-text/deepgram-flux-frames.ts +11 -0
  445. package/src/providers/speech-to-text/vellum-managed-flux-realtime.ts +13 -0
  446. package/src/providers/types.ts +5 -0
  447. package/src/routes/control.ts +7 -5
  448. package/src/runtime/AGENTS.md +4 -2
  449. package/src/runtime/actor-trust-resolver.ts +3 -3
  450. package/src/runtime/agent-wake.ts +1 -1
  451. package/src/runtime/approval-message-composer.ts +3 -69
  452. package/src/runtime/assistant-event-hub.ts +73 -75
  453. package/src/runtime/assistant-event-publish-options.ts +7 -0
  454. package/src/runtime/assistant-event-targeting.ts +79 -0
  455. package/src/runtime/assistant-stream-state.ts +13 -100
  456. package/src/runtime/background-job-runner.ts +5 -5
  457. package/src/runtime/channel-retry-sweep.ts +1 -0
  458. package/src/runtime/guardian-action-message-composer.ts +8 -161
  459. package/src/runtime/guardian-reply-router.ts +28 -32
  460. package/src/runtime/http-types.ts +6 -0
  461. package/src/runtime/message-composer-types.ts +3 -61
  462. package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +67 -1
  463. package/src/runtime/migrations/vbundle-builder.ts +24 -1
  464. package/src/runtime/migrations/vbundle-import-analyzer.ts +13 -4
  465. package/src/runtime/migrations/vbundle-import-policy.ts +10 -0
  466. package/src/runtime/migrations/vbundle-importer.ts +4 -1
  467. package/src/runtime/migrations/vbundle-streaming-importer.ts +4 -1
  468. package/src/runtime/migrations/vbundle-validator.ts +1 -0
  469. package/src/runtime/pending-interactions-mode-session.test.ts +62 -0
  470. package/src/runtime/pending-interactions.ts +29 -0
  471. package/src/runtime/routes/__tests__/client-history-routes.test.ts +104 -0
  472. package/src/runtime/routes/__tests__/image-generation-routes.test.ts +97 -0
  473. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +1 -1
  474. package/src/runtime/routes/__tests__/roadmap-routes.test.ts +29 -6
  475. package/src/runtime/routes/browser-context.ts +31 -11
  476. package/src/runtime/routes/browser-routes.ts +43 -4
  477. package/src/runtime/routes/btw-routes.ts +9 -4
  478. package/src/runtime/routes/client-routes.ts +108 -0
  479. package/src/runtime/routes/conversation-management-routes.ts +4 -1
  480. package/src/runtime/routes/conversation-routes.ts +113 -27
  481. package/src/runtime/routes/credential-prompt-routes.ts +4 -3
  482. package/src/runtime/routes/credential-routes.ts +1 -2
  483. package/src/runtime/routes/desktop-setup-routes.test.ts +9 -18
  484. package/src/runtime/routes/desktop-setup-routes.ts +4 -6
  485. package/src/runtime/routes/documents-routes.ts +51 -0
  486. package/src/runtime/routes/email-routes.ts +3 -7
  487. package/src/runtime/routes/events-routes.ts +75 -54
  488. package/src/runtime/routes/identity-routes.ts +1 -5
  489. package/src/runtime/routes/image-generation-routes.ts +8 -14
  490. package/src/runtime/routes/inbound-message-handler.ts +35 -38
  491. package/src/runtime/routes/inbound-stages/backfill-sender-provenance.test.ts +168 -0
  492. package/src/runtime/routes/inbound-stages/backfill-sender-provenance.ts +96 -0
  493. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +38 -9
  494. package/src/runtime/routes/inbound-stages/background-dispatch.ts +2 -5
  495. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +2 -0
  496. package/src/runtime/routes/llm-context-normalization.ts +1 -7
  497. package/src/runtime/routes/migration-routes.ts +73 -2
  498. package/src/runtime/routes/notification-routes.ts +10 -0
  499. package/src/runtime/routes/oauth-providers.ts +7 -0
  500. package/src/runtime/routes/plugins-routes.ts +6 -0
  501. package/src/runtime/routes/roadmap-routes.ts +47 -8
  502. package/src/runtime/routes/secret-routes.ts +1 -1
  503. package/src/runtime/routes/settings-routes.ts +10 -4
  504. package/src/runtime/routes/watcher-routes.ts +1 -3
  505. package/src/runtime/trust-class.test.ts +7 -6
  506. package/src/runtime/trust-class.ts +9 -17
  507. package/src/schedule/__tests__/worker-control.test.ts +36 -5
  508. package/src/schedule/worker-control.ts +15 -5
  509. package/src/security/secure-keys.ts +53 -103
  510. package/src/skills/clawhub.ts +3 -8
  511. package/src/skills/managed-store.ts +1 -5
  512. package/src/stt/types.ts +4 -0
  513. package/src/subagent/notify.ts +54 -34
  514. package/src/subagent/parent-notification.ts +6 -0
  515. package/src/telemetry/__tests__/phone-call-funnel.test.ts +72 -0
  516. package/src/telemetry/phone-call-funnel.ts +144 -0
  517. package/src/tools/__tests__/tool-schema-root-combinator-guard.test.ts +81 -0
  518. package/src/tools/ask-question/ask-question-tool.ts +6 -4
  519. package/src/tools/browser/browser-execution.ts +22 -59
  520. package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +561 -39
  521. package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +435 -39
  522. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +1 -8
  523. package/src/tools/client-os.ts +15 -0
  524. package/src/tools/computer-use/skill-proxy-bridge.ts +1 -1
  525. package/src/tools/computer-use/target.ts +42 -0
  526. package/src/tools/document/document-tool.ts +13 -18
  527. package/src/tools/execution-target.ts +14 -16
  528. package/src/tools/host-terminal/host-shell.ts +4 -1
  529. package/src/tools/network/__tests__/web-fetch-tinyfish.test.ts +250 -0
  530. package/src/tools/network/__tests__/web-search.test.ts +203 -0
  531. package/src/tools/network/web-fetch.ts +274 -7
  532. package/src/tools/network/web-search.ts +468 -6
  533. package/src/tools/permission-checker.ts +3 -3
  534. package/src/tools/policy-context.ts +3 -1
  535. package/src/tools/skills/load.ts +2 -1
  536. package/src/tools/skills/sandbox-runner.ts +11 -2
  537. package/src/tools/skills/skill-script-runner.ts +7 -0
  538. package/src/tools/skills/skill-tool-factory.ts +15 -1
  539. package/src/tools/terminal/__tests__/safe-env.test.ts +11 -0
  540. package/src/tools/terminal/safe-env.ts +4 -0
  541. package/src/tools/terminal/shell.ts +2 -0
  542. package/src/tools/tool-approval-handler.ts +5 -1
  543. package/src/tools/tool-types.ts +3 -3
  544. package/src/tools/types.ts +8 -2
  545. package/src/util/__tests__/worker-process-command.test.ts +5 -4
  546. package/src/util/abort-reasons.ts +18 -26
  547. package/src/util/canonicalize-identity.test.ts +21 -0
  548. package/src/util/canonicalize-identity.ts +16 -0
  549. package/src/util/errors.ts +13 -3
  550. package/src/util/provider-error-patterns.ts +3 -0
  551. package/src/util/worker-process.ts +77 -35
  552. package/src/workspace/migrations/157-add-desktop-files-launcher.ts +94 -0
  553. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +4 -10
  554. package/src/workspace/migrations/__tests__/157-add-desktop-files-launcher.test.ts +113 -0
  555. package/src/workspace/migrations/registry.ts +2 -0
  556. package/src/__tests__/computer-use-window-schema.test.ts +0 -24
  557. package/src/api/events/heartbeat-alert.ts +0 -20
  558. package/src/daemon/message-types/web-activity.ts +0 -65
  559. package/src/desktop/desktop-dependencies.test.ts +0 -131
  560. package/src/tools/computer-use/definitions.ts +0 -607
@@ -32,22 +32,29 @@ mock.module("../../daemon/conversation-store.js", () => ({
32
32
  getOrCreateConversation: async () => fakeConversation,
33
33
  }));
34
34
 
35
- // Vision capability of the image pin's target profile. Install-dependent in
36
- // production (a BYO provider resolves the profile key through its own column
37
- // of the intent matrix), so it is scripted rather than read from a catalog.
38
- let pinProfileSupportsVision = true;
39
- // Vision capability of the conversation's own profile, which an escalated
40
- // leg is pinned to. Scripted for the same reason.
41
- let conversationProfileSupportsVision = true;
42
- // Per-profile answers that outrank the two switches above, for a mix whose
43
- // arms differ.
44
- const visionByProfile = new Map<string, boolean>();
45
- mock.module("../../plugin-api/vision-support.js", () => ({
46
- doesSupportVision: (profile: string) =>
47
- visionByProfile.get(profile) ??
48
- (profile === "latency-optimized"
49
- ? pinProfileSupportsVision
50
- : conversationProfileSupportsVision),
35
+ // The escalation judge's verdict for front-door legs, scripted per test.
36
+ let judgeEscalationCalls: Array<{ utterance: string }> = [];
37
+ let judgeEscalationVerdict = false;
38
+ // When set, the judge's verdict waits on this before resolving.
39
+ let judgeEscalationGate: Promise<void> | null = null;
40
+ mock.module("../voice-escalation-judge.js", () => ({
41
+ judgeEscalation: async (args: { utterance: string }) => {
42
+ judgeEscalationCalls.push({ utterance: args.utterance });
43
+ if (judgeEscalationGate) {
44
+ await judgeEscalationGate;
45
+ }
46
+ return {
47
+ escalate: judgeEscalationVerdict,
48
+ outcome: judgeEscalationVerdict ? "escalate" : "clear",
49
+ latencyMs: 0,
50
+ };
51
+ },
52
+ }));
53
+
54
+ const unresolvableProviderNames = new Set<string>();
55
+ mock.module("../../providers/provider-resolvability.js", () => ({
56
+ dispatchProviderResolvable: (provider: string) =>
57
+ !unresolvableProviderNames.has(provider),
51
58
  }));
52
59
 
53
60
  // Attachment hydration for the parked-camera-frame path. Only `att-frame-*`
@@ -137,6 +144,7 @@ mock.module("../../persistence/conversation-crud.js", () => ({
137
144
  }));
138
145
 
139
146
  import { setConfig } from "../../__tests__/helpers/set-config.js";
147
+ import type { PreparedModelCall } from "../../agent/loop.js";
140
148
  import { selectWinningProfile } from "../../config/llm-resolver.js";
141
149
  import { getConfig } from "../../config/loader.js";
142
150
  import { ABORT_WATCHDOG_MS } from "../../daemon/abort-watchdog.js";
@@ -205,6 +213,7 @@ interface FakeConversation {
205
213
  opts?: { decisionContext?: string },
206
214
  ) => void;
207
215
  runAgentLoop: (...args: unknown[]) => Promise<void>;
216
+ warmPromptCache: (options?: Record<string, unknown>) => Promise<void>;
208
217
  getMessages: () => Array<{ role: string; content: unknown[] }>;
209
218
  abort: (reason?: unknown) => void;
210
219
  loadFromDb: () => Promise<void>;
@@ -304,6 +313,7 @@ function makeFakeConversation(opts: {
304
313
  confirmationDecisions.push({ requestId, decision });
305
314
  },
306
315
  runAgentLoop: () => (opts.runAgentLoop ?? (async () => {}))(),
316
+ warmPromptCache: async () => {},
307
317
  getMessages: () => opts.messages ?? [],
308
318
  abort: () => {},
309
319
  loadFromDb: async () => {
@@ -721,9 +731,142 @@ describe("startVoiceTurn camera-frame attachments", () => {
721
731
  voiceSessionTurn: true,
722
732
  });
723
733
  });
734
+
735
+ test("claims a captured camera source before voice user persistence", async () => {
736
+ const events: string[] = [];
737
+ const fake = makeFakeConversation({
738
+ processing: false,
739
+ events,
740
+ onPersist: () => events.push("persisted"),
741
+ });
742
+ const claims: Array<{ turnId: string; sourceId: string }> = [];
743
+ Object.assign(fake.conversation, {
744
+ modeSessions: {
745
+ claimTurn: (
746
+ turnId: string,
747
+ source: { sourceId: string; id: string; mode: string },
748
+ ) => {
749
+ claims.push({ turnId, sourceId: source.sourceId });
750
+ events.push("claimed");
751
+ return { id: source.id, mode: source.mode };
752
+ },
753
+ releaseTurn: () => {},
754
+ },
755
+ });
756
+ fakeConversation = fake.conversation;
757
+
758
+ await startVoiceTurn({
759
+ ...makeTurnOptions(),
760
+ content: "what is this",
761
+ modeSessionSource: {
762
+ sourceId: "live-voice-camera:voice-1",
763
+ generation: 7,
764
+ activation: 1,
765
+ id: "session-camera",
766
+ mode: "live_vision",
767
+ },
768
+ });
769
+
770
+ expect(claims).toHaveLength(1);
771
+ expect(claims[0]?.sourceId).toBe("live-voice-camera:voice-1");
772
+ expect(events.indexOf("claimed")).toBeLessThan(events.indexOf("persist"));
773
+ });
774
+
775
+ test("uses preaccepted camera ownership without reclaiming a retired source", async () => {
776
+ let releaseIdle!: () => void;
777
+ const idle = new Promise<void>((resolve) => {
778
+ releaseIdle = resolve;
779
+ });
780
+ let reachedAdmission!: () => void;
781
+ const admissionStarted = new Promise<void>((resolve) => {
782
+ reachedAdmission = resolve;
783
+ });
784
+ const fake = makeFakeConversation({
785
+ processing: true,
786
+ waitForIdle: async () => {
787
+ reachedAdmission();
788
+ await idle;
789
+ fake.setProcessingFlag(false);
790
+ return true;
791
+ },
792
+ });
793
+ const claims: string[] = [];
794
+ const owner = { id: "session-camera", mode: "live_vision" as const };
795
+ Object.assign(fake.conversation, {
796
+ modeSessions: {
797
+ claimTurn: (turnId: string) => {
798
+ claims.push(turnId);
799
+ return undefined;
800
+ },
801
+ getTurnOwner: (turnId: string) =>
802
+ turnId === "preaccepted-request" ? owner : undefined,
803
+ releaseTurn: () => {},
804
+ },
805
+ });
806
+ fakeConversation = fake.conversation;
807
+
808
+ const starting = startVoiceTurn({
809
+ ...makeTurnOptions(),
810
+ content: "what is this",
811
+ preacceptedModeSession: {
812
+ requestId: "preaccepted-request",
813
+ source: {
814
+ sourceId: "live-voice-camera:voice-1",
815
+ generation: 7,
816
+ activation: 1,
817
+ ...owner,
818
+ },
819
+ },
820
+ });
821
+ await admissionStarted;
822
+
823
+ releaseIdle();
824
+ await starting;
825
+
826
+ expect(claims).toEqual([]);
827
+ expect(fake.lastPersistOpts()?.requestId).toBe("preaccepted-request");
828
+ });
724
829
  });
725
830
 
726
831
  describe("startVoiceTurn hiddenSyntheticPrompt", () => {
832
+ test("task announcements retain metadata and cron attribution without a user echo", async () => {
833
+ const fake = makeFakeConversation({ processing: false });
834
+ fakeConversation = fake.conversation;
835
+ const loopOptions: unknown[] = [];
836
+ fake.conversation.runAgentLoop = async (...args: unknown[]) => {
837
+ loopOptions.push(args[2]);
838
+ };
839
+ const metadata = {
840
+ subagentNotification: {
841
+ subagentId: "task-1",
842
+ label: "Compare options",
843
+ status: "completed",
844
+ },
845
+ };
846
+ const echoes = await collectUserMessageEchoes(async () => {
847
+ await startVoiceTurn({
848
+ ...makeTurnOptions(),
849
+ content: "Comparison completed",
850
+ hiddenSyntheticPrompt: true,
851
+ subagentNotification: {
852
+ taskId: "task-1",
853
+ message: "Comparison completed",
854
+ metadata,
855
+ cronRunId: "run-123",
856
+ },
857
+ });
858
+ });
859
+ expect(fake.lastPersistOpts()?.metadata).toMatchObject({
860
+ ...metadata,
861
+ hidden: true,
862
+ scripted: true,
863
+ voiceSessionTurn: true,
864
+ });
865
+ expect(loopOptions).toEqual([
866
+ expect.objectContaining({ cronRunId: "run-123" }),
867
+ ]);
868
+ expect(echoes).toEqual([]);
869
+ });
727
870
  // A caller whose internal instruction is composed per call carries no
728
871
  // sentinel for the content comparisons to recognize, so it declares itself.
729
872
  const SYNTHETIC_CONTENT =
@@ -831,6 +974,22 @@ describe("startVoiceTurn triage-and-escalate control prompt", () => {
831
974
  expect(installed()).toContain(frontDoorDecisionRule({ includeHold: true }));
832
975
  });
833
976
 
977
+ test("a hidden look follow-up routes the original request", async () => {
978
+ const installed = captureInstalledPrompt();
979
+ const callerUtterance = "Show me where to add a new page.";
980
+ const content = "(fresh view taken; answer from it now)";
981
+ await startVoiceTurn({
982
+ ...makeTurnOptions(),
983
+ content,
984
+ routingUtterance: callerUtterance,
985
+ hiddenSyntheticPrompt: true,
986
+ voiceControlPrompt: LIVE_VOICE_PROMPT,
987
+ routingLeg: "front-door",
988
+ });
989
+ expect(installed()).toContain(frontDoorDecisionRule({ callerUtterance }));
990
+ expect(installed()).not.toContain(JSON.stringify(content));
991
+ });
992
+
834
993
  test("appends the escalated continuation rule to a caller-supplied prompt", async () => {
835
994
  const installed = captureInstalledPrompt();
836
995
  await startVoiceTurn({
@@ -2374,6 +2533,51 @@ describe("front-door hub stream gate", () => {
2374
2533
  expect(texts.join("")).toBe("It is Tuesday, and it is sunny.");
2375
2534
  });
2376
2535
 
2536
+ test("an answer waits on the escalation judge before reaching the hub", async () => {
2537
+ let openGate!: () => void;
2538
+ judgeEscalationGate = new Promise<void>((resolve) => {
2539
+ openGate = resolve;
2540
+ });
2541
+ judgeEscalationVerdict = false;
2542
+ makeStreamingConversation(["Sure, ", "it's Tuesday."]);
2543
+ try {
2544
+ const whilePending = await collectBroadcastText(() =>
2545
+ startVoiceTurn({ ...makeTurnOptions(), routingLeg: "front-door" }),
2546
+ );
2547
+ expect(whilePending).toEqual([]);
2548
+
2549
+ const afterClear = await collectBroadcastText(async () => {
2550
+ openGate();
2551
+ await new Promise((resolve) => setTimeout(resolve, 5));
2552
+ });
2553
+ expect(afterClear.join("")).toBe("Sure, it's Tuesday.");
2554
+ } finally {
2555
+ judgeEscalationGate = null;
2556
+ }
2557
+ });
2558
+
2559
+ test("an overruled answer never reaches the hub", async () => {
2560
+ judgeEscalationVerdict = true;
2561
+ makeStreamingConversation(["Yeah okay, ", "I'll do it."]);
2562
+
2563
+ const texts = await collectBroadcastText(async () => {
2564
+ const handle = await startVoiceTurn({
2565
+ ...makeTurnOptions(),
2566
+ routingLeg: "front-door",
2567
+ });
2568
+ // What the driver does with an escalate verdict on a held answer.
2569
+ void handle.escalationJudgement?.then((escalate) => {
2570
+ if (escalate) {
2571
+ handle.overrule?.();
2572
+ }
2573
+ });
2574
+ await new Promise((resolve) => setTimeout(resolve, 5));
2575
+ });
2576
+
2577
+ expect(texts).toEqual([]);
2578
+ judgeEscalationVerdict = false;
2579
+ });
2580
+
2377
2581
  test("an answer that merely opens with a bracket is released in full", async () => {
2378
2582
  // "[" alone could still become the escalate token, so the gate holds it;
2379
2583
  // the next delta disproves the token and the whole prefix must come out.
@@ -2520,6 +2724,60 @@ describe("transcript hygiene (teardown pass)", () => {
2520
2724
  expect(events).toContain("loadFromDb");
2521
2725
  });
2522
2726
 
2727
+ test("an answer the escalation judge overruled is deleted", async () => {
2728
+ const { events, releaseLoop } = makeReservedRowConversation({
2729
+ holdLoopOpen: true,
2730
+ });
2731
+ getMessageByIdImpl = () => makeRow("I'm adding it to the draft now.");
2732
+
2733
+ const handle = await startVoiceTurn({
2734
+ ...makeTurnOptions(),
2735
+ routingLeg: "front-door",
2736
+ });
2737
+ await flushMicrotasks();
2738
+ handle.overrule?.();
2739
+ releaseLoop();
2740
+ await flushMicrotasks();
2741
+
2742
+ // Only the unheard answer goes: the user row stays for the escalated leg.
2743
+ expect(crudLog.deletes).toEqual(["assistant-row-1"]);
2744
+ expect(events).toContain("loadFromDb");
2745
+ });
2746
+
2747
+ test("a verdict that lands after the model finished still deletes the overruled row", async () => {
2748
+ let openGate!: () => void;
2749
+ judgeEscalationGate = new Promise<void>((resolve) => {
2750
+ openGate = resolve;
2751
+ });
2752
+ judgeEscalationVerdict = true;
2753
+ const { events } = makeReservedRowConversation();
2754
+ getMessageByIdImpl = () => makeRow("Yeah okay, I'll do it.");
2755
+ try {
2756
+ const handle = await startVoiceTurn({
2757
+ ...makeTurnOptions(),
2758
+ routingLeg: "front-door",
2759
+ });
2760
+ void handle.escalationJudgement?.then((escalate) => {
2761
+ if (escalate) {
2762
+ handle.overrule?.();
2763
+ }
2764
+ });
2765
+ // The loop has finished; teardown hygiene waits on the verdict.
2766
+ await flushMicrotasks();
2767
+ expect(crudLog.deletes).toEqual([]);
2768
+
2769
+ openGate();
2770
+ await new Promise((resolve) => setTimeout(resolve, 5));
2771
+ await flushMicrotasks();
2772
+
2773
+ expect(crudLog.deletes).toEqual(["assistant-row-1"]);
2774
+ expect(events).toContain("loadFromDb");
2775
+ } finally {
2776
+ judgeEscalationGate = null;
2777
+ judgeEscalationVerdict = false;
2778
+ }
2779
+ });
2780
+
2523
2781
  test("a committed front-door answer (no verdict token) is left untouched", async () => {
2524
2782
  const { events } = makeReservedRowConversation();
2525
2783
  getMessageByIdImpl = () => makeRow("It is Tuesday.");
@@ -2756,11 +3014,7 @@ const PHOTO_HISTORY = [
2756
3014
  },
2757
3015
  ];
2758
3016
 
2759
- describe("startVoiceTurn image-bearing profile pin", () => {
2760
- beforeEach(() => {
2761
- pinProfileSupportsVision = true;
2762
- });
2763
-
3017
+ describe("startVoiceTurn with images in history", () => {
2764
3018
  async function runOptionsFor(opts: {
2765
3019
  messages?: Array<{ role: string; content: unknown[] }>;
2766
3020
  turn?: Record<string, unknown>;
@@ -2778,42 +3032,16 @@ describe("startVoiceTurn image-bearing profile pin", () => {
2778
3032
  return runOptions;
2779
3033
  }
2780
3034
 
2781
- test("a text-only call keeps the call-site profile", async () => {
2782
- const runOptions = await runOptionsFor({});
3035
+ test("an image in history leaves the call-site profile in place", async () => {
3036
+ // A text-only model gets the image captioned by the image-fallback
3037
+ // plugin, the same as a typed turn, so no profile is pinned for it.
3038
+ const runOptions = await runOptionsFor({ messages: PHOTO_HISTORY });
2783
3039
 
3040
+ expect(runOptions.callSite).toBe("callAgent");
2784
3041
  expect(runOptions.overrideProfile).toBeUndefined();
2785
3042
  expect(runOptions.forceOverrideProfile).toBeUndefined();
2786
3043
  });
2787
3044
 
2788
- test("an image in history pins the image-capable profile", async () => {
2789
- // `callAgent`'s balanced profile carries no guarantee that its model takes
2790
- // an image, and a model that rejects one fails the whole leg.
2791
- const runOptions = await runOptionsFor({ messages: PHOTO_HISTORY });
2792
-
2793
- expect(runOptions.overrideProfile).toBe("latency-optimized");
2794
- // callAgent is not `mainAgent`, so an unforced override would sit below
2795
- // the call-site profile and never apply.
2796
- expect(runOptions.forceOverrideProfile).toBe(true);
2797
- });
2798
-
2799
- test("an image nested in a tool result counts too", async () => {
2800
- const runOptions = await runOptionsFor({
2801
- messages: [
2802
- {
2803
- role: "user",
2804
- content: [
2805
- {
2806
- type: "tool_result",
2807
- contentBlocks: [{ type: "image", source: { data: "abc" } }],
2808
- },
2809
- ],
2810
- },
2811
- ],
2812
- });
2813
-
2814
- expect(runOptions.overrideProfile).toBe("latency-optimized");
2815
- });
2816
-
2817
3045
  test("a front-door leg is left alone — its own call site pins it", async () => {
2818
3046
  const runOptions = await runOptionsFor({
2819
3047
  messages: PHOTO_HISTORY,
@@ -2824,19 +3052,7 @@ describe("startVoiceTurn image-bearing profile pin", () => {
2824
3052
  expect(runOptions.callSite).toBe("voiceFrontDoor");
2825
3053
  });
2826
3054
 
2827
- test("no pin when the pin target can't take an image either", async () => {
2828
- // Fireworks: `latency-optimized` resolves to a text-only model while
2829
- // `balanced` is vision-capable, so pinning would break the very turn the
2830
- // pin exists to save.
2831
- pinProfileSupportsVision = false;
2832
-
2833
- const runOptions = await runOptionsFor({ messages: PHOTO_HISTORY });
2834
-
2835
- expect(runOptions.overrideProfile).toBeUndefined();
2836
- expect(runOptions.forceOverrideProfile).toBeUndefined();
2837
- });
2838
-
2839
- test("an explicit routing pin wins over the image pin", async () => {
3055
+ test("an explicit routing pin still applies", async () => {
2840
3056
  const runOptions = await runOptionsFor({
2841
3057
  messages: PHOTO_HISTORY,
2842
3058
  turn: { overrideProfile: "quality-optimized" },
@@ -2848,12 +3064,13 @@ describe("startVoiceTurn image-bearing profile pin", () => {
2848
3064
 
2849
3065
  describe("startVoiceTurn escalated-leg profile pin", () => {
2850
3066
  beforeEach(() => {
2851
- pinProfileSupportsVision = true;
2852
- conversationProfileSupportsVision = true;
3067
+ unresolvableProviderNames.clear();
2853
3068
  });
2854
3069
 
2855
3070
  afterEach(() => {
3071
+ unresolvableProviderNames.clear();
2856
3072
  setConfig("llm", {});
3073
+ setConfig("rateLimit", {});
2857
3074
  });
2858
3075
 
2859
3076
  async function runOptionsFor(opts: {
@@ -2885,10 +3102,31 @@ describe("startVoiceTurn escalated-leg profile pin", () => {
2885
3102
  return runOptions;
2886
3103
  }
2887
3104
 
3105
+ function reportPreparedTarget(
3106
+ runOptions: Record<string, unknown>,
3107
+ overrideProfile: string,
3108
+ ): void {
3109
+ const onPrepared = runOptions.onFirstModelCallPrepared as (prepared: {
3110
+ callSite: "mainAgent";
3111
+ overrideProfile: string;
3112
+ forceOverrideProfile: boolean;
3113
+ systemPrompt: string;
3114
+ tools: [];
3115
+ }) => void;
3116
+ onPrepared({
3117
+ callSite: "mainAgent",
3118
+ overrideProfile,
3119
+ forceOverrideProfile: true,
3120
+ systemPrompt: "prepared prompt",
3121
+ tools: [],
3122
+ });
3123
+ }
3124
+
2888
3125
  test("with no chat-model selection the leg keeps the call-site profile", async () => {
2889
3126
  const runOptions = await runOptionsFor({});
2890
3127
 
2891
3128
  expect(runOptions.callSite).toBe("callAgent");
3129
+ expect(runOptions.inferenceCallSite).toBe("mainAgent");
2892
3130
  expect(runOptions.overrideProfile).toBeUndefined();
2893
3131
  expect(runOptions.forceOverrideProfile).toBeUndefined();
2894
3132
  });
@@ -2902,8 +3140,168 @@ describe("startVoiceTurn escalated-leg profile pin", () => {
2902
3140
  const runOptions = await runOptionsFor({});
2903
3141
 
2904
3142
  expect(runOptions.callSite).toBe("callAgent");
3143
+ expect(runOptions.inferenceCallSite).toBe("mainAgent");
2905
3144
  expect(runOptions.overrideProfile).toBe("quality-optimized");
2906
3145
  expect(runOptions.forceOverrideProfile).toBe(true);
3146
+ expect(runOptions.onFirstModelCallPrepared).toBeFunction();
3147
+ });
3148
+
3149
+ test("starts warming the finalized request without awaiting it", async () => {
3150
+ setConfig("llm", { activeProfile: "quality-optimized" });
3151
+ const runOptions = await runOptionsFor({});
3152
+ const warmPromptCache = mock(() => new Promise<void>(() => {}));
3153
+ fakeConversation.warmPromptCache = warmPromptCache;
3154
+ const onFirstModelCallPrepared = runOptions.onFirstModelCallPrepared as (
3155
+ prepared: PreparedModelCall,
3156
+ ) => void;
3157
+ const tools = [
3158
+ {
3159
+ name: "dynamic_tool",
3160
+ description: "Dynamic",
3161
+ input_schema: { type: "object" as const },
3162
+ },
3163
+ ];
3164
+ const turnAbort = new AbortController();
3165
+
3166
+ expect(
3167
+ onFirstModelCallPrepared({
3168
+ callSite: "mainAgent",
3169
+ overrideProfile: "hook-selected-profile",
3170
+ forceOverrideProfile: true,
3171
+ signal: turnAbort.signal,
3172
+ systemPrompt: "hook-edited prompt",
3173
+ tools,
3174
+ }),
3175
+ ).toBeUndefined();
3176
+
3177
+ expect(warmPromptCache).toHaveBeenCalledWith({
3178
+ callSite: "mainAgent",
3179
+ overrideProfile: "hook-selected-profile",
3180
+ forceOverrideProfile: true,
3181
+ signal: turnAbort.signal,
3182
+ systemPrompt: "hook-edited prompt",
3183
+ tools,
3184
+ });
3185
+ });
3186
+
3187
+ test("reports the conversation profile selected for the handoff", async () => {
3188
+ setConfig("llm", { activeProfile: "quality-optimized" });
3189
+ const onEscalationTargetResolved = mock();
3190
+
3191
+ const runOptions = await runOptionsFor({
3192
+ turn: { onEscalationTargetResolved },
3193
+ });
3194
+ reportPreparedTarget(runOptions, "quality-optimized");
3195
+
3196
+ expect(onEscalationTargetResolved).toHaveBeenCalledWith({
3197
+ profile: "quality-optimized",
3198
+ source: "conversation",
3199
+ });
3200
+ });
3201
+
3202
+ test("reports the concrete profile selected from a mix", async () => {
3203
+ setConfig("llm", {
3204
+ activeProfile: "voice-mix",
3205
+ profiles: {
3206
+ "voice-mix": {
3207
+ mix: [
3208
+ { profile: "quality-optimized", weight: 1 },
3209
+ { profile: "cost-optimized", weight: 1 },
3210
+ ],
3211
+ },
3212
+ },
3213
+ });
3214
+ let selectedProfile: string | undefined;
3215
+ selectWinningProfile("mainAgent", getConfig().llm, {
3216
+ overrideProfile: "voice-mix",
3217
+ forceOverrideProfile: true,
3218
+ selectionSeed: "conv-voice-bridge-test",
3219
+ onMixSelected: ({ chosenProfile }) => {
3220
+ selectedProfile = chosenProfile;
3221
+ },
3222
+ });
3223
+ const onEscalationTargetResolved = mock();
3224
+ const runOptions = await runOptionsFor({
3225
+ turn: { onEscalationTargetResolved },
3226
+ });
3227
+
3228
+ // The leg is pinned to the chosen arm itself (see the mix pin test).
3229
+ expect(runOptions.overrideProfile).toBe(selectedProfile);
3230
+ reportPreparedTarget(runOptions, runOptions.overrideProfile as string);
3231
+
3232
+ expect(selectedProfile).toBeDefined();
3233
+ expect(onEscalationTargetResolved).toHaveBeenCalledWith({
3234
+ profile: selectedProfile,
3235
+ source: "conversation",
3236
+ });
3237
+ });
3238
+
3239
+ test("warms the main-agent route when the conversation has no profile pin", async () => {
3240
+ const runOptions = await runOptionsFor({});
3241
+ const warmPromptCache = mock(async () => {});
3242
+ fakeConversation.warmPromptCache = warmPromptCache;
3243
+ const onFirstModelCallPrepared = runOptions.onFirstModelCallPrepared as (
3244
+ prepared: PreparedModelCall,
3245
+ ) => void;
3246
+ onFirstModelCallPrepared({
3247
+ callSite: "mainAgent",
3248
+ forceOverrideProfile: false,
3249
+ systemPrompt: "system prompt",
3250
+ tools: [],
3251
+ });
3252
+
3253
+ expect(warmPromptCache).toHaveBeenCalledWith({
3254
+ callSite: "mainAgent",
3255
+ forceOverrideProfile: false,
3256
+ signal: undefined,
3257
+ systemPrompt: "system prompt",
3258
+ tools: [],
3259
+ });
3260
+ });
3261
+
3262
+ test("does not warm a route whose finalized policy disables caching", async () => {
3263
+ const runOptions = await runOptionsFor({});
3264
+ const warmPromptCache = mock(async () => {});
3265
+ fakeConversation.warmPromptCache = warmPromptCache;
3266
+ const onFirstModelCallPrepared = runOptions.onFirstModelCallPrepared as (
3267
+ prepared: PreparedModelCall,
3268
+ ) => void;
3269
+
3270
+ onFirstModelCallPrepared({
3271
+ callSite: "mainAgent",
3272
+ forceOverrideProfile: false,
3273
+ disableCache: true,
3274
+ systemPrompt: "system prompt",
3275
+ tools: [],
3276
+ });
3277
+
3278
+ expect(warmPromptCache).not.toHaveBeenCalled();
3279
+ });
3280
+
3281
+ test("does not warm when requests are rate limited", async () => {
3282
+ setConfig("rateLimit", { maxRequestsPerMinute: 1 });
3283
+ const runOptions = await runOptionsFor({});
3284
+ const warmPromptCache = mock(async () => {});
3285
+ fakeConversation.warmPromptCache = warmPromptCache;
3286
+
3287
+ expect(runOptions.onFirstModelCallPrepared).toBeFunction();
3288
+ reportPreparedTarget(runOptions, "quality-optimized");
3289
+ expect(warmPromptCache).not.toHaveBeenCalled();
3290
+ });
3291
+
3292
+ test("reports a profile selected by final pre-model routing", async () => {
3293
+ setConfig("llm", { activeProfile: "quality-optimized" });
3294
+ const onEscalationTargetResolved = mock();
3295
+
3296
+ const runOptions = await runOptionsFor({
3297
+ turn: { onEscalationTargetResolved },
3298
+ });
3299
+ reportPreparedTarget(runOptions, "cost-optimized");
3300
+
3301
+ expect(onEscalationTargetResolved).toHaveBeenCalledWith({
3302
+ profile: "cost-optimized",
3303
+ source: "pre_model_hook",
3304
+ });
2907
3305
  });
2908
3306
 
2909
3307
  test("the conversation's own pin wins over the workspace selection", async () => {
@@ -2942,34 +3340,47 @@ describe("startVoiceTurn escalated-leg profile pin", () => {
2942
3340
  expect(unrouted.overrideProfile).toBeUndefined();
2943
3341
  });
2944
3342
 
2945
- test("an image stays on a conversation profile whose model takes it", async () => {
3343
+ test("an image keeps a text-only conversation profile", async () => {
3344
+ // The escalated leg is the tool-capable brain of the call. Images reach a
3345
+ // text-only model as image-fallback captions, never by swapping the leg
3346
+ // onto the latency-class profile.
2946
3347
  setConfig("llm", { activeProfile: "quality-optimized" });
2947
3348
 
2948
3349
  const runOptions = await runOptionsFor({ messages: PHOTO_HISTORY });
2949
3350
 
2950
3351
  expect(runOptions.overrideProfile).toBe("quality-optimized");
2951
- });
2952
-
2953
- test("an image hands a text-only conversation profile to the image pin", async () => {
2954
- // A model that rejects the image fails the whole leg, so the image pin
2955
- // outranks the conversation's choice for this one turn.
2956
- setConfig("llm", { activeProfile: "quality-optimized" });
2957
- conversationProfileSupportsVision = false;
2958
-
2959
- const runOptions = await runOptionsFor({ messages: PHOTO_HISTORY });
2960
-
2961
- expect(runOptions.overrideProfile).toBe("latency-optimized");
2962
3352
  expect(runOptions.forceOverrideProfile).toBe(true);
3353
+ expect(runOptions.inferenceCallSite).toBe("mainAgent");
2963
3354
  });
2964
3355
 
2965
- test("an image with no image-capable profile anywhere keeps the conversation profile", async () => {
2966
- setConfig("llm", { activeProfile: "quality-optimized" });
2967
- conversationProfileSupportsVision = false;
2968
- pinProfileSupportsVision = false;
3356
+ test("a mix pins the arm serving this conversation, not the mix name", async () => {
3357
+ // The image-fallback check judges the pinned profile, and a mix reads as
3358
+ // vision-capable when any arm is. Pinning the chosen arm keeps a
3359
+ // text-only arm from receiving raw images.
3360
+ setConfig("llm", {
3361
+ activeProfile: "voice-mix",
3362
+ profiles: {
3363
+ "voice-mix": {
3364
+ mix: [
3365
+ { profile: "quality-optimized", weight: 1 },
3366
+ { profile: "cost-optimized", weight: 1 },
3367
+ ],
3368
+ },
3369
+ },
3370
+ });
3371
+ let chosenArm: string | undefined;
3372
+ selectWinningProfile("mainAgent", getConfig().llm, {
3373
+ selectionSeed: "conv-voice-bridge-test",
3374
+ onMixSelected: ({ chosenProfile }) => {
3375
+ chosenArm = chosenProfile;
3376
+ },
3377
+ });
3378
+ expect(chosenArm).toBeDefined();
2969
3379
 
2970
3380
  const runOptions = await runOptionsFor({ messages: PHOTO_HISTORY });
2971
3381
 
2972
- expect(runOptions.overrideProfile).toBe("quality-optimized");
3382
+ expect(runOptions.overrideProfile).toBe(chosenArm);
3383
+ expect(runOptions.forceOverrideProfile).toBe(true);
2973
3384
  });
2974
3385
 
2975
3386
  test("an explicit routing pin wins over the conversation profile", async () => {
@@ -2982,51 +3393,94 @@ describe("startVoiceTurn escalated-leg profile pin", () => {
2982
3393
  expect(runOptions.overrideProfile).toBe("balanced");
2983
3394
  });
2984
3395
 
2985
- test("a mix is judged by the arm serving this conversation, not by any arm", async () => {
2986
- // A mix reads as vision-capable when any arm is, but dispatch expands it
2987
- // to one arm from the conversation seed. Only that arm's model sees the
2988
- // image, so only that arm's capability decides whether the image pin
2989
- // takes over. The pin itself stays the mix's own name, so dispatch lands
2990
- // on the same arm.
2991
- const llm = {
2992
- activeProfile: "voice-mix",
3396
+ test("a mix with no resolvable arm falls back to the main-agent profile", async () => {
3397
+ setConfig("llm", {
3398
+ activeProfile: "stale-mix",
2993
3399
  profiles: {
2994
- "voice-mix": {
3400
+ "stale-a": {
3401
+ provider: "deleted-connection-a",
3402
+ model: "model-a",
3403
+ },
3404
+ "stale-b": {
3405
+ provider: "deleted-connection-b",
3406
+ model: "model-b",
3407
+ },
3408
+ "stale-mix": {
2995
3409
  mix: [
2996
- { profile: "quality-optimized", weight: 1 },
2997
- { profile: "cost-optimized", weight: 1 },
3410
+ { profile: "stale-a", weight: 1 },
3411
+ { profile: "stale-b", weight: 1 },
2998
3412
  ],
2999
3413
  },
3000
3414
  },
3001
- };
3002
- setConfig("llm", llm);
3003
- let chosenArm: string | undefined;
3004
- selectWinningProfile("mainAgent", getConfig().llm, {
3005
- selectionSeed: "conv-voice-bridge-test",
3006
- onMixSelected: ({ chosenProfile }) => {
3007
- chosenArm = chosenProfile;
3415
+ callSites: {
3416
+ mainAgent: { profile: "quality-optimized" },
3008
3417
  },
3009
3418
  });
3010
- expect(chosenArm).toBeDefined();
3011
- const otherArm =
3012
- chosenArm === "quality-optimized"
3013
- ? "cost-optimized"
3014
- : "quality-optimized";
3015
- try {
3016
- // Only the unchosen arm takes images: judged as "any arm", the mix
3017
- // would keep the pin off and the image would reach a text-only model.
3018
- visionByProfile.set(chosenArm!, false);
3019
- visionByProfile.set(otherArm, true);
3020
- const textOnlyArm = await runOptionsFor({ messages: PHOTO_HISTORY });
3021
- expect(textOnlyArm.overrideProfile).toBe("latency-optimized");
3022
-
3023
- // Only the chosen arm takes images: no pin needed, the mix stands.
3024
- visionByProfile.set(chosenArm!, true);
3025
- visionByProfile.set(otherArm, false);
3026
- const visionArm = await runOptionsFor({ messages: PHOTO_HISTORY });
3027
- expect(visionArm.overrideProfile).toBe("voice-mix");
3028
- } finally {
3029
- visionByProfile.clear();
3419
+ unresolvableProviderNames.add("deleted-connection-a");
3420
+ unresolvableProviderNames.add("deleted-connection-b");
3421
+ const runOptions = await runOptionsFor({ messages: PHOTO_HISTORY });
3422
+
3423
+ expect(runOptions.overrideProfile).toBe("quality-optimized");
3424
+ });
3425
+ });
3426
+
3427
+ describe("startVoiceTurn escalation judge", () => {
3428
+ beforeEach(() => {
3429
+ judgeEscalationCalls = [];
3430
+ judgeEscalationVerdict = false;
3431
+ fakeConversation = makeFakeConversation({ processing: false }).conversation;
3432
+ });
3433
+
3434
+ test("a front-door leg carries the judge's verdict and an overrule", async () => {
3435
+ judgeEscalationVerdict = true;
3436
+
3437
+ const handle = await startVoiceTurn({
3438
+ ...makeTurnOptions(),
3439
+ content: "Text my mom I'm late.",
3440
+ routingLeg: "front-door",
3441
+ });
3442
+
3443
+ expect(judgeEscalationCalls).toEqual([
3444
+ { utterance: "Text my mom I'm late." },
3445
+ ]);
3446
+ expect(await handle.escalationJudgement).toBe(true);
3447
+ expect(typeof handle.overrule).toBe("function");
3448
+ });
3449
+
3450
+ test("escalated, unrouted, and synthetic legs are not judged", async () => {
3451
+ const escalated = await startVoiceTurn({
3452
+ ...makeTurnOptions(),
3453
+ routingLeg: "escalated",
3454
+ });
3455
+ const unrouted = await startVoiceTurn(makeTurnOptions());
3456
+ const synthetic = await startVoiceTurn({
3457
+ ...makeTurnOptions(),
3458
+ routingLeg: "front-door",
3459
+ hiddenSyntheticPrompt: true,
3460
+ });
3461
+
3462
+ expect(judgeEscalationCalls).toEqual([]);
3463
+ for (const handle of [escalated, unrouted, synthetic]) {
3464
+ expect(handle.escalationJudgement).toBeUndefined();
3465
+ expect(handle.overrule).toBeUndefined();
3030
3466
  }
3031
3467
  });
3468
+
3469
+ test.each([
3470
+ "What do you see on my screen?",
3471
+ "Show me where to add a new page.",
3472
+ ])("a captured look bypasses the text-only judge: %s", async (request) => {
3473
+ judgeEscalationVerdict = true;
3474
+ const handle = await startVoiceTurn({
3475
+ ...makeTurnOptions(),
3476
+ content: "(fresh view taken; answer from it now)",
3477
+ routingUtterance: request,
3478
+ routingLeg: "front-door",
3479
+ hiddenSyntheticPrompt: true,
3480
+ });
3481
+
3482
+ expect(judgeEscalationCalls).toEqual([]);
3483
+ expect(handle.escalationJudgement).toBeUndefined();
3484
+ expect(handle.overrule).toBeUndefined();
3485
+ });
3032
3486
  });