@vellumai/assistant 0.12.1 → 0.12.2-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (577) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +20 -21
  3. package/docs/architecture/turn-actor.md +15 -8
  4. package/docs/guardian-request-flow.md +16 -0
  5. package/node_modules/@vellumai/slack-text/src/index.ts +13 -8
  6. package/node_modules/@vellumai/slack-text/src/label-resolution-entities.test.ts +95 -0
  7. package/openapi.yaml +360 -51
  8. package/package.json +3 -1
  9. package/scripts/smoke-desktop-window-theme.ts +186 -0
  10. package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
  11. package/src/__tests__/anthropic-provider.test.ts +0 -63
  12. package/src/__tests__/app-compiler.test.ts +6 -0
  13. package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
  14. package/src/__tests__/app-dir-path-guard.test.ts +1 -3
  15. package/src/__tests__/assistant-event-hub-machine-name.test.ts +6 -7
  16. package/src/__tests__/assistant-event-hub.test.ts +44 -0
  17. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  18. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
  19. package/src/__tests__/computer-use-tools.test.ts +19 -7
  20. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  21. package/src/__tests__/config-schema.test.ts +28 -29
  22. package/src/__tests__/config-watcher.test.ts +23 -8
  23. package/src/__tests__/connection-model-compat.test.ts +2 -2
  24. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  25. package/src/__tests__/conversation-pairing.test.ts +26 -0
  26. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
  27. package/src/__tests__/conversation-queue.test.ts +48 -207
  28. package/src/__tests__/conversation-retry-discard.test.ts +54 -1
  29. package/src/__tests__/conversation-runtime-assembly.test.ts +28 -0
  30. package/src/__tests__/conversation-skill-tools.test.ts +27 -0
  31. package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
  32. package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +1 -0
  33. package/src/__tests__/conversation-surfaces-point-at-capability.test.ts +1 -0
  34. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  35. package/src/__tests__/conversation-title-service.test.ts +14 -10
  36. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
  37. package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
  38. package/src/__tests__/credential-record-retire.test.ts +171 -1
  39. package/src/__tests__/credential-routes.test.ts +250 -3
  40. package/src/__tests__/credential-security-invariants.test.ts +5 -3
  41. package/src/__tests__/cu-unified-flow.test.ts +14 -4
  42. package/src/__tests__/document-sync-tags.test.ts +25 -0
  43. package/src/__tests__/drain-kick-guard.test.ts +0 -2
  44. package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
  45. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  46. package/src/__tests__/git-porcelain.test.ts +47 -0
  47. package/src/__tests__/heartbeat-service.test.ts +56 -40
  48. package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
  49. package/src/__tests__/host-bash-proxy.test.ts +26 -45
  50. package/src/__tests__/host-browser-proxy.test.ts +6 -17
  51. package/src/__tests__/host-cu-proxy.test.ts +596 -0
  52. package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
  53. package/src/__tests__/host-file-proxy.test.ts +11 -23
  54. package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
  55. package/src/__tests__/list-messages-camera-frame.test.ts +121 -0
  56. package/src/__tests__/llm-catalog-parity.test.ts +25 -0
  57. package/src/__tests__/mcp-auth-routes.test.ts +6 -9
  58. package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
  59. package/src/__tests__/mcp-health-check.test.ts +8 -11
  60. package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
  61. package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
  62. package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
  63. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  64. package/src/__tests__/messaging-send-tool.test.ts +355 -548
  65. package/src/__tests__/model-intents.test.ts +3 -0
  66. package/src/__tests__/notification-vellum-adapter.test.ts +85 -2
  67. package/src/__tests__/oauth-commands-routes.test.ts +168 -0
  68. package/src/__tests__/oauth-provider-profiles.test.ts +18 -0
  69. package/src/__tests__/oauth-provider-serializer.test.ts +22 -0
  70. package/src/__tests__/oauth-providers-routes.test.ts +1 -0
  71. package/src/__tests__/openai-provider.test.ts +1 -0
  72. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  73. package/src/__tests__/platform-callback-registration.test.ts +8 -16
  74. package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
  75. package/src/__tests__/plugin-import-boundary-guard.test.ts +14 -0
  76. package/src/__tests__/qdrant-manager.test.ts +132 -0
  77. package/src/__tests__/resolve-app.test.ts +135 -0
  78. package/src/__tests__/scaffold-managed-skill-tool.test.ts +229 -6
  79. package/src/__tests__/script-proxy-certs.test.ts +35 -0
  80. package/src/__tests__/secret-routes-acp-guard.test.ts +59 -1
  81. package/src/__tests__/service-account-token.test.ts +247 -0
  82. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  83. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  84. package/src/__tests__/skills.test.ts +9 -4
  85. package/src/__tests__/source-fingerprint.test.ts +184 -0
  86. package/src/__tests__/subagent-tool-gate-mode.test.ts +70 -3
  87. package/src/__tests__/subagent-tools.test.ts +3 -2
  88. package/src/__tests__/telephony-tts-guard.test.ts +38 -3
  89. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  90. package/src/__tests__/ui-channel-variants.test.ts +1 -56
  91. package/src/__tests__/unicode.test.ts +13 -0
  92. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
  93. package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
  94. package/src/__tests__/workspace-git-service.test.ts +26 -0
  95. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
  96. package/src/acp/__tests__/acp-claude-oauth.test.ts +257 -6
  97. package/src/acp/__tests__/acp-credentials.test.ts +13 -0
  98. package/src/acp/__tests__/agent-process.test.ts +258 -4
  99. package/src/acp/__tests__/auth-required.test.ts +96 -0
  100. package/src/acp/__tests__/claude-token-refresh.test.ts +257 -0
  101. package/src/acp/__tests__/client-handler.test.ts +90 -2
  102. package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
  103. package/src/acp/__tests__/prepare-agent-env.test.ts +60 -1
  104. package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
  105. package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
  106. package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
  107. package/src/acp/__tests__/session-manager.test.ts +895 -4
  108. package/src/acp/acp-claude-oauth.ts +328 -14
  109. package/src/acp/acp-credentials.ts +19 -0
  110. package/src/acp/agent-process.test.ts +4 -1
  111. package/src/acp/agent-process.ts +96 -10
  112. package/src/acp/auth-required.ts +50 -1
  113. package/src/acp/auto-install.test.ts +9 -7
  114. package/src/acp/claude-token-refresh.ts +150 -0
  115. package/src/acp/client-handler.ts +16 -3
  116. package/src/acp/model-config.test.ts +166 -0
  117. package/src/acp/model-config.ts +138 -0
  118. package/src/acp/prepare-agent-env.ts +21 -6
  119. package/src/acp/resolve-agent.test.ts +220 -6
  120. package/src/acp/resolve-agent.ts +88 -23
  121. package/src/acp/session-manager.test.ts +196 -3
  122. package/src/acp/session-manager.ts +441 -18
  123. package/src/acp/types.ts +50 -5
  124. package/src/agent/loop.ts +1 -0
  125. package/src/api/events/acp-session-model-update.test.ts +88 -0
  126. package/src/api/events/acp-session-model-update.ts +46 -0
  127. package/src/api/events/app-files-changed.ts +4 -3
  128. package/src/api/events/notification-intent.ts +2 -0
  129. package/src/api/events/user-message-echo.ts +7 -1
  130. package/src/api/index.ts +6 -0
  131. package/src/api/responses/conversation-message.ts +5 -0
  132. package/src/apps/app-store.ts +1 -1
  133. package/src/apps/resolve-app.ts +81 -0
  134. package/src/apps/source-fingerprint.ts +266 -0
  135. package/src/avatar/identity-avatar.ts +7 -2
  136. package/src/bundler/app-compiler.ts +15 -0
  137. package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
  138. package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
  139. package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
  140. package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
  141. package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
  142. package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
  143. package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
  144. package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
  145. package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
  146. package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
  147. package/src/calls/__tests__/voice-control-protocol.test.ts +120 -25
  148. package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
  149. package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
  150. package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
  151. package/src/calls/__tests__/voice-session-bridge.test.ts +56 -1
  152. package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
  153. package/src/calls/barge-in-guard.ts +118 -0
  154. package/src/calls/call-constants.ts +24 -4
  155. package/src/calls/call-controller.ts +499 -199
  156. package/src/calls/call-transport.ts +16 -0
  157. package/src/calls/media-stream-output.ts +32 -0
  158. package/src/calls/media-stream-server.ts +82 -30
  159. package/src/calls/media-stream-stt-session.ts +15 -0
  160. package/src/{live-voice → calls}/progress-narration.ts +2 -2
  161. package/src/calls/resolve-call-tts-provider.ts +33 -2
  162. package/src/calls/tts-call-strategy.ts +9 -5
  163. package/src/calls/types.ts +2 -0
  164. package/src/calls/voice-control-protocol.ts +155 -21
  165. package/src/calls/voice-leg-coordinator.ts +189 -0
  166. package/src/calls/voice-progress-cadence.ts +423 -0
  167. package/src/calls/voice-session-bridge.ts +92 -72
  168. package/src/calls/voice-triage-escalate.ts +178 -59
  169. package/src/cli/commands/__tests__/apps.test.ts +245 -0
  170. package/src/cli/commands/__tests__/bash.test.ts +53 -0
  171. package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
  172. package/src/cli/commands/apps.help.ts +67 -13
  173. package/src/cli/commands/apps.ts +183 -0
  174. package/src/cli/commands/bash.help.ts +8 -13
  175. package/src/cli/commands/bash.ts +7 -25
  176. package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
  177. package/src/cli/commands/channels/index.help.ts +70 -2
  178. package/src/cli/commands/channels/index.ts +16 -3
  179. package/src/cli/commands/channels/send.ts +128 -0
  180. package/src/cli/commands/credentials.help.ts +6 -2
  181. package/src/cli/commands/credentials.ts +2 -2
  182. package/src/cli/commands/mcp.help.ts +8 -8
  183. package/src/cli/commands/mcp.ts +1 -1
  184. package/src/cli/commands/oauth/status.ts +5 -0
  185. package/src/cli/commands/roadmap.help.ts +5 -1
  186. package/src/cli/commands/roadmap.ts +1 -1
  187. package/src/cli/lib/bundled-marketplace.json +103 -90
  188. package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
  189. package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
  190. package/src/config/__tests__/platform-identity.test.ts +255 -0
  191. package/src/config/__tests__/platform-rehydration.test.ts +99 -9
  192. package/src/config/acp-agent-write.ts +43 -0
  193. package/src/config/acp-defaults.test.ts +13 -3
  194. package/src/config/acp-defaults.ts +23 -6
  195. package/src/config/acp-schema.test.ts +81 -0
  196. package/src/config/acp-schema.ts +30 -2
  197. package/src/config/bundled-skills/acp/SKILL.md +22 -14
  198. package/src/config/bundled-skills/acp/TOOLS.json +5 -1
  199. package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
  200. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  201. package/src/config/bundled-skills/computer-use/SKILL.md +29 -2
  202. package/src/config/bundled-skills/computer-use/TOOLS.json +99 -0
  203. package/src/config/bundled-skills/computer-use/tools/computer-use-sequence.ts +12 -0
  204. package/src/config/bundled-skills/media-processing/services/preprocess.ts +2 -1
  205. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +2 -1
  206. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -1
  207. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  208. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  209. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
  210. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  211. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -1
  212. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -1
  213. package/src/config/bundled-skills/schedule/SKILL.md +3 -4
  214. package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
  215. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
  216. package/src/config/bundled-skills/skill-management/TOOLS.json +6 -2
  217. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -2
  218. package/src/config/default-profile-catalog.ts +1 -1
  219. package/src/config/env.ts +2 -2
  220. package/src/config/feature-flag-registry.json +33 -13
  221. package/src/config/loader.ts +2 -0
  222. package/src/config/memory-v3-gate.test.ts +31 -0
  223. package/src/config/memory-v3-gate.ts +20 -16
  224. package/src/config/platform-identity.ts +201 -0
  225. package/src/config/platform-rehydration.ts +11 -27
  226. package/src/config/schema.ts +2 -2
  227. package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
  228. package/src/config/schemas/__tests__/voice.test.ts +148 -0
  229. package/src/config/schemas/calls.ts +1 -28
  230. package/src/config/schemas/live-voice.ts +1 -173
  231. package/src/config/schemas/mcp.ts +1 -1
  232. package/src/config/schemas/memory-retrospective.ts +10 -0
  233. package/src/config/schemas/services.ts +18 -0
  234. package/src/config/schemas/skills.ts +0 -14
  235. package/src/config/schemas/voice.ts +150 -0
  236. package/src/credential-execution/process-manager.test.ts +18 -19
  237. package/src/credential-health/credential-health-service.ts +11 -17
  238. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
  239. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
  240. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
  241. package/src/daemon/app-change-notify.ts +45 -0
  242. package/src/daemon/config-watcher.ts +13 -194
  243. package/src/daemon/conversation-error.ts +3 -2
  244. package/src/daemon/conversation-media-retry.ts +2 -1
  245. package/src/daemon/conversation-process.ts +35 -64
  246. package/src/daemon/conversation-runtime-assembly.ts +20 -7
  247. package/src/daemon/conversation-skill-tools.ts +13 -31
  248. package/src/daemon/conversation-store.ts +1 -1
  249. package/src/daemon/conversation-surfaces.ts +21 -10
  250. package/src/daemon/conversation-tool-setup.ts +45 -59
  251. package/src/daemon/conversation.ts +0 -3
  252. package/src/daemon/dictation-profile-store.ts +2 -1
  253. package/src/daemon/handlers/shared.ts +2 -1
  254. package/src/daemon/handlers/skills.ts +6 -3
  255. package/src/daemon/host-bash-proxy.ts +8 -12
  256. package/src/daemon/host-browser-proxy.ts +15 -1
  257. package/src/daemon/host-cu-proxy.ts +224 -19
  258. package/src/daemon/host-file-proxy.ts +8 -10
  259. package/src/daemon/host-proxy-preactivation.ts +2 -1
  260. package/src/daemon/host-transfer-proxy.ts +14 -18
  261. package/src/daemon/lifecycle.ts +0 -5
  262. package/src/daemon/mcp-reload-service.ts +5 -5
  263. package/src/daemon/message-types/sync.ts +1 -0
  264. package/src/daemon/orphan-reaper.ts +1 -1
  265. package/src/daemon/providers-setup.ts +1 -1
  266. package/src/daemon/reaction-history-render.ts +2 -1
  267. package/src/daemon/shutdown-handlers.ts +1 -9
  268. package/src/daemon/skill-memory-refresh.ts +64 -0
  269. package/src/daemon/tool-side-effects.ts +7 -18
  270. package/src/desktop/__tests__/fake-desktop.ts +122 -0
  271. package/src/desktop/desktop-chrome-policy.test.ts +79 -0
  272. package/src/desktop/desktop-chrome-policy.ts +47 -0
  273. package/src/desktop/desktop-chrome-session.ts +37 -0
  274. package/src/desktop/desktop-dependencies.test.ts +102 -0
  275. package/src/desktop/desktop-dependencies.ts +308 -0
  276. package/src/desktop/desktop-feature.test.ts +18 -0
  277. package/src/desktop/desktop-feature.ts +19 -0
  278. package/src/desktop/desktop-panel-config.test.ts +135 -0
  279. package/src/desktop/desktop-panel-config.ts +149 -0
  280. package/src/desktop/desktop-session-manager.test.ts +730 -0
  281. package/src/desktop/desktop-session-manager.ts +850 -0
  282. package/src/desktop/desktop-stream-bridge.test.ts +395 -0
  283. package/src/desktop/desktop-stream-bridge.ts +214 -0
  284. package/src/desktop/desktop-wallpaper.test.ts +191 -0
  285. package/src/desktop/desktop-wallpaper.ts +113 -0
  286. package/src/desktop/desktop-window-drag.ts +177 -0
  287. package/src/desktop/desktop-window-manager-config.test.ts +230 -0
  288. package/src/desktop/desktop-window-manager-config.ts +238 -0
  289. package/src/desktop/desktop-window-theme.ts +170 -0
  290. package/src/heartbeat/heartbeat-service.ts +21 -3
  291. package/src/home/__tests__/feed-writer.test.ts +22 -0
  292. package/src/home/feed-writer.ts +11 -2
  293. package/src/hooks/types.ts +33 -0
  294. package/src/i18n/AGENTS.md +22 -0
  295. package/src/i18n/__tests__/i18n.test.ts +108 -0
  296. package/src/i18n/index.ts +16 -0
  297. package/src/i18n/locales.ts +93 -0
  298. package/src/i18n/messages.ts +61 -0
  299. package/src/i18n/title.ts +25 -0
  300. package/src/inbound/platform-callback-registration.ts +4 -5
  301. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +121 -0
  302. package/src/live-voice/__tests__/live-voice-events.test.ts +8 -7
  303. package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
  304. package/src/live-voice/__tests__/live-voice-progress.test.ts +94 -15
  305. package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +238 -0
  306. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  307. package/src/live-voice/__tests__/protocol.test.ts +39 -0
  308. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
  309. package/src/live-voice/__tests__/session-controls.test.ts +79 -0
  310. package/src/live-voice/live-voice-photo.ts +25 -6
  311. package/src/live-voice/live-voice-session.ts +297 -699
  312. package/src/live-voice/protocol.ts +60 -2
  313. package/src/live-voice/session-controls.ts +111 -0
  314. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  315. package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
  316. package/src/mcp/effective-config.ts +3 -3
  317. package/src/mcp/mcp-header-store.ts +29 -45
  318. package/src/mcp/spec-schema.ts +111 -0
  319. package/src/mcp/startup.ts +8 -4
  320. package/src/mcp/workspace-mcp-config.ts +188 -0
  321. package/src/messaging/provider.ts +10 -3
  322. package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
  323. package/src/messaging/providers/callback-routing.ts +18 -0
  324. package/src/messaging/providers/channel-transport.ts +56 -0
  325. package/src/messaging/providers/discord/transport.ts +17 -0
  326. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
  327. package/src/messaging/providers/slack/adapter.ts +7 -44
  328. package/src/messaging/providers/slack/transport.ts +16 -0
  329. package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
  330. package/src/messaging/providers/telegram-bot/client.ts +0 -26
  331. package/src/messaging/providers/telegram-bot/transport.ts +20 -0
  332. package/src/messaging/providers/whatsapp/adapter.ts +1 -48
  333. package/src/messaging/providers/whatsapp/transport.ts +18 -0
  334. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
  335. package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
  336. package/src/notifications/__tests__/guardian-feed-projection.test.ts +158 -2
  337. package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
  338. package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
  339. package/src/notifications/adapters/macos.ts +3 -0
  340. package/src/notifications/assistant-reply-producer.ts +8 -3
  341. package/src/notifications/conversation-pairing.ts +36 -16
  342. package/src/notifications/delivered-post-record.ts +11 -1
  343. package/src/notifications/guardian-feed-projection.ts +125 -5
  344. package/src/notifications/home-feed-side-effect.ts +55 -7
  345. package/src/notifications/schedule-result-producer.ts +66 -22
  346. package/src/oauth/AGENTS.md +2 -0
  347. package/src/oauth/__tests__/seed-providers-managed.test.ts +149 -0
  348. package/src/oauth/byo-connection.test.ts +105 -0
  349. package/src/oauth/byo-connection.ts +36 -6
  350. package/src/oauth/connection-resolver.test.ts +100 -0
  351. package/src/oauth/connection-resolver.ts +120 -3
  352. package/src/oauth/provider-serializer.ts +9 -0
  353. package/src/oauth/scope-utils.ts +24 -0
  354. package/src/oauth/seed-providers.ts +314 -27
  355. package/src/outbound-proxy/certs.ts +3 -5
  356. package/src/permissions/checker.test.ts +11 -6
  357. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  358. package/src/persistence/conversation-crud.ts +71 -63
  359. package/src/persistence/conversation-lineage.ts +12 -4
  360. package/src/persistence/conversation-queries.ts +2 -2
  361. package/src/persistence/conversation-title-placeholders.ts +23 -0
  362. package/src/persistence/conversation-title-service.ts +9 -26
  363. package/src/persistence/embeddings/qdrant-manager.ts +163 -18
  364. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  365. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  366. package/src/persistence/message-cursor.ts +78 -0
  367. package/src/persistence/message-reads.ts +30 -0
  368. package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
  369. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
  370. package/src/persistence/schema/memory-core.ts +5 -0
  371. package/src/platform/client.test.ts +4 -2
  372. package/src/platform/client.ts +2 -10
  373. package/src/plugin-api/constants.ts +2 -0
  374. package/src/plugin-api/index.ts +1 -0
  375. package/src/plugin-api/types.ts +2 -0
  376. package/src/plugins/defaults/index.ts +2 -0
  377. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
  378. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
  379. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
  380. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
  381. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
  382. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +5 -0
  383. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
  384. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
  385. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
  386. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
  387. package/src/plugins/defaults/memory/context-search/agent-runner.ts +3 -2
  388. package/src/plugins/defaults/memory/context-search/format.ts +3 -2
  389. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +6 -3
  390. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +3 -2
  391. package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -7
  392. package/src/plugins/defaults/memory/hooks/init.ts +10 -0
  393. package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
  394. package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
  395. package/src/plugins/defaults/memory/host-utils.ts +10 -0
  396. package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
  397. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
  398. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
  399. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
  400. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
  401. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
  402. package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
  403. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  404. package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
  405. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
  406. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
  407. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
  408. package/src/plugins/defaults/memory/startup.ts +28 -2
  409. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
  410. package/src/plugins/defaults/memory/substrate/page-index.ts +6 -1
  411. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  412. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +4 -2
  413. package/src/plugins/defaults/memory/v1/graph/extraction.ts +4 -2
  414. package/src/plugins/defaults/memory/v1/graph/retriever.ts +4 -1
  415. package/src/plugins/defaults/memory/v2/reranker.ts +6 -1
  416. package/src/plugins/defaults/memory/v3/__tests__/card.test.ts +24 -0
  417. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +125 -0
  418. package/src/plugins/defaults/memory/v3/__tests__/sections.test.ts +26 -0
  419. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +30 -0
  420. package/src/plugins/defaults/memory/v3/card.ts +3 -2
  421. package/src/plugins/defaults/memory/v3/pool-select.ts +5 -3
  422. package/src/plugins/defaults/memory/v3/sections.ts +9 -3
  423. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +5 -2
  424. package/src/plugins/defaults/memory/worker.ts +6 -0
  425. package/src/plugins/mcp-servers.ts +15 -75
  426. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  427. package/src/providers/__tests__/retry-callsite.test.ts +41 -0
  428. package/src/providers/__tests__/retry-outbound-sanitize.test.ts +176 -0
  429. package/src/providers/anthropic/client.ts +0 -47
  430. package/src/providers/gemini/client.ts +9 -23
  431. package/src/providers/gemini-thought-signature.test.ts +63 -0
  432. package/src/providers/gemini-thought-signature.ts +44 -0
  433. package/src/providers/inference/__tests__/endpoint-probe.test.ts +42 -3
  434. package/src/providers/inference/auth.ts +2 -3
  435. package/src/providers/inference/connection-availability.ts +27 -8
  436. package/src/providers/inference/endpoint-probe.ts +10 -1
  437. package/src/providers/inference/resolve-auth.ts +22 -4
  438. package/src/providers/inference/service-account-token.ts +248 -0
  439. package/src/providers/model-catalog.ts +34 -0
  440. package/src/providers/model-intents.ts +10 -5
  441. package/src/providers/openai/__tests__/chat-completions-output-token-limit.test.ts +141 -0
  442. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
  443. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
  444. package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
  445. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
  446. package/src/providers/openai/chat-completions-provider.ts +212 -79
  447. package/src/providers/openai/codex-models.ts +0 -4
  448. package/src/providers/openai/google-thought-signature.ts +320 -0
  449. package/src/providers/opencode/client.test.ts +73 -4
  450. package/src/providers/opencode/client.ts +60 -3
  451. package/src/providers/openrouter/client.ts +20 -0
  452. package/src/providers/outbound-request-sanitize.ts +96 -0
  453. package/src/providers/retry.ts +30 -19
  454. package/src/providers/unparseable-tool-args.ts +5 -1
  455. package/src/runtime/AGENTS.md +3 -1
  456. package/src/runtime/__tests__/channel-send.test.ts +443 -0
  457. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
  458. package/src/runtime/__tests__/host-observe.test.ts +40 -1
  459. package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
  460. package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
  461. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
  462. package/src/runtime/agent-wake.ts +2 -1
  463. package/src/runtime/assistant-event-hub.ts +21 -3
  464. package/src/runtime/auth/same-actor.ts +38 -3
  465. package/src/runtime/channel-send.ts +316 -0
  466. package/src/runtime/host-observe.ts +11 -1
  467. package/src/runtime/http-server.ts +160 -151
  468. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +16 -5
  469. package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
  470. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +275 -0
  471. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
  472. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
  473. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
  474. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
  475. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
  476. package/src/runtime/routes/acp-claude-auth-routes.ts +11 -3
  477. package/src/runtime/routes/acp-routes.test.ts +15 -0
  478. package/src/runtime/routes/acp-routes.ts +72 -9
  479. package/src/runtime/routes/app-management-routes.ts +90 -2
  480. package/src/runtime/routes/app-routes.ts +1 -1
  481. package/src/runtime/routes/channel-send-routes.ts +85 -0
  482. package/src/runtime/routes/conversation-list-routes.ts +14 -3
  483. package/src/runtime/routes/conversation-query-routes.ts +40 -0
  484. package/src/runtime/routes/conversation-routes.ts +16 -3
  485. package/src/runtime/routes/credential-routes.ts +20 -14
  486. package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
  487. package/src/runtime/routes/desktop-setup-routes.ts +35 -0
  488. package/src/runtime/routes/diagnostics-routes.ts +61 -4
  489. package/src/runtime/routes/events-routes.ts +4 -0
  490. package/src/runtime/routes/host-cu-routes.ts +6 -0
  491. package/src/runtime/routes/index.ts +4 -2
  492. package/src/runtime/routes/log-export-routes.ts +2 -0
  493. package/src/runtime/routes/mcp-auth-routes.ts +39 -47
  494. package/src/runtime/routes/migration-routes.ts +6 -20
  495. package/src/runtime/routes/oauth-commands-routes.ts +50 -0
  496. package/src/runtime/routes/oauth-providers.ts +7 -0
  497. package/src/runtime/routes/platform-routes.ts +18 -15
  498. package/src/runtime/routes/workspace-routes.test.ts +55 -26
  499. package/src/runtime/routes/workspace-routes.ts +46 -63
  500. package/src/runtime/routes/workspace-tree-walk.test.ts +244 -0
  501. package/src/runtime/routes/workspace-tree-walk.ts +240 -0
  502. package/src/runtime/routes/workspace-utils.ts +17 -0
  503. package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
  504. package/src/runtime/services/conversation-serializer.ts +16 -3
  505. package/src/runtime/sync/resource-sync-events.ts +2 -1
  506. package/src/schedule/run-script.ts +2 -1
  507. package/src/schedule/schedule-store.ts +9 -2
  508. package/src/schedule/worker.ts +1 -1
  509. package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
  510. package/src/security/ces-http-record-backend.ts +120 -0
  511. package/src/security/ces-rpc-record-backend.ts +7 -0
  512. package/src/security/untrusted-content.ts +3 -1
  513. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
  514. package/src/skills/workspace-skill-md-signature.ts +50 -0
  515. package/src/subagent/consult-context.ts +12 -25
  516. package/src/tools/__tests__/side-effect-abort-guard.test.ts +1 -0
  517. package/src/tools/acp/list-agents.test.ts +3 -1
  518. package/src/tools/acp/spawn.test.ts +146 -11
  519. package/src/tools/acp/spawn.ts +21 -3
  520. package/src/tools/acp/status.test.ts +94 -0
  521. package/src/tools/acp/status.ts +40 -3
  522. package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
  523. package/src/tools/browser/browser-execution.ts +1 -1
  524. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +9 -3
  525. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +2 -1
  526. package/src/tools/browser/cdp-client/extension-cdp-client.ts +3 -2
  527. package/src/tools/browser/network-recorder.ts +2 -1
  528. package/src/tools/capability-offer.test.ts +22 -2
  529. package/src/tools/capability-offer.ts +16 -1
  530. package/src/tools/computer-use/definitions.ts +121 -2
  531. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
  532. package/src/tools/credentials/metadata-store.ts +100 -0
  533. package/src/tools/filesystem/search.ts +2 -1
  534. package/src/tools/host-filesystem/edit.test.ts +2 -1
  535. package/src/tools/host-filesystem/edit.ts +2 -2
  536. package/src/tools/host-filesystem/read.test.ts +2 -1
  537. package/src/tools/host-filesystem/read.ts +2 -2
  538. package/src/tools/host-filesystem/transfer.test.ts +2 -1
  539. package/src/tools/host-filesystem/transfer.ts +2 -2
  540. package/src/tools/host-filesystem/write.test.ts +2 -1
  541. package/src/tools/host-filesystem/write.ts +2 -2
  542. package/src/tools/host-terminal/host-shell.test.ts +16 -0
  543. package/src/tools/host-terminal/host-shell.ts +4 -20
  544. package/src/tools/network/web-search.ts +6 -1
  545. package/src/tools/policy-context.ts +6 -7
  546. package/src/tools/skills/scaffold-managed.ts +61 -3
  547. package/src/tools/subagent/spawn.ts +5 -8
  548. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
  549. package/src/tools/terminal/sanitized-bash.ts +105 -0
  550. package/src/tools/ui-surface/channel-variants.ts +10 -59
  551. package/src/tools/watch/watch-retro-report.ts +0 -8
  552. package/src/types/host-capabilities.ts +5 -3
  553. package/src/util/__tests__/language-subtag.test.ts +23 -1
  554. package/src/util/ca-bundle.ts +13 -0
  555. package/src/util/language-subtag.ts +16 -0
  556. package/src/util/platform.ts +8 -0
  557. package/src/util/unicode.ts +15 -1
  558. package/src/watch/watch-retro.ts +6 -8
  559. package/src/watch/watch-timeline.ts +2 -2
  560. package/src/workflows/run-manager.ts +2 -1
  561. package/src/workspace/git-porcelain.ts +66 -0
  562. package/src/workspace/git-service.ts +6 -55
  563. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
  564. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
  565. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
  566. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
  567. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
  568. package/src/workspace/migrations/registry.ts +6 -0
  569. package/src/__tests__/app-source-watcher.test.ts +0 -218
  570. package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
  571. package/src/__tests__/speaker-identification.test.ts +0 -56
  572. package/src/calls/speaker-identification.ts +0 -233
  573. package/src/daemon/app-source-watcher.ts +0 -218
  574. package/src/messaging/providers/whatsapp/client.ts +0 -29
  575. package/src/runtime/routes/debug-bash-routes.ts +0 -173
  576. package/src/util/fs-watcher-error.ts +0 -36
  577. /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
