@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
@@ -55,6 +55,8 @@ export type BargeInGuardStep = "pending" | "reset" | "fired";
55
55
  export interface BargeInGuard {
56
56
  /** Speech accumulated in the current run. */
57
57
  readonly speechMs: number;
58
+ /** Whether this run has met the threshold, latched until the guard is replaced. */
59
+ readonly fired: boolean;
58
60
  /** Feed one classified chunk of `chunkMs` audio. */
59
61
  track(
60
62
  classification: BargeInChunkClassification,
@@ -82,6 +84,9 @@ export function createBargeInGuard(minSpeechMs: number): BargeInGuard {
82
84
  };
83
85
 
84
86
  return {
87
+ get fired() {
88
+ return fired;
89
+ },
85
90
  get speechMs() {
86
91
  return speechMs;
87
92
  },
@@ -94,6 +94,11 @@ import {
94
94
  type FrontDoorLegCoordinator,
95
95
  type SpokenEscalationBridge,
96
96
  } from "./voice-leg-coordinator.js";
97
+ import {
98
+ getVoiceMetricsAggregateFields,
99
+ VoiceMetricsCollector,
100
+ type VoiceMetricsSnapshot,
101
+ } from "./voice-metrics.js";
97
102
  import { createProgressCadence } from "./voice-progress-cadence.js";
98
103
  import {
99
104
  CONVERSATION_BUSY_MESSAGE,
@@ -248,6 +253,23 @@ export class CallController {
248
253
  private readonly progressConfig: VoiceProgressConfig;
249
254
  /** Rotates the static narration fallback across turns. */
250
255
  private progressPhraseCounter = 0;
256
+ /** Per-turn latency marks for the call, shared with live voice. */
257
+ private readonly metrics: VoiceMetricsCollector;
258
+ /**
259
+ * The turn the collector currently has open, or null between turns. Every
260
+ * mark is guarded on this: the collector opens a turn for any mark that
261
+ * arrives without one, so an unguarded late callback would invent a phantom
262
+ * turn that never settles.
263
+ */
264
+ private metricsTurnId: string | null = null;
265
+ /**
266
+ * When the caller's utterance was transcribed, held until the turn it
267
+ * dispatches opens. The phone commits on the provider's utterance-boundary
268
+ * final, so this is the turn's speech-end anchor as well as its transcript
269
+ * mark; seeding it backdates the turn to the moment the caller stopped
270
+ * talking rather than the moment the controller got around to dispatching.
271
+ */
272
+ private pendingFinalTranscriptAtMs: number | null = null;
251
273
 
252
274
  constructor(
253
275
  callSessionId: string,
@@ -285,6 +307,11 @@ export class CallController {
285
307
  this.conversationId = session?.conversationId ?? callSessionId;
286
308
  this.skipDisclosure = session?.skipDisclosure ?? false;
287
309
 
310
+ this.metrics = new VoiceMetricsCollector({
311
+ sessionId: callSessionId,
312
+ conversationId: this.conversationId,
313
+ });
314
+
288
315
  this.startDurationTimer();
289
316
  this.resetSilenceTimer();
290
317
  registerCallController(callSessionId, this);
@@ -297,6 +324,14 @@ export class CallController {
297
324
  return this.state;
298
325
  }
299
326
 
327
+ /**
328
+ * The call's per-turn latency marks so far. Read-only: the collector is fed
329
+ * from this controller's own turn lifecycle.
330
+ */
331
+ getMetricsSnapshot(): VoiceMetricsSnapshot {
332
+ return this.metrics.getSnapshot();
333
+ }
334
+
300
335
  /**
301
336
  * Returns the question ID of the currently pending guardian consultation,
302
337
  * or null if no consultation is active. Used by answerCall to match
@@ -370,6 +405,10 @@ export class CallController {
370
405
  * consultation is pending — the consultation is tracked separately.
371
406
  */
372
407
  async handleCallerUtterance(transcript: string): Promise<void> {
408
+ // Stamped before the teardown waits below, which can run for hundreds of
409
+ // milliseconds: the caller stopped talking now, not once the prior turn
410
+ // finished dying.
411
+ this.pendingFinalTranscriptAtMs = Date.now();
373
412
  // If the caller speaks while an END_CALL teardown is pending (during the
374
413
  // drain wait or the listen window), this is a deferral — the caller is
375
414
  // re-engaging after we tried to hang up. Track it so we can cap repeats.
@@ -518,6 +557,7 @@ export class CallController {
518
557
  { callSessionId: this.callSessionId, state: this.state },
519
558
  "Barge-in accepted: interrupting the assistant's turn",
520
559
  );
560
+ this.markTurnMetric((turnId) => this.metrics.markBargeIn(turnId));
521
561
  onAccepted?.();
522
562
  this.handleInterrupt();
523
563
  return true;
@@ -532,6 +572,9 @@ export class CallController {
532
572
  * the transport's sustained-speech guard to vouch for the interruption.
533
573
  */
534
574
  handleInterrupt(): void {
575
+ // One cancelled turn: the interruption ends the turn here, whether or not
576
+ // anything picks the work up afterwards.
577
+ this.cancelMetricsTurn("interrupted");
535
578
  const wasSpeaking = this.state === "speaking";
536
579
  this.abortCurrentTurn();
537
580
  this.llmRunVersion++;
@@ -551,6 +594,7 @@ export class CallController {
551
594
  */
552
595
  destroy(): void {
553
596
  this.destroyed = true;
597
+ this.cancelMetricsTurn("call_ended");
554
598
  if (this.silenceTimer) {
555
599
  clearTimeout(this.silenceTimer);
556
600
  }
@@ -658,6 +702,7 @@ export class CallController {
658
702
  // Stamped before any pre-bridge work so the bridge's dispatch-timing log
659
703
  // attributes the whole turn, TTS provider resolution included.
660
704
  const launchedAtMs = Date.now();
705
+ this.openMetricsTurn(runVersion);
661
706
 
662
707
  // Clear silence timer while actively processing. The caller said
663
708
  // something (or a turn was triggered), so silence detection should
@@ -687,6 +732,10 @@ export class CallController {
687
732
  await this.handleTurnCompletion(outcome);
688
733
  } catch (err: unknown) {
689
734
  this.currentTurnHandle = null;
735
+ // Only this run's turn: a turn whose abort outlives the teardown wait
736
+ // rejects after its replacement has already opened, and settling by
737
+ // "whatever is open" would cancel the healthy turn's marks.
738
+ this.cancelMetricsTurnForRun(runVersion, "turn_error");
690
739
  // Aborted requests are expected (interruptions, rapid utterances)
691
740
  if (this.isExpectedAbortError(err) || runSignal.aborted) {
692
741
  log.debug(
@@ -1016,7 +1065,17 @@ export class CallController {
1016
1065
  transcriptSoFar: () => callerTranscript,
1017
1066
  language: () => this.resolveSynthesisLanguage(),
1018
1067
  deltaEpoch: () => deltaEpoch,
1019
- speak: speakFixedPhrase,
1068
+ // Only the cadence's own phrases count as narration; the escalation
1069
+ // bridge speaks through the same helper and is part of the answer.
1070
+ speak: (text, language) => {
1071
+ const spoken = speakFixedPhrase(text, language);
1072
+ if (spoken) {
1073
+ this.markTurnMetric((turnId) =>
1074
+ this.metrics.markProgressSpoken(turnId),
1075
+ );
1076
+ }
1077
+ return spoken;
1078
+ },
1020
1079
  },
1021
1080
  });
1022
1081
  const settleTurn = (): void => {
@@ -1149,6 +1208,13 @@ export class CallController {
1149
1208
  flushSafeText(fullResponseText);
1150
1209
  },
1151
1210
  abortLeg: () => legHandle?.abort(),
1211
+ overruleLeg: () => {
1212
+ if (legHandle?.overrule) {
1213
+ legHandle.overrule();
1214
+ } else {
1215
+ legHandle?.abort();
1216
+ }
1217
+ },
1152
1218
  speakBridge: speakEscalationBridge,
1153
1219
  startEscalatedLeg: (escalated) => {
1154
1220
  const started = runVoiceLeg(escalated);
@@ -1179,6 +1245,9 @@ export class CallController {
1179
1245
  if (!this.isCurrentRun(runVersion) || coordinator?.handedOff) {
1180
1246
  return;
1181
1247
  }
1248
+ this.markTurnMetric((turnId) =>
1249
+ this.metrics.markFirstAssistantDelta(turnId),
1250
+ );
1182
1251
  ingest(reasoningFilter.push(text));
1183
1252
  };
1184
1253
 
@@ -1197,6 +1266,15 @@ export class CallController {
1197
1266
  // A held "[..." tail that never completed a marker is real text,
1198
1267
  // released to both consumers before the leg settles.
1199
1268
  ingest(reasoningFilter.flush());
1269
+ // An answer held for the escalation judge is spoken or overruled
1270
+ // before the leg settles, so the turn awaits the right leg.
1271
+ if (coordinator?.awaitingJudge === true) {
1272
+ void coordinator.settled().then(() => {
1273
+ coordinator.complete();
1274
+ resolve();
1275
+ });
1276
+ return;
1277
+ }
1200
1278
  coordinator?.complete();
1201
1279
  resolve();
1202
1280
  };
@@ -1205,6 +1283,11 @@ export class CallController {
1205
1283
  reject(new Error(message));
1206
1284
  };
1207
1285
 
1286
+ // First-wins across the turn's two legs, so this is the front-door
1287
+ // dispatch: the moment the caller's wait actually starts.
1288
+ this.markTurnMetric((turnId) =>
1289
+ this.metrics.markAssistantDispatch(turnId),
1290
+ );
1208
1291
  startVoiceTurn({
1209
1292
  conversationId: this.conversationId,
1210
1293
  callSessionId: this.callSessionId,
@@ -1239,6 +1322,9 @@ export class CallController {
1239
1322
  legHandle = handle;
1240
1323
  if (this.isCurrentRun(runVersion) && !coordinator?.handedOff) {
1241
1324
  this.currentTurnHandle = handle;
1325
+ if (coordinator !== null && handle.escalationJudgement) {
1326
+ coordinator.attachEscalationJudge(handle.escalationJudgement);
1327
+ }
1242
1328
  } else {
1243
1329
  // Superseded, or the front-door leg handed off before its
1244
1330
  // handle arrived: abort immediately.
@@ -1494,6 +1580,7 @@ export class CallController {
1494
1580
  * and normal idle transition.
1495
1581
  */
1496
1582
  private async handleTurnCompletion(outcome: VoiceTurnOutcome): Promise<void> {
1583
+ this.completeMetricsTurn();
1497
1584
  const responseText = outcome.text;
1498
1585
 
1499
1586
  // Record the assistant response event, keyed to the conversation rows it
@@ -1923,6 +2010,10 @@ export class CallController {
1923
2010
  if (this.state === "processing") {
1924
2011
  this.state = "speaking";
1925
2012
  }
2013
+ // Real outbound audio, not merely buffered tokens: on media-stream this
2014
+ // fires from the transport's audio-start signal, which is the closest the
2015
+ // daemon gets to the moment the caller hears the answer.
2016
+ this.markTurnMetric((turnId) => this.metrics.markFirstTtsAudio(turnId));
1926
2017
  }
1927
2018
 
1928
2019
  /**
@@ -1947,6 +2038,110 @@ export class CallController {
1947
2038
  }
1948
2039
  }
1949
2040
 
2041
+ /**
2042
+ * Open the collector's turn for this run, seeded with the caller's
2043
+ * transcript mark.
2044
+ *
2045
+ * A turn still open here was superseded by a rapid follow-up utterance
2046
+ * (which aborts the old turn without going through the interrupt path), so
2047
+ * it settles as cancelled and gets its line in the log before the new one
2048
+ * starts.
2049
+ */
2050
+ private openMetricsTurn(runVersion: number): void {
2051
+ this.cancelMetricsTurn("superseded");
2052
+ const turnId = this.metricsTurnIdForRun(runVersion);
2053
+ const finalTranscriptAtMs = this.pendingFinalTranscriptAtMs;
2054
+ this.pendingFinalTranscriptAtMs = null;
2055
+ this.metricsTurnId = turnId;
2056
+ // The provider's utterance-boundary final is both the transcript and the
2057
+ // moment the caller stopped speaking, so it anchors the round trip too;
2058
+ // a phone turn has no separate VAD end or push-to-talk release to use.
2059
+ this.metrics.startTurn(
2060
+ turnId,
2061
+ finalTranscriptAtMs !== null
2062
+ ? { finalTranscriptAtMs, utteranceEndAtMs: finalTranscriptAtMs }
2063
+ : {},
2064
+ );
2065
+ }
2066
+
2067
+ /**
2068
+ * Run a mark against the call's open turn, if there is one.
2069
+ *
2070
+ * The collector opens a turn for any mark that arrives without one, so
2071
+ * every phone mark is guarded: a delta from a superseded leg or a barge-in
2072
+ * between turns must leave no trace rather than invent a turn that never
2073
+ * settles.
2074
+ */
2075
+ private markTurnMetric(mark: (turnId: string) => void): void {
2076
+ if (this.metricsTurnId === null) {
2077
+ return;
2078
+ }
2079
+ mark(this.metricsTurnId);
2080
+ }
2081
+
2082
+ private completeMetricsTurn(): void {
2083
+ this.settleMetricsTurn("completed", (turnId) =>
2084
+ this.metrics.completeTurn(turnId),
2085
+ );
2086
+ }
2087
+
2088
+ private cancelMetricsTurn(reason: string): void {
2089
+ this.settleMetricsTurn(reason, (turnId) =>
2090
+ this.metrics.cancelTurn(reason, turnId),
2091
+ );
2092
+ }
2093
+
2094
+ /** The turn id a given run's marks belong to. */
2095
+ private metricsTurnIdForRun(runVersion: number): string {
2096
+ return `${this.callSessionId}#${runVersion}`;
2097
+ }
2098
+
2099
+ /**
2100
+ * Cancel the open turn only if it is the one this run opened.
2101
+ *
2102
+ * Defensive: the abort listener resolves a superseded leg rather than
2103
+ * rejecting it, and a genuine rejection is processed before the replacement
2104
+ * turn opens, so no path today reaches the error handler holding a newer
2105
+ * turn. Scoping the settle to its own run keeps that true by construction
2106
+ * rather than by the ordering of two other code paths.
2107
+ */
2108
+ private cancelMetricsTurnForRun(runVersion: number, reason: string): void {
2109
+ if (this.metricsTurnId !== this.metricsTurnIdForRun(runVersion)) {
2110
+ return;
2111
+ }
2112
+ this.cancelMetricsTurn(reason);
2113
+ }
2114
+
2115
+ /**
2116
+ * Settle the open turn and log what it measured.
2117
+ *
2118
+ * Live voice streams a metrics frame to its client on every mark; a call
2119
+ * has no client on the line, so the per-turn aggregate lands in the log
2120
+ * instead. Cancelled turns are logged too: a turn the caller cut off still
2121
+ * measured everything up to the interruption, and how long the assistant
2122
+ * took before being cut off is the more interesting number of the two.
2123
+ */
2124
+ private settleMetricsTurn(
2125
+ status: string,
2126
+ settle: (turnId: string) => void,
2127
+ ): void {
2128
+ const turnId = this.metricsTurnId;
2129
+ if (turnId === null) {
2130
+ return;
2131
+ }
2132
+ this.metricsTurnId = null;
2133
+ settle(turnId);
2134
+ log.info(
2135
+ {
2136
+ callSessionId: this.callSessionId,
2137
+ turnId,
2138
+ status,
2139
+ ...getVoiceMetricsAggregateFields(this.metrics.getSnapshot(), turnId),
2140
+ },
2141
+ "Phone call turn metrics",
2142
+ );
2143
+ }
2144
+
1950
2145
  private isCurrentRun(runVersion: number): boolean {
1951
2146
  return runVersion === this.llmRunVersion;
1952
2147
  }
@@ -299,6 +299,7 @@ export async function createInboundVoiceSession(
299
299
  const session = createCallSession({
300
300
  conversationId: voiceConversationId,
301
301
  provider: "twilio",
302
+ direction: "inbound",
302
303
  fromNumber,
303
304
  toNumber,
304
305
  });
@@ -438,6 +439,7 @@ export async function startCall(
438
439
  signal?.throwIfAborted();
439
440
 
440
441
  const session = createCallSession({
442
+ direction: "outbound",
441
443
  conversationId,
442
444
  provider: "twilio",
443
445
  fromNumber,
@@ -976,6 +978,7 @@ export async function startVerificationCall(
976
978
  });
977
979
 
978
980
  const session = createCallSession({
981
+ direction: "outbound",
979
982
  conversationId,
980
983
  provider: "twilio",
981
984
  fromNumber: identityResult.fromNumber,
@@ -1109,6 +1112,7 @@ export async function startInviteCall(
1109
1112
  });
1110
1113
 
1111
1114
  const session = createCallSession({
1115
+ direction: "outbound",
1112
1116
  conversationId,
1113
1117
  provider: "twilio",
1114
1118
  fromNumber: identityResult.fromNumber,
@@ -1,6 +1,10 @@
1
- import { and, desc, eq, notInArray, or } from "drizzle-orm";
1
+ import { and, desc, eq, like, notInArray, or } from "drizzle-orm";
2
2
  import { v4 as uuid } from "uuid";
3
3
 
4
+ import {
5
+ recordPhoneCallEnded,
6
+ recordPhoneCallStarted,
7
+ } from "../onboarding/onboarding-events-store.js";
4
8
  import { getDb } from "../persistence/db-connection.js";
5
9
  import { rawChanges, rawRun } from "../persistence/raw-query.js";
6
10
  import {
@@ -8,10 +12,16 @@ import {
8
12
  callPendingQuestions,
9
13
  callSessions,
10
14
  } from "../persistence/schema/index.js";
15
+ import {
16
+ type PhoneCallDirection,
17
+ phoneCallEndScreen,
18
+ phoneCallSilenceReason,
19
+ phoneCallStartScreen,
20
+ } from "../telemetry/phone-call-funnel.js";
11
21
  import { getLogger } from "../util/logger.js";
12
22
  import { cast, createRowMapper } from "../util/row-mapper.js";
13
23
  import { syncActiveCallLeaseFromSession } from "./active-call-lease.js";
14
- import { validateTransition } from "./call-state-machine.js";
24
+ import { isTerminalState, validateTransition } from "./call-state-machine.js";
15
25
  import type {
16
26
  CallEvent,
17
27
  CallEventType,
@@ -85,6 +95,12 @@ export function createCallSession(opts: {
85
95
  provider: string;
86
96
  fromNumber: string;
87
97
  toNumber: string;
98
+ /**
99
+ * Which way the call is being placed, for the funnel stamp. Stated rather
100
+ * than derived: a verification or invite call carries no task and is still
101
+ * outbound, so task presence is not a direction.
102
+ */
103
+ direction: PhoneCallDirection;
88
104
  task?: string;
89
105
  callMode?: string;
90
106
  verificationSessionId?: string;
@@ -122,6 +138,14 @@ export function createCallSession(opts: {
122
138
  updatedAt: now,
123
139
  };
124
140
  db.insert(callSessions).values(row).run();
141
+ // The call is attempted the moment its row exists, which is before the
142
+ // provider dials: a call that never connects is exactly the one the funnel
143
+ // needs to count. An outbound attempt the ingress preflight rejects never
144
+ // reaches here; see `telemetry/phone-call-funnel.ts`.
145
+ recordPhoneCallStarted({
146
+ callSessionId: row.id,
147
+ screen: phoneCallStartScreen(opts.direction, row.callMode),
148
+ });
125
149
  return { ...row, skipDisclosure };
126
150
  }
127
151
 
@@ -224,6 +248,13 @@ export function updateCallSession(
224
248
  .where(eq(callSessions.id, id))
225
249
  .run();
226
250
 
251
+ // Terminal states are immutable and the validator above rejects any write
252
+ // that follows one, so the first terminal transition to get here is the only
253
+ // one: the ended event needs no separate once-only latch.
254
+ if (updates.status && isTerminalState(updates.status)) {
255
+ recordCallEndedEvent(id, updates.status);
256
+ }
257
+
227
258
  opts?.beforeLeaseSync?.();
228
259
 
229
260
  if (shouldSyncActiveLease) {
@@ -240,6 +271,60 @@ export function updateCallSession(
240
271
 
241
272
  // ── Recovery queries ─────────────────────────────────────────────────
242
273
 
274
+ /**
275
+ * Whether the call's own event log holds an event of this type. Existence
276
+ * only: the row itself is never read.
277
+ */
278
+ function hasCallEvent(
279
+ id: string,
280
+ eventType: CallEventType,
281
+ payloadContains?: string,
282
+ ): boolean {
283
+ const db = getDb();
284
+ const row = db
285
+ .select({ id: callEvents.id })
286
+ .from(callEvents)
287
+ .where(
288
+ and(
289
+ eq(callEvents.callSessionId, id),
290
+ eq(callEvents.eventType, eventType),
291
+ ...(payloadContains
292
+ ? [like(callEvents.payloadJson, `%${payloadContains}%`)]
293
+ : []),
294
+ ),
295
+ )
296
+ .limit(1)
297
+ .get();
298
+ return row !== undefined;
299
+ }
300
+
301
+ /**
302
+ * Emit the call's funnel end event, classifying a call that never took a
303
+ * caller turn by how far it got. Both signals come from the call's own event
304
+ * log: `call_connected` is written when the media stream opens, and
305
+ * `caller_spoke` when the caller is heard.
306
+ *
307
+ * `caller_spoke` carries two senses, though: a transcribed utterance
308
+ * (`transcript`) and a single DTMF digit (`dtmfDigit`). Only the first is a
309
+ * conversational turn, so the payload discriminates them. Splitting DTMF into
310
+ * an event type of its own would be the real fix, but that vocabulary is read
311
+ * by other consumers and is not this change's to alter.
312
+ */
313
+ function recordCallEndedEvent(id: string, status: CallStatus): void {
314
+ const spoke = hasCallEvent(id, "caller_spoke", '"transcript"');
315
+ const silence = spoke
316
+ ? null
317
+ : phoneCallSilenceReason({
318
+ connected: hasCallEvent(id, "call_connected"),
319
+ });
320
+
321
+ recordPhoneCallEnded({
322
+ callSessionId: id,
323
+ screen: phoneCallEndScreen(status, silence),
324
+ outcome: status === "failed" ? "failed" : "completed",
325
+ });
326
+ }
327
+
243
328
  /**
244
329
  * Returns all call sessions that are in a non-terminal state
245
330
  * (i.e. not completed, failed, or cancelled). Used during daemon startup
@@ -2,15 +2,17 @@
2
2
  * Guardian action expiry notices.
3
3
  *
4
4
  * Sends "this request has expired" notices to guardian delivery
5
- * destinations — vellum conversations get an assistant message, external
6
- * channels (telegram, slack) get a direct channel reply.
5
+ * destinations: vellum conversations get an assistant message, and external
6
+ * channels get a direct channel reply on the route
7
+ * `resolveDeliverCallbackUrlForChannel` names for the channel.
7
8
  */
8
9
 
9
10
  import { DELIVERY_STATUS } from "@vellumai/gateway-client";
10
11
 
12
+ import { resolveDeliverCallbackUrlForChannel } from "../approvals/guardian-channel-delivery.js";
11
13
  import { addMessage } from "../persistence/conversation-crud.js";
12
14
  import { deliverChannelReply } from "../runtime/gateway-client.js";
13
- import { composeGuardianActionMessageGenerative } from "../runtime/guardian-action-message-composer.js";
15
+ import { composeGuardianActionMessage } from "../runtime/guardian-action-message-composer.js";
14
16
  import { getLogger } from "../util/logger.js";
15
17
 
16
18
  const log = getLogger("guardian-action-sweep");
@@ -27,7 +29,12 @@ export interface ExpiryDeliveryInfo {
27
29
  /**
28
30
  * Send expiry notices to all delivery destinations for a guardian action
29
31
  * request. Handles both vellum/mac conversation messages and external channel
30
- * replies (telegram, slack).
32
+ * replies.
33
+ *
34
+ * A channel delivery's `destinationChatId` is the notification endpoint the
35
+ * card went to. On Discord that is the guardian's user snowflake, so the
36
+ * notice must take the `dm`-marked route that opens their DM; posted as a
37
+ * channel id it would address nothing.
31
38
  *
32
39
  * Deliveries must be captured *before* their status is changed to 'expired'
33
40
  * so the sent/pending filter still matches.
@@ -45,9 +52,8 @@ export async function sendGuardianExpiryNotices(
45
52
  }
46
53
 
47
54
  try {
48
- const expiryText = await composeGuardianActionMessageGenerative({
55
+ const expiryText = composeGuardianActionMessage({
49
56
  scenario: "guardian_stale_expired",
50
- channel: delivery.destinationChannel,
51
57
  });
52
58
 
53
59
  if (
@@ -69,8 +75,16 @@ export async function sendGuardianExpiryNotices(
69
75
  },
70
76
  );
71
77
  } else if (delivery.destinationChatId) {
72
- // External channel — send expiry notice via direct delivery
73
- const deliverUrl = `/deliver/${delivery.destinationChannel}`;
78
+ const deliverUrl = resolveDeliverCallbackUrlForChannel(
79
+ delivery.destinationChannel,
80
+ );
81
+ if (!deliverUrl) {
82
+ log.warn(
83
+ { deliveryId: delivery.id, channel: delivery.destinationChannel },
84
+ "No delivery route for guardian expiry notice channel",
85
+ );
86
+ continue;
87
+ }
74
88
  await deliverChannelReply(deliverUrl, {
75
89
  chatId: delivery.destinationChatId,
76
90
  text: expiryText,
@@ -68,9 +68,7 @@ function linearToMulaw(sample: number): number {
68
68
  // Determine segment
69
69
  const exponent = segmentSearch(sample);
70
70
  const mantissa = (sample >> (exponent + 3)) & 0x0f;
71
- const mulawByte = ~(sign | (exponent << 4) | mantissa) & 0xff;
72
-
73
- return mulawByte;
71
+ return ~(sign | (exponent << 4) | mantissa) & 0xff;
74
72
  }
75
73
 
76
74
  function segmentSearch(val: number): number {