@@ -8,17 +8,17 @@ import {
8
8
  test,
9
9
  } from "bun:test";
10
10
 
11
- mock.module("../config/env.js", () => ({ isHttpAuthDisabled: () => true }));
11
+ mock.module("../../config/env.js", () => ({ isHttpAuthDisabled: () => true }));
12
12
 
13
13
  // Guardian requests/deliveries are created through the gateway client; serve
14
14
  // that surface from the in-memory sim so dispatch never opens IPC.
15
15
  import {
16
16
  bridgeState,
17
17
  gatewayGuardianRequestsStoreBridge,
18
- } from "./helpers/gateway-guardian-requests-store-bridge.js";
18
+ } from "../../__tests__/helpers/gateway-guardian-requests-store-bridge.js";
19
19
 
20
20
  mock.module(
21
- "../channels/gateway-guardian-requests.js",
21
+ "../../channels/gateway-guardian-requests.js",
22
22
  () => gatewayGuardianRequestsStoreBridge,
23
23
  );
24
24
 
@@ -36,7 +36,7 @@ mock.module(
36
36
  let mockResolvableProviderKeys: ((service: string) => string | null) | null =
37
37
  null;
38
38
 
39
- mock.module("../security/secure-keys.js", () => ({
39
+ mock.module("../../security/secure-keys.js", () => ({
40
40
  getSecureKeyAsync: async () => null,
41
41
  getSecureKey: () => null,
42
42
  getProviderKeyAsync: async (service: string) =>
@@ -45,7 +45,19 @@ mock.module("../security/secure-keys.js", () => ({
45
45
  : "test-key",
46
46
  }));
47
47
 
48
- mock.module("../security/credential-key.js", () => ({
48
+ // Managed-speech defaulting reaches the platform client and the credential
49
+ // store. The controller tests drive the bridge mock on a microtask budget, so
50
+ // the configured providers stand as the effective ones here.
51
+ mock.module("../../config/managed-speech-defaults.js", () => ({
52
+ resolveEffectiveSpeechProviders: async (config?: {
53
+ services: { stt: { provider: string }; tts: { provider: string } };
54
+ }) => {
55
+ const services = (config ?? loadConfig()).services;
56
+ return { stt: services.stt.provider, tts: services.tts.provider };
57
+ },
58
+ }));
59
+
60
+ mock.module("../../security/credential-key.js", () => ({
49
61
  credentialKey: (...args: string[]) => args.join("/"),
50
62
  }));
51
63
 
@@ -56,7 +68,10 @@ let mockSilenceTimeoutMs = 30_000;
56
68
  let mockEndCallListenWindowMs = 0;
57
69
  let mockEndCallDrainMaxWaitMs = 15_000;
58
70
 
59
- mock.module("../calls/call-constants.js", () => ({
71
+ import * as realCallConstants from "../call-constants.js";
72
+
73
+ mock.module("../call-constants.js", () => ({
74
+ ...realCallConstants,
60
75
  getMaxCallDurationMs: () => 12 * 60 * 1000,
61
76
  getUserConsultationTimeoutMs: () => mockConsultationTimeoutMs,
62
77
  getSilenceTimeoutMs: () => mockSilenceTimeoutMs,
@@ -110,7 +125,7 @@ let mockStartVoiceTurn: Mock<any>;
110
125
 
111
126
  // ── Notification pipeline mock (prevent async handle leaks from fire-and-forget dispatches) ──
112
127
 
113
- mock.module("../notifications/emit-signal.js", () => ({
128
+ mock.module("../../notifications/emit-signal.js", () => ({
114
129
  emitNotificationSignal: async () => ({
115
130
  signalId: "mock-signal",
116
131
  deduplicated: false,
@@ -122,7 +137,7 @@ mock.module("../notifications/emit-signal.js", () => ({
122
137
 
123
138
  // Guardian principalId is resolved from the gateway binding reader. Mirror the
124
139
  // vellum binding seeded by resetTables so guardian dispatch can resolve it.
125
- mock.module("../contacts/guardian-delivery-reader.js", () => ({
140
+ mock.module("../../contacts/guardian-delivery-reader.js", () => ({
126
141
  getGuardianDelivery: async () => [
127
142
  {
128
143
  channelType: "vellum",
@@ -139,7 +154,7 @@ mock.module("../contacts/guardian-delivery-reader.js", () => ({
139
154
  anyGuardian: (list: unknown[]) => list[0],
140
155
  }));
141
156
 
142
- mock.module("../calls/voice-session-bridge.js", () => {
157
+ mock.module("../voice-session-bridge.js", () => {
143
158
  mockStartVoiceTurn = mock(createMockVoiceTurn(["Hello", " there"]));
144
159
  return {
145
160
  startVoiceTurn: (...args: unknown[]) => mockStartVoiceTurn(...args),
@@ -155,8 +170,8 @@ mock.module("../calls/voice-session-bridge.js", () => {
155
170
  import {
156
171
  _resetTtsProviderOverridesForTests,
157
172
  _setTtsProviderForTests,
158
- } from "../tts/provider-catalog.js";
159
- import type { TtsProvider } from "../tts/types.js";
173
+ } from "../../tts/provider-catalog.js";
174
+ import type { TtsProvider } from "../../tts/types.js";
160
175
 
161
176
  function registerTestTtsProviders(): void {
162
177
  _resetTtsProviderOverridesForTests();
@@ -206,26 +221,33 @@ registerTestTtsProviders();
206
221
 
207
222
  // ── Import source modules after all mocks are registered ────────────
208
223
 
209
- import { CallController } from "../calls/call-controller.js";
210
- import { getCallController } from "../calls/call-state.js";
224
+ import { resetDbForTesting } from "../../__tests__/db-test-helpers.js";
225
+ import { createGuardianBinding } from "../../__tests__/helpers/create-guardian-binding.js";
226
+ import { setConfig } from "../../__tests__/helpers/set-config.js";
227
+ import { loadConfig } from "../../config/loader.js";
228
+ import type { VoiceProgressConfig } from "../../config/schemas/voice.js";
229
+ import { getMessages } from "../../persistence/conversation-crud.js";
230
+ import { getDb } from "../../persistence/db-connection.js";
231
+ import { initializeDb } from "../../persistence/db-init.js";
232
+ import { resetTestTables } from "../../persistence/raw-query.js";
233
+ import { conversations } from "../../persistence/schema/index.js";
234
+ import { CallController } from "../call-controller.js";
235
+ import { getCallController } from "../call-state.js";
211
236
  import {
212
237
  createCallSession,
213
238
  getCallEvents,
214
239
  getCallSession,
215
240
  getPendingQuestion,
216
241
  updateCallSession,
217
- } from "../calls/call-store.js";
218
- import type { CallTransport } from "../calls/call-transport.js";
219
- import { resolveCallTtsProvider } from "../calls/resolve-call-tts-provider.js";
220
- import { loadConfig } from "../config/loader.js";
221
- import { getMessages } from "../persistence/conversation-crud.js";
222
- import { getDb } from "../persistence/db-connection.js";
223
- import { initializeDb } from "../persistence/db-init.js";
224
- import { resetTestTables } from "../persistence/raw-query.js";
225
- import { conversations } from "../persistence/schema/index.js";
226
- import { resetDbForTesting } from "./db-test-helpers.js";
227
- import { createGuardianBinding } from "./helpers/create-guardian-binding.js";
228
- import { setConfig } from "./helpers/set-config.js";
242
+ } from "../call-store.js";
243
+ import type { CallTransport } from "../call-transport.js";
244
+ import type { VoiceProgressNarrator } from "../progress-narration.js";
245
+ import { resolveCallTtsProvider } from "../resolve-call-tts-provider.js";
246
+ import {
247
+ ESCALATION_CONTINUATION_CONTENT,
248
+ FALLBACK_ESCALATION_BRIDGE,
249
+ FALLBACK_ESCALATION_BRIDGE_BY_LANGUAGE,
250
+ } from "../voice-triage-escalate.js";
229
251
 
230
252
  // Disable memory so persisted call messages skip background indexing, and
231
253
  // seed the ingress base URL used for synthesized-audio play URLs.
@@ -245,6 +267,8 @@ afterAll(() => {
245
267
 
246
268
  interface MockTransport extends CallTransport {
247
269
  sentTokens: Array<{ token: string; last: boolean }>;
270
+ /** Tokens sent with the system-copy flag (no caller-language hint). */
271
+ systemCopyTokens: string[];
248
272
  sentPlayUrls: string[];
249
273
  endCalled: boolean;
250
274
  endReason: string | undefined;
@@ -254,6 +278,7 @@ interface MockTransport extends CallTransport {
254
278
  function createMockTransport(): MockTransport {
255
279
  const state = {
256
280
  sentTokens: [] as Array<{ token: string; last: boolean }>,
281
+ systemCopyTokens: [] as string[],
257
282
  sentPlayUrls: [] as string[],
258
283
  _endCalled: false,
259
284
  _endReason: undefined as string | undefined,
@@ -264,6 +289,9 @@ function createMockTransport(): MockTransport {
264
289
  get sentTokens() {
265
290
  return state.sentTokens;
266
291
  },
292
+ get systemCopyTokens() {
293
+ return state.systemCopyTokens;
294
+ },
267
295
  get sentPlayUrls() {
268
296
  return state.sentPlayUrls;
269
297
  },
@@ -276,8 +304,15 @@ function createMockTransport(): MockTransport {
276
304
  get cancelPendingSpeechCount() {
277
305
  return state._cancelPendingSpeechCount;
278
306
  },
279
- sendTextToken(token: string, last: boolean) {
307
+ sendTextToken(
308
+ token: string,
309
+ last: boolean,
310
+ opts?: { systemCopy?: boolean },
311
+ ) {
280
312
  state.sentTokens.push({ token, last });
313
+ if (opts?.systemCopy === true) {
314
+ state.systemCopyTokens.push(token);
315
+ }
281
316
  },
282
317
  sendPlayUrl(url: string) {
283
318
  state.sentPlayUrls.push(url);
@@ -372,13 +407,16 @@ function setupController(
372
407
  task?: string,
373
408
  opts?: {
374
409
  assistantId?: string;
375
- trustContext?: import("../daemon/trust-context-types.js").TrustContext;
410
+ trustContext?: import("../../daemon/trust-context-types.js").TrustContext;
376
411
  /** Simulate the media-stream transport's PCM requirement. */
377
412
  requiresPcmAudio?: boolean;
378
413
  /** Simulate a transport that gates teardown on playback drain. */
379
414
  awaitPlaybackDrained?: () => Promise<void>;
380
415
  /** Synthesis-language resolver, as the media-stream server wires it. */
381
416
  resolveSynthesisLanguage?: () => string | undefined;
417
+ /** Progress narration seam; an explicit null keeps the call silent. */
418
+ progressNarrator?: VoiceProgressNarrator | null;
419
+ progressConfig?: VoiceProgressConfig;
382
420
  },
383
421
  ) {
384
422
  ensureConversation("conv-ctrl-test");
@@ -403,6 +441,11 @@ function setupController(
403
441
  assistantId: opts?.assistantId,
404
442
  trustContext: opts?.trustContext,
405
443
  resolveSynthesisLanguage: opts?.resolveSynthesisLanguage,
444
+ // Tests that do not exercise narration keep the default narrator out of
445
+ // the way: a null narrator never generates or speaks.
446
+ progressNarrator:
447
+ opts?.progressNarrator === undefined ? null : opts.progressNarrator,
448
+ progressConfig: opts?.progressConfig,
406
449
  });
407
450
  return { session, relay: transport, controller };
408
451
  }
@@ -506,35 +549,6 @@ describe("call-controller", () => {
506
549
  controller.destroy();
507
550
  });
508
551
 
509
- test("handleCallerUtterance: includes speaker context in voice turn content", async () => {
510
- mockStartVoiceTurn.mockImplementation(
511
- async (opts: {
512
- content: string;
513
- onTextDelta: (t: string) => void;
514
- onComplete: () => void;
515
- }) => {
516
- expect(opts.content).toContain(
517
- '[SPEAKER id="speaker-1" label="Aaron" source="provider" confidence="0.91"]',
518
- );
519
- expect(opts.content).toContain("Can you summarize this meeting?");
520
- opts.onTextDelta("Sure, here is a summary.");
521
- opts.onComplete();
522
- return { turnId: "run-1", abort: () => {} };
523
- },
524
- );
525
-
526
- const { controller } = setupController();
527
-
528
- await controller.handleCallerUtterance("Can you summarize this meeting?", {
529
- speakerId: "speaker-1",
530
- speakerLabel: "Aaron",
531
- speakerConfidence: 0.91,
532
- source: "provider",
533
- });
534
-
535
- controller.destroy();
536
- });
537
-
538
552
  test("startInitialGreeting: sends CALL_OPENING content and strips control marker from speech", async () => {
539
553
  let turnCount = 0;
540
554
  mockStartVoiceTurn.mockImplementation(
@@ -2287,7 +2301,7 @@ describe("call-controller", () => {
2287
2301
 
2288
2302
  // Compute expected digest independently using the same utility
2289
2303
  const { computeToolApprovalDigest } =
2290
- await import("../security/tool-approval-digest.js");
2304
+ await import("../../security/tool-approval-digest.js");
2291
2305
  const expectedDigest = computeToolApprovalDigest("send_email", {
2292
2306
  subject: "Hello",
2293
2307
  to: "bob@example.com",
@@ -3087,7 +3101,7 @@ describe("call-controller", () => {
3087
3101
  controller.destroy();
3088
3102
  });
3089
3103
 
3090
- test("synthesized provider: stays 'processing' during synthesis latency, so barge-in cannot abort an inaudible turn", async () => {
3104
+ test("synthesized provider: stays 'processing' during synthesis latency until the play URL is sent", async () => {
3091
3105
  const cfg = loadConfig();
3092
3106
  cfg.services.tts.provider = "fish-audio";
3093
3107
  cfg.services.tts.providers["fish-audio"].referenceId = "fish-ref-123";
@@ -3128,11 +3142,11 @@ describe("call-controller", () => {
3128
3142
  await new Promise((r) => setTimeout(r, 20));
3129
3143
 
3130
3144
  // No audio has reached the caller yet (play URL not sent), so the controller
3131
- // must stay in `processing` a barge-in here would abort a turn the caller
3132
- // cannot hear. See JARVIS-1232.
3145
+ // must stay in `processing`: the state tells the transport nothing is
3146
+ // audible, and only sustained caller speech (the transport's guard) may
3147
+ // cut the inaudible turn off. See JARVIS-1232.
3133
3148
  expect(relay.sentPlayUrls.length).toBe(0);
3134
3149
  expect(controller.getState()).toBe("processing");
3135
- expect(controller.handleBargeIn()).toBe(false);
3136
3150
 
3137
3151
  // Release audio → play URL sent → turn finishes and returns to idle.
3138
3152
  releaseChunk?.();
@@ -3404,7 +3418,7 @@ describe("call-controller", () => {
3404
3418
  function registerFishAudioSegmentRecorder(opts?: {
3405
3419
  onSynthesizeStream?: (
3406
3420
  text: string,
3407
- request: import("../tts/types.js").TtsSynthesisRequest,
3421
+ request: import("../../tts/types.js").TtsSynthesisRequest,
3408
3422
  onChunk: (chunk: Buffer) => void,
3409
3423
  ) => Promise<void>;
3410
3424
  }): { synthesizedTexts: string[] } {
@@ -4220,6 +4234,415 @@ describe("call-controller", () => {
4220
4234
  });
4221
4235
  });
4222
4236
 
4237
+ // ── Triage-and-escalate routing ─────────────────────────────────────
4238
+
4239
+ describe("triage-and-escalate routing", () => {
4240
+ interface LegOpts {
4241
+ content: string;
4242
+ routingLeg?: string;
4243
+ spokenEscalationBridge?: string;
4244
+ launchedAtMs?: number;
4245
+ voiceTelemetry?: { sessionId: string; entry?: string };
4246
+ callbacks?: {
4247
+ persisted_user_message_id?: (id: string) => void;
4248
+ persisted_assistant_message_id?: (id: string) => void;
4249
+ tool_use_start?: (
4250
+ toolName: string,
4251
+ detail?: { toolUseId?: string },
4252
+ ) => void;
4253
+ tool_result?: (event: {
4254
+ toolName: string;
4255
+ toolUseId?: string;
4256
+ isError?: boolean;
4257
+ resultPreview: string;
4258
+ }) => void;
4259
+ };
4260
+ onTextDelta: (t: string) => void;
4261
+ onComplete: () => void;
4262
+ signal?: AbortSignal;
4263
+ }
4264
+
4265
+ /**
4266
+ * Script the bridge one leg per call: the front-door leg's tokens first,
4267
+ * then the escalated leg's. Records every call's options and which legs
4268
+ * the controller aborted.
4269
+ */
4270
+ function scriptLegs(
4271
+ legs: string[][],
4272
+ beforeComplete?: (opts: LegOpts, index: number) => void,
4273
+ ) {
4274
+ const calls: LegOpts[] = [];
4275
+ const aborted: number[] = [];
4276
+ mockStartVoiceTurn.mockImplementation(async (opts: LegOpts) => {
4277
+ const index = calls.length;
4278
+ calls.push(opts);
4279
+ for (const token of legs[index] ?? []) {
4280
+ opts.onTextDelta(token);
4281
+ }
4282
+ beforeComplete?.(opts, index);
4283
+ opts.onComplete();
4284
+ return { turnId: `run-${index}`, abort: () => aborted.push(index) };
4285
+ });
4286
+ return { calls, aborted };
4287
+ }
4288
+
4289
+ function spokenText(relay: MockTransport): string {
4290
+ return relay.sentTokens
4291
+ .filter((t) => !t.last)
4292
+ .map((t) => t.token)
4293
+ .join("");
4294
+ }
4295
+
4296
+ function assistantSpokePayload(sessionId: string): Record<string, unknown> {
4297
+ const spoke = getCallEvents(sessionId).filter(
4298
+ (e) => e.eventType === "assistant_spoke",
4299
+ );
4300
+ expect(spoke.length).toBe(1);
4301
+ return JSON.parse(spoke[0].payloadJson) as Record<string, unknown>;
4302
+ }
4303
+
4304
+ test("a front-door answer runs one leg and is spoken verbatim", async () => {
4305
+ const { calls, aborted } = scriptLegs([["Hello", " there."]]);
4306
+ const { session, relay, controller } = setupController();
4307
+
4308
+ await controller.handleCallerUtterance("Hi");
4309
+
4310
+ expect(calls.length).toBe(1);
4311
+ expect(calls[0].routingLeg).toBe("front-door");
4312
+ expect(calls[0].content).toBe("Hi");
4313
+ expect(calls[0].spokenEscalationBridge).toBeUndefined();
4314
+ expect(aborted).toEqual([]);
4315
+ expect(spokenText(relay)).toContain("Hello there.");
4316
+ expect(assistantSpokePayload(session.id).text).toBe("Hello there.");
4317
+
4318
+ controller.destroy();
4319
+ });
4320
+
4321
+ test("an escalate verdict speaks the holding phrase, aborts the front-door leg, and continues on the escalated leg", async () => {
4322
+ const { calls, aborted } = scriptLegs([
4323
+ // The verdict token may arrive split across deltas; the leading
4324
+ // "[" is held until it classifies.
4325
+ ["[", "1] Let me check", " that.", " Ignored past the cap."],
4326
+ ["The answer is 42."],
4327
+ ]);
4328
+ const { session, relay, controller } = setupController();
4329
+
4330
+ await controller.handleCallerUtterance("What is six times seven?");
4331
+
4332
+ expect(calls.length).toBe(2);
4333
+ expect(calls[0].routingLeg).toBe("front-door");
4334
+ expect(calls[1].routingLeg).toBe("escalated");
4335
+ expect(calls[1].content).toBe(ESCALATION_CONTINUATION_CONTENT);
4336
+ expect(calls[1].spokenEscalationBridge).toBe("Let me check that.");
4337
+ expect(aborted).toEqual([0]);
4338
+
4339
+ const spoken = spokenText(relay);
4340
+ expect(spoken).toContain("Let me check that.");
4341
+ expect(spoken).toContain("The answer is 42.");
4342
+ expect(spoken.indexOf("Let me check that.")).toBeLessThan(
4343
+ spoken.indexOf("The answer is 42."),
4344
+ );
4345
+ expect(spoken).not.toContain("[1]");
4346
+ expect(spoken).not.toContain("Ignored past the cap.");
4347
+ expect(assistantSpokePayload(session.id).text).toBe(
4348
+ "Let me check that. The answer is 42.",
4349
+ );
4350
+ // One end-of-turn signal for the whole turn, after the escalated leg.
4351
+ expect(relay.sentTokens.filter((t) => t.last).length).toBe(1);
4352
+
4353
+ controller.destroy();
4354
+ });
4355
+
4356
+ test("a bare escalate verdict speaks the canned bridge, audio-only", async () => {
4357
+ const { calls } = scriptLegs([["[1]"], ["Forty-two."]]);
4358
+ const { session, relay, controller } = setupController();
4359
+
4360
+ await controller.handleCallerUtterance("What is six times seven?");
4361
+
4362
+ expect(calls.length).toBe(2);
4363
+ expect(calls[1].spokenEscalationBridge).toBe(FALLBACK_ESCALATION_BRIDGE);
4364
+ expect(spokenText(relay)).toContain(FALLBACK_ESCALATION_BRIDGE);
4365
+ // The canned bridge is not the model's speech: the turn's text (and the
4366
+ // transcript row the bridge keeps) carry only the escalated answer.
4367
+ expect(assistantSpokePayload(session.id).text).toBe("Forty-two.");
4368
+
4369
+ controller.destroy();
4370
+ });
4371
+
4372
+ test("a leg cancelled mid-bridge never spawns the escalated leg", async () => {
4373
+ const calls: LegOpts[] = [];
4374
+ let releaseLeg: () => void = () => {};
4375
+ mockStartVoiceTurn.mockImplementation(async (opts: LegOpts) => {
4376
+ calls.push(opts);
4377
+ opts.onTextDelta("[1] Let me");
4378
+ await new Promise<void>((resolve) => {
4379
+ releaseLeg = resolve;
4380
+ opts.signal?.addEventListener("abort", () => resolve());
4381
+ });
4382
+ // The bridge reports a cancelled leg's completion too.
4383
+ opts.onComplete();
4384
+ return { turnId: "run-cancelled", abort: () => releaseLeg() };
4385
+ });
4386
+ const { relay, controller } = setupController();
4387
+
4388
+ const turnPromise = controller.handleCallerUtterance("Hi");
4389
+ for (let i = 0; i < 10; i++) {
4390
+ await Promise.resolve();
4391
+ }
4392
+ expect(calls.length).toBe(1);
4393
+
4394
+ controller.handleInterrupt();
4395
+ await turnPromise;
4396
+
4397
+ expect(calls.length).toBe(1);
4398
+ expect(spokenText(relay)).toBe("");
4399
+
4400
+ controller.destroy();
4401
+ });
4402
+
4403
+ test("passes the launch time, phone telemetry, and callbacks to the bridge", async () => {
4404
+ const before = Date.now();
4405
+ const { calls } = scriptLegs([["Done."]], (opts) => {
4406
+ opts.callbacks?.persisted_user_message_id?.("user-row-1");
4407
+ opts.callbacks?.tool_use_start?.("web_search", { toolUseId: "tu-1" });
4408
+ opts.callbacks?.tool_result?.({
4409
+ toolName: "web_search",
4410
+ toolUseId: "tu-1",
4411
+ isError: false,
4412
+ resultPreview: "three results",
4413
+ });
4414
+ opts.callbacks?.persisted_assistant_message_id?.("assistant-row-1");
4415
+ });
4416
+ const { session, controller } = setupController();
4417
+
4418
+ await controller.handleCallerUtterance("Look that up");
4419
+
4420
+ expect(calls.length).toBe(1);
4421
+ expect(calls[0].launchedAtMs).toBeGreaterThanOrEqual(before);
4422
+ expect(calls[0].launchedAtMs).toBeLessThanOrEqual(Date.now());
4423
+ expect(calls[0].voiceTelemetry).toEqual({
4424
+ sessionId: session.id,
4425
+ entry: "phone_inbound",
4426
+ });
4427
+
4428
+ const events = getCallEvents(session.id);
4429
+ const started = events.filter((e) => e.eventType === "tool_use_started");
4430
+ expect(started.length).toBe(1);
4431
+ expect(JSON.parse(started[0].payloadJson)).toEqual({
4432
+ toolName: "web_search",
4433
+ toolUseId: "tu-1",
4434
+ });
4435
+ const completed = events.filter(
4436
+ (e) => e.eventType === "tool_use_completed",
4437
+ );
4438
+ expect(completed.length).toBe(1);
4439
+ expect(JSON.parse(completed[0].payloadJson)).toEqual({
4440
+ toolName: "web_search",
4441
+ toolUseId: "tu-1",
4442
+ isError: false,
4443
+ resultPreview: "three results",
4444
+ });
4445
+ expect(assistantSpokePayload(session.id)).toEqual({
4446
+ text: "Done.",
4447
+ userMessageId: "user-row-1",
4448
+ assistantMessageId: "assistant-row-1",
4449
+ });
4450
+
4451
+ controller.destroy();
4452
+ });
4453
+
4454
+ test("the escalated leg's hidden continuation row is not the caller's utterance", async () => {
4455
+ const { calls } = scriptLegs(
4456
+ [["[1] One moment."], ["Here you go."]],
4457
+ (opts, index) => {
4458
+ opts.callbacks?.persisted_user_message_id?.(`user-row-${index}`);
4459
+ opts.callbacks?.persisted_assistant_message_id?.(
4460
+ `assistant-row-${index}`,
4461
+ );
4462
+ },
4463
+ );
4464
+ const { session, controller } = setupController();
4465
+
4466
+ await controller.handleCallerUtterance("Hi");
4467
+
4468
+ expect(calls.length).toBe(2);
4469
+ expect(assistantSpokePayload(session.id)).toEqual({
4470
+ text: "One moment. Here you go.",
4471
+ userMessageId: "user-row-0",
4472
+ assistantMessageId: "assistant-row-1",
4473
+ });
4474
+
4475
+ controller.destroy();
4476
+ });
4477
+
4478
+ test("native route: a canned bridge in a language the table lacks goes out as system copy", async () => {
4479
+ scriptLegs([["[1]"], ["Forty-two."]]);
4480
+ const { relay, controller } = setupController(undefined, {
4481
+ resolveSynthesisLanguage: () => "ko",
4482
+ });
4483
+
4484
+ await controller.handleCallerUtterance("What is six times seven?");
4485
+
4486
+ // The English fallback must not ride the caller's Korean hint.
4487
+ expect(relay.systemCopyTokens).toEqual([
4488
+ `${FALLBACK_ESCALATION_BRIDGE} `,
4489
+ ]);
4490
+ expect(spokenText(relay)).toContain("Forty-two.");
4491
+
4492
+ controller.destroy();
4493
+ });
4494
+
4495
+ test("native route: a localized canned bridge rides the caller's language like model text", async () => {
4496
+ scriptLegs([["[1]"], ["Cuarenta y dos."]]);
4497
+ const { relay, controller } = setupController(undefined, {
4498
+ resolveSynthesisLanguage: () => "es",
4499
+ });
4500
+
4501
+ await controller.handleCallerUtterance("¿Cuánto es seis por siete?");
4502
+
4503
+ expect(relay.systemCopyTokens).toEqual([]);
4504
+ expect(spokenText(relay)).toContain(
4505
+ FALLBACK_ESCALATION_BRIDGE_BY_LANGUAGE.es,
4506
+ );
4507
+
4508
+ controller.destroy();
4509
+ });
4510
+
4511
+ test("synthesized route: the canned bridge carries its own language hint, the answer the caller's", async () => {
4512
+ const requests: Array<{ text: string; language: string | undefined }> =
4513
+ [];
4514
+ registerFishAudioSegmentRecorder({
4515
+ onSynthesizeStream: async (text, request) => {
4516
+ requests.push({ text, language: request.language });
4517
+ },
4518
+ });
4519
+ scriptLegs([["[1]"], ["Forty-two."]]);
4520
+ const { controller } = setupController(undefined, {
4521
+ resolveSynthesisLanguage: () => "ko",
4522
+ });
4523
+
4524
+ await controller.handleCallerUtterance("What is six times seven?");
4525
+
4526
+ expect(requests).toEqual([
4527
+ { text: FALLBACK_ESCALATION_BRIDGE, language: "en" },
4528
+ { text: "Forty-two.", language: "ko" },
4529
+ ]);
4530
+
4531
+ controller.destroy();
4532
+ });
4533
+
4534
+ test("progress narration speaks into the escalated leg's tool loop, audio-only", async () => {
4535
+ const narrationConfig: VoiceProgressConfig = {
4536
+ enabled: true,
4537
+ opsThreshold: 1,
4538
+ idleIntervalMs: 60_000,
4539
+ maxSilenceMs: 60_000,
4540
+ longOpMs: 15_000,
4541
+ minGapMs: 10,
4542
+ generationTimeoutMs: 1_500,
4543
+ };
4544
+ const narrator: VoiceProgressNarrator = {
4545
+ generateProgressText: async (input) => {
4546
+ expect(input.transcriptSoFar).toBe("What is on my calendar?");
4547
+ expect(input.currentOp?.toolName).toBe("calendar_list");
4548
+ return "Pulling up your calendar now.";
4549
+ },
4550
+ };
4551
+ const { session, relay, controller } = setupController(undefined, {
4552
+ progressNarrator: narrator,
4553
+ progressConfig: narrationConfig,
4554
+ });
4555
+ const calls: LegOpts[] = [];
4556
+ mockStartVoiceTurn.mockImplementation(async (opts: LegOpts) => {
4557
+ const index = calls.length;
4558
+ calls.push(opts);
4559
+ if (index === 0) {
4560
+ opts.onTextDelta("[1] One moment.");
4561
+ opts.onComplete();
4562
+ return { turnId: "run-front", abort: () => {} };
4563
+ }
4564
+ // The escalated leg runs a tool; narration must land while it runs.
4565
+ // The bridge just spoken is a floor holder, so the tool starts after
4566
+ // the (test-short) minimum gap.
4567
+ await new Promise((r) => setTimeout(r, 25));
4568
+ opts.callbacks?.tool_use_start?.("calendar_list", { toolUseId: "c1" });
4569
+ await pollUntil(() =>
4570
+ relay.sentTokens.some((t) =>
4571
+ t.token.includes("Pulling up your calendar now."),
4572
+ ),
4573
+ );
4574
+ opts.callbacks?.tool_result?.({
4575
+ toolName: "calendar_list",
4576
+ toolUseId: "c1",
4577
+ resultPreview: "two events",
4578
+ });
4579
+ opts.onTextDelta("You have two events today.");
4580
+ opts.onComplete();
4581
+ return { turnId: "run-escalated", abort: () => {} };
4582
+ });
4583
+
4584
+ await controller.handleCallerUtterance("What is on my calendar?");
4585
+
4586
+ const spoken = spokenText(relay);
4587
+ expect(spoken.indexOf("One moment.")).toBeLessThan(
4588
+ spoken.indexOf("Pulling up your calendar now."),
4589
+ );
4590
+ expect(spoken.indexOf("Pulling up your calendar now.")).toBeLessThan(
4591
+ spoken.indexOf("You have two events today."),
4592
+ );
4593
+ // Narration is audio-only: the turn's recorded text is the model's.
4594
+ expect(assistantSpokePayload(session.id).text).toBe(
4595
+ "One moment. You have two events today.",
4596
+ );
4597
+
4598
+ controller.destroy();
4599
+ });
4600
+
4601
+ test("no narration without a narrator", async () => {
4602
+ const { relay, controller } = setupController(undefined, {
4603
+ progressNarrator: null,
4604
+ progressConfig: {
4605
+ enabled: true,
4606
+ opsThreshold: 1,
4607
+ idleIntervalMs: 60_000,
4608
+ maxSilenceMs: 60_000,
4609
+ longOpMs: 15_000,
4610
+ minGapMs: 10,
4611
+ generationTimeoutMs: 1_500,
4612
+ },
4613
+ });
4614
+ scriptLegs([["Done."]], (opts) => {
4615
+ opts.callbacks?.tool_use_start?.("web_search", { toolUseId: "t1" });
4616
+ opts.callbacks?.tool_result?.({
4617
+ toolName: "web_search",
4618
+ toolUseId: "t1",
4619
+ resultPreview: "ok",
4620
+ });
4621
+ });
4622
+
4623
+ await controller.handleCallerUtterance("Look it up");
4624
+
4625
+ expect(spokenText(relay)).toBe("Done.");
4626
+
4627
+ controller.destroy();
4628
+ });
4629
+
4630
+ test("an outbound call reports the outbound entry", async () => {
4631
+ const { calls } = scriptLegs([["Hello, this is Ava."]]);
4632
+ const { session, controller } = setupController("Book a table for two");
4633
+
4634
+ await controller.startInitialGreeting();
4635
+
4636
+ expect(calls.length).toBe(1);
4637
+ expect(calls[0].voiceTelemetry).toEqual({
4638
+ sessionId: session.id,
4639
+ entry: "phone_outbound",
4640
+ });
4641
+
4642
+ controller.destroy();
4643
+ });
4644
+ });
4645
+
4223
4646
  // ── handleBargeIn ───────────────────────────────────────────────────
4224
4647
 
4225
4648
  describe("handleBargeIn", () => {
@@ -4242,10 +4665,11 @@ describe("call-controller", () => {
4242
4665
  controller.destroy();
4243
4666
  });
4244
4667
 
4245
- test("handleBargeIn returns false and does not abort while still processing (no output yet)", async () => {
4668
+ test("handleBargeIn accepts a barge-in while still processing and aborts the silent turn", async () => {
4246
4669
  // Simulate a turn stuck waiting for the processing lock: no tokens
4247
- // emitted, no completion. The controller must stay in `processing` and
4248
- // NOT flip to `speaking`, so barge-in can't abort a silent turn.
4670
+ // emitted, no completion. The controller stays in `processing`, and a
4671
+ // sustained caller barge-in (the transport's guard has already
4672
+ // vouched for it) cuts the thinking turn off before it speaks.
4249
4673
  mockStartVoiceTurn.mockImplementation(
4250
4674
  async (opts: {
4251
4675
  onTextDelta: (t: string) => void;
@@ -4270,18 +4694,17 @@ describe("call-controller", () => {
4270
4694
 
4271
4695
  const onAccepted = mock(() => {});
4272
4696
  const bargeResult = controller.handleBargeIn(onAccepted);
4273
- expect(bargeResult).toBe(false);
4274
- expect(onAccepted).not.toHaveBeenCalled();
4275
- // Still processing (not aborted), and no interrupt/end-of-turn token sent.
4276
- expect(controller.getState()).toBe("processing");
4697
+ expect(bargeResult).toBe(true);
4698
+ expect(onAccepted).toHaveBeenCalledTimes(1);
4699
+ // The turn is aborted; nothing was spoken, so no end-of-turn token.
4700
+ expect(controller.getState()).toBe("idle");
4277
4701
  const endTokens = relay.sentTokens.filter(
4278
4702
  (t) => t.last === true && t.token === "",
4279
4703
  );
4280
4704
  expect(endTokens.length).toBe(0);
4281
4705
 
4282
- // Cleanup: abort the pending turn
4283
- controller.destroy();
4284
4706
  await turnPromise.catch(() => {});
4707
+ controller.destroy();
4285
4708
  });
4286
4709
 
4287
4710
  test("stays in processing until first token, then flips to speaking and barge-in is accepted", async () => {
@@ -4323,9 +4746,7 @@ describe("call-controller", () => {
4323
4746
  await Promise.resolve();
4324
4747
  }
4325
4748
 
4326
- // Before any token: processing, barge-in ignored (turn not aborted).
4327
- expect(controller.getState()).toBe("processing");
4328
- expect(controller.handleBargeIn()).toBe(false);
4749
+ // Before any token: processing (no audio out yet).
4329
4750
  expect(controller.getState()).toBe("processing");
4330
4751
 
4331
4752
  // Release the first token → controller flips to speaking.
@@ -4343,7 +4764,7 @@ describe("call-controller", () => {
4343
4764
  await turnPromise.catch(() => {});
4344
4765
  });
4345
4766
 
4346
- test("buffering transport (media-stream): streamed tokens do not flip to speaking; barge-in is rejected and the turn still delivers", async () => {
4767
+ test("buffering transport (media-stream): streamed tokens do not flip to speaking; a barge-in still cuts the thinking turn off", async () => {
4347
4768
  // Simulates the media-stream transport: sendTextToken(.., false)
4348
4769
  // only buffers; audio starts later. The controller must stay in
4349
4770
  // `processing` until the transport's audio-start signal fires, so
@@ -4387,17 +4808,17 @@ describe("call-controller", () => {
4387
4808
  expect(controller.getState()).toBe("processing");
4388
4809
  expect(audioStartCallback).not.toBeNull();
4389
4810
 
4390
- // VAD speech-start mid-generation barge-in rejected, turn intact.
4391
- expect(controller.handleBargeIn()).toBe(false);
4392
- expect(controller.getState()).toBe("processing");
4811
+ // Sustained caller speech mid-generation: the buffered, unspoken turn
4812
+ // is cut off like a spoken one would be, and its text is discarded.
4813
+ expect(controller.handleBargeIn()).toBe(true);
4814
+ expect(controller.getState()).toBe("idle");
4393
4815
 
4394
- // The turn completes and delivers its end-of-turn signal.
4395
- releaseTurn();
4396
- await turnPromise;
4816
+ await turnPromise.catch(() => {});
4817
+ // No end-of-turn token: nothing had been spoken.
4397
4818
  const endTokens = relay.sentTokens.filter(
4398
4819
  (t) => t.last === true && t.token === "",
4399
4820
  );
4400
- expect(endTokens.length).toBe(1);
4821
+ expect(endTokens.length).toBe(0);
4401
4822
 
4402
4823
  controller.destroy();
4403
4824
  });