@vellumai/assistant 0.12.1-staging.1 → 0.12.1

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 (404) hide show
  1. package/AGENTS.md +0 -4
  2. package/ARCHITECTURE.md +12 -11
  3. package/docs/architecture/turn-actor.md +8 -15
  4. package/openapi.yaml +1 -291
  5. package/package.json +1 -1
  6. package/src/__tests__/always-loaded-tools-guard.test.ts +10 -18
  7. package/src/__tests__/app-compiler.test.ts +0 -6
  8. package/src/__tests__/app-control-tool-schemas.test.ts +2 -5
  9. package/src/__tests__/app-dir-path-guard.test.ts +3 -1
  10. package/src/__tests__/app-source-watcher.test.ts +218 -0
  11. package/src/{calls/__tests__ → __tests__}/call-controller.test.ts +82 -503
  12. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +2 -5
  13. package/src/__tests__/computer-use-tools.test.ts +5 -17
  14. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  15. package/src/__tests__/config-schema.test.ts +29 -28
  16. package/src/__tests__/config-watcher-skill-reseed.test.ts +359 -0
  17. package/src/__tests__/config-watcher.test.ts +8 -23
  18. package/src/__tests__/connection-model-compat.test.ts +2 -2
  19. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  20. package/src/__tests__/conversation-pairing.test.ts +0 -26
  21. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +10 -4
  22. package/src/__tests__/conversation-queue.test.ts +207 -48
  23. package/src/__tests__/conversation-retry-discard.test.ts +1 -54
  24. package/src/__tests__/conversation-skill-tools.test.ts +0 -27
  25. package/src/__tests__/conversation-surfaces-app-control.test.ts +3 -8
  26. package/src/__tests__/conversation-surfaces-task-progress.test.ts +0 -67
  27. package/src/__tests__/conversation-title-service.test.ts +10 -14
  28. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +5 -3
  29. package/src/__tests__/conversation-tool-setup-attribution.test.ts +0 -51
  30. package/src/__tests__/credential-record-retire.test.ts +1 -171
  31. package/src/__tests__/credential-routes.test.ts +3 -212
  32. package/src/__tests__/credential-security-invariants.test.ts +1 -2
  33. package/src/__tests__/cu-unified-flow.test.ts +2 -8
  34. package/src/__tests__/drain-kick-guard.test.ts +2 -0
  35. package/src/__tests__/drain-requeue-on-contention.test.ts +6 -0
  36. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  37. package/src/__tests__/host-bash-proxy.test.ts +45 -26
  38. package/src/__tests__/host-browser-proxy.test.ts +17 -6
  39. package/src/__tests__/host-file-proxy-targeted.test.ts +0 -23
  40. package/src/__tests__/host-file-proxy.test.ts +23 -11
  41. package/src/__tests__/host-transfer-proxy-targeted.test.ts +9 -30
  42. package/src/__tests__/llm-catalog-parity.test.ts +0 -25
  43. package/src/__tests__/mcp-auth-routes.test.ts +9 -6
  44. package/src/__tests__/mcp-config-secret-boundary.test.ts +136 -78
  45. package/src/__tests__/mcp-health-check.test.ts +11 -8
  46. package/src/__tests__/mcp-list-plugin-servers.test.ts +10 -8
  47. package/src/{calls/__tests__ → __tests__}/media-stream-audio-transcode.test.ts +1 -1
  48. package/src/{calls/__tests__ → __tests__}/media-stream-output.test.ts +8 -37
  49. package/src/{calls/__tests__ → __tests__}/media-stream-parser.test.ts +1 -1
  50. package/src/{calls/__tests__ → __tests__}/media-stream-server-integration.test.ts +57 -150
  51. package/src/{calls/__tests__ → __tests__}/media-stream-stt-session.test.ts +5 -5
  52. package/src/{calls/__tests__ → __tests__}/media-turn-detector.test.ts +1 -1
  53. package/src/__tests__/messages-after-tiebreaker.test.ts +0 -91
  54. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  55. package/src/__tests__/messaging-send-tool.test.ts +548 -355
  56. package/src/__tests__/model-intents.test.ts +0 -3
  57. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  58. package/src/__tests__/platform-client-verify-credential.test.ts +0 -2
  59. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -13
  60. package/src/__tests__/script-proxy-certs.test.ts +0 -35
  61. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  62. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  63. package/src/__tests__/speaker-identification.test.ts +56 -0
  64. package/src/__tests__/subagent-tool-gate-mode.test.ts +3 -19
  65. package/src/__tests__/subagent-tools.test.ts +2 -3
  66. package/src/__tests__/telephony-tts-guard.test.ts +3 -38
  67. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +0 -4
  68. package/src/{calls/__tests__/voice-session-bridge-turn-flow.test.ts → __tests__/voice-session-bridge.test.ts} +12 -20
  69. package/src/__tests__/watch-retro-tool-availability.test.ts +17 -8
  70. package/src/__tests__/workspace-git-service.test.ts +0 -26
  71. package/src/acp/__tests__/agent-process.test.ts +4 -258
  72. package/src/acp/__tests__/auth-required.test.ts +0 -96
  73. package/src/acp/__tests__/client-handler.test.ts +2 -90
  74. package/src/acp/__tests__/session-manager-persistence.test.ts +1 -5
  75. package/src/acp/__tests__/session-manager-resume.test.ts +6 -270
  76. package/src/acp/__tests__/session-manager-usage.test.ts +2 -4
  77. package/src/acp/__tests__/session-manager.test.ts +4 -895
  78. package/src/acp/agent-process.test.ts +1 -4
  79. package/src/acp/agent-process.ts +9 -94
  80. package/src/acp/auth-required.ts +1 -50
  81. package/src/acp/auto-install.test.ts +7 -9
  82. package/src/acp/client-handler.ts +3 -16
  83. package/src/acp/resolve-agent.test.ts +6 -220
  84. package/src/acp/resolve-agent.ts +23 -88
  85. package/src/acp/session-manager.test.ts +3 -196
  86. package/src/acp/session-manager.ts +18 -441
  87. package/src/acp/types.ts +5 -50
  88. package/src/api/events/app-files-changed.ts +3 -4
  89. package/src/api/index.ts +0 -6
  90. package/src/apps/app-store.ts +1 -1
  91. package/src/bundler/app-compiler.ts +0 -15
  92. package/src/calls/__tests__/voice-control-protocol.test.ts +25 -58
  93. package/src/calls/__tests__/voice-session-bridge.test.ts +1 -37
  94. package/src/calls/__tests__/voice-triage-escalate.test.ts +0 -105
  95. package/src/calls/call-constants.ts +4 -24
  96. package/src/calls/call-controller.ts +199 -499
  97. package/src/calls/call-transport.ts +0 -16
  98. package/src/calls/media-stream-output.ts +0 -32
  99. package/src/calls/media-stream-server.ts +30 -82
  100. package/src/calls/media-stream-stt-session.ts +0 -15
  101. package/src/calls/resolve-call-tts-provider.ts +2 -33
  102. package/src/calls/speaker-identification.ts +233 -0
  103. package/src/calls/tts-call-strategy.ts +5 -9
  104. package/src/calls/types.ts +0 -2
  105. package/src/calls/voice-control-protocol.ts +21 -53
  106. package/src/calls/voice-session-bridge.ts +46 -59
  107. package/src/calls/voice-triage-escalate.ts +56 -175
  108. package/src/cli/commands/apps.help.ts +13 -67
  109. package/src/cli/commands/apps.ts +0 -183
  110. package/src/cli/commands/channels/index.help.ts +2 -70
  111. package/src/cli/commands/channels/index.ts +3 -16
  112. package/src/cli/commands/credentials.help.ts +2 -6
  113. package/src/cli/commands/credentials.ts +2 -2
  114. package/src/cli/commands/mcp.help.ts +8 -8
  115. package/src/cli/commands/mcp.ts +1 -1
  116. package/src/cli/commands/roadmap.help.ts +1 -5
  117. package/src/cli/commands/roadmap.ts +1 -1
  118. package/src/cli/lib/bundled-marketplace.json +90 -103
  119. package/src/config/__tests__/default-profile-catalog.test.ts +0 -22
  120. package/src/config/__tests__/memory-retrospective-schema.test.ts +1 -15
  121. package/src/config/__tests__/platform-rehydration.test.ts +0 -93
  122. package/src/config/acp-defaults.test.ts +3 -13
  123. package/src/config/acp-defaults.ts +6 -23
  124. package/src/config/acp-schema.ts +2 -30
  125. package/src/config/bundled-skills/acp/SKILL.md +14 -22
  126. package/src/config/bundled-skills/acp/TOOLS.json +1 -5
  127. package/src/config/bundled-skills/app-builder/SKILL.md +9 -12
  128. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  129. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  130. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +139 -67
  131. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  132. package/src/config/bundled-skills/screen-annotation/SKILL.md +1 -8
  133. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +1 -4
  134. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  135. package/src/config/default-profile-catalog.ts +1 -1
  136. package/src/config/env.ts +2 -2
  137. package/src/config/feature-flag-registry.json +12 -8
  138. package/src/config/memory-v3-gate.test.ts +0 -31
  139. package/src/config/memory-v3-gate.ts +16 -20
  140. package/src/config/platform-rehydration.ts +21 -75
  141. package/src/config/schema.ts +2 -2
  142. package/src/config/schemas/__tests__/live-voice.test.ts +158 -8
  143. package/src/config/schemas/calls.ts +28 -1
  144. package/src/config/schemas/live-voice.ts +173 -1
  145. package/src/config/schemas/mcp.ts +1 -1
  146. package/src/config/schemas/memory-retrospective.ts +0 -10
  147. package/src/config/schemas/skills.ts +14 -0
  148. package/src/credential-execution/process-manager.test.ts +19 -18
  149. package/src/daemon/__tests__/conversation-tool-setup.test.ts +121 -66
  150. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +1 -4
  151. package/src/daemon/app-source-watcher.ts +218 -0
  152. package/src/daemon/config-watcher.ts +194 -13
  153. package/src/daemon/conversation-process.ts +64 -35
  154. package/src/daemon/conversation-skill-tools.ts +31 -13
  155. package/src/daemon/conversation-store.ts +1 -1
  156. package/src/daemon/conversation-surfaces.ts +8 -10
  157. package/src/daemon/conversation-tool-setup.ts +43 -37
  158. package/src/daemon/conversation.ts +3 -0
  159. package/src/daemon/host-bash-proxy.ts +12 -8
  160. package/src/daemon/host-browser-proxy.ts +1 -15
  161. package/src/daemon/host-file-proxy.ts +10 -8
  162. package/src/daemon/host-transfer-proxy.ts +18 -14
  163. package/src/daemon/lifecycle.ts +5 -0
  164. package/src/daemon/mcp-reload-service.ts +5 -5
  165. package/src/daemon/message-types/sync.ts +0 -1
  166. package/src/daemon/providers-setup.ts +1 -1
  167. package/src/daemon/shutdown-handlers.ts +9 -1
  168. package/src/daemon/skill-memory-refresh.ts +0 -64
  169. package/src/daemon/tool-side-effects.ts +18 -7
  170. package/src/hooks/types.ts +0 -33
  171. package/src/inbound/platform-callback-registration.ts +5 -4
  172. package/src/live-voice/__tests__/live-voice-metrics.test.ts +3 -0
  173. package/src/live-voice/__tests__/live-voice-progress.test.ts +15 -15
  174. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  175. package/src/{calls → live-voice}/__tests__/progress-narration.test.ts +4 -4
  176. package/src/{calls → live-voice}/__tests__/progress-phrases.test.ts +1 -1
  177. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +69 -13
  178. package/src/live-voice/live-voice-session.ts +693 -213
  179. package/src/{calls → live-voice}/progress-narration.ts +2 -2
  180. package/src/live-voice/protocol.ts +2 -0
  181. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  182. package/src/mcp/effective-config.ts +3 -3
  183. package/src/mcp/mcp-header-store.ts +45 -29
  184. package/src/mcp/startup.ts +4 -8
  185. package/src/messaging/provider.ts +3 -10
  186. package/src/messaging/providers/callback-routing.ts +0 -18
  187. package/src/messaging/providers/channel-transport.ts +0 -56
  188. package/src/messaging/providers/discord/transport.ts +0 -17
  189. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +17 -6
  190. package/src/messaging/providers/slack/adapter.ts +44 -7
  191. package/src/messaging/providers/slack/transport.ts +0 -16
  192. package/src/messaging/providers/telegram-bot/adapter.ts +48 -1
  193. package/src/messaging/providers/telegram-bot/client.ts +26 -0
  194. package/src/messaging/providers/telegram-bot/transport.ts +0 -20
  195. package/src/messaging/providers/whatsapp/adapter.ts +48 -1
  196. package/src/messaging/providers/whatsapp/client.ts +29 -0
  197. package/src/messaging/providers/whatsapp/transport.ts +0 -18
  198. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +0 -8
  199. package/src/notifications/__tests__/assistant-reply-producer.test.ts +0 -55
  200. package/src/notifications/__tests__/home-feed-side-effect.test.ts +3 -153
  201. package/src/notifications/assistant-reply-producer.ts +3 -8
  202. package/src/notifications/conversation-pairing.ts +16 -36
  203. package/src/notifications/delivered-post-record.ts +1 -11
  204. package/src/notifications/home-feed-side-effect.ts +7 -55
  205. package/src/outbound-proxy/certs.ts +5 -3
  206. package/src/permissions/checker.test.ts +6 -11
  207. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  208. package/src/persistence/conversation-crud.ts +63 -71
  209. package/src/persistence/conversation-lineage.ts +4 -12
  210. package/src/persistence/conversation-title-service.ts +26 -9
  211. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  212. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  213. package/src/persistence/message-reads.ts +0 -30
  214. package/src/persistence/migrations/157-invite-contact-id.ts +6 -18
  215. package/src/persistence/schema/memory-core.ts +0 -5
  216. package/src/platform/client.test.ts +0 -2
  217. package/src/platform/client.ts +10 -2
  218. package/src/plugin-api/constants.ts +0 -2
  219. package/src/plugin-api/index.ts +0 -1
  220. package/src/plugin-api/types.ts +0 -2
  221. package/src/plugins/defaults/index.ts +0 -2
  222. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +0 -46
  223. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +5 -9
  224. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +10 -20
  225. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +0 -219
  226. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +0 -31
  227. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +0 -63
  228. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +0 -4
  229. package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -4
  230. package/src/plugins/defaults/memory/hooks/init.ts +0 -10
  231. package/src/plugins/defaults/memory/hooks/shutdown.ts +0 -2
  232. package/src/plugins/defaults/memory/jobs-worker.ts +0 -13
  233. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +60 -34
  234. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +5 -3
  235. package/src/plugins/defaults/memory/memory-retrospective-job.ts +3 -12
  236. package/src/plugins/defaults/memory/memory-retrospective-state.ts +71 -312
  237. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +1 -2
  238. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +3 -5
  239. package/src/plugins/defaults/memory/startup.ts +1 -6
  240. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +9 -7
  241. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  242. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +0 -89
  243. package/src/plugins/defaults/memory/worker.ts +0 -6
  244. package/src/plugins/mcp-servers.ts +75 -15
  245. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  246. package/src/providers/gemini/client.ts +23 -9
  247. package/src/providers/model-catalog.ts +0 -34
  248. package/src/providers/model-intents.ts +5 -10
  249. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +18 -146
  250. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +1 -83
  251. package/src/providers/openai/chat-completions-provider.ts +78 -192
  252. package/src/providers/openai/codex-models.ts +4 -0
  253. package/src/runtime/AGENTS.md +0 -2
  254. package/src/runtime/auth/same-actor.ts +3 -38
  255. package/src/runtime/http-server.ts +151 -160
  256. package/src/runtime/routes/__tests__/acp-routes.test.ts +4 -159
  257. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +1 -108
  258. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +12 -16
  259. package/src/runtime/routes/acp-routes.test.ts +0 -15
  260. package/src/runtime/routes/acp-routes.ts +9 -72
  261. package/src/runtime/routes/app-management-routes.ts +0 -73
  262. package/src/runtime/routes/app-routes.ts +1 -1
  263. package/src/runtime/routes/conversation-list-routes.ts +3 -14
  264. package/src/runtime/routes/conversation-query-routes.ts +0 -40
  265. package/src/runtime/routes/credential-routes.ts +10 -19
  266. package/src/runtime/routes/diagnostics-routes.ts +3 -55
  267. package/src/runtime/routes/index.ts +0 -4
  268. package/src/runtime/routes/log-export-routes.ts +0 -2
  269. package/src/runtime/routes/mcp-auth-routes.ts +47 -39
  270. package/src/runtime/routes/migration-routes.ts +21 -8
  271. package/src/runtime/routes/platform-routes.ts +15 -18
  272. package/src/runtime/services/__tests__/conversation-serializer.test.ts +0 -28
  273. package/src/runtime/services/conversation-serializer.ts +3 -16
  274. package/src/runtime/sync/resource-sync-events.ts +1 -2
  275. package/src/schedule/worker.ts +1 -1
  276. package/src/security/ces-rpc-record-backend.ts +0 -7
  277. package/src/subagent/consult-context.ts +25 -12
  278. package/src/tools/acp/list-agents.test.ts +1 -3
  279. package/src/tools/acp/spawn.test.ts +11 -146
  280. package/src/tools/acp/spawn.ts +3 -21
  281. package/src/tools/acp/status.ts +3 -40
  282. package/src/tools/app-control/skill-proxy-bridge.ts +1 -2
  283. package/src/tools/capability-offer.test.ts +2 -22
  284. package/src/tools/capability-offer.ts +1 -16
  285. package/src/tools/computer-use/definitions.ts +2 -4
  286. package/src/tools/computer-use/skill-proxy-bridge.ts +1 -2
  287. package/src/tools/credentials/metadata-store.ts +0 -100
  288. package/src/tools/host-filesystem/edit.test.ts +1 -2
  289. package/src/tools/host-filesystem/edit.ts +2 -2
  290. package/src/tools/host-filesystem/read.test.ts +1 -2
  291. package/src/tools/host-filesystem/read.ts +2 -2
  292. package/src/tools/host-filesystem/transfer.test.ts +1 -2
  293. package/src/tools/host-filesystem/transfer.ts +2 -2
  294. package/src/tools/host-filesystem/write.test.ts +1 -2
  295. package/src/tools/host-filesystem/write.ts +2 -2
  296. package/src/tools/host-terminal/host-shell.test.ts +0 -16
  297. package/src/tools/host-terminal/host-shell.ts +20 -4
  298. package/src/tools/policy-context.ts +7 -6
  299. package/src/tools/subagent/spawn.ts +8 -5
  300. package/src/tools/watch/watch-retro-report.ts +8 -0
  301. package/src/util/__tests__/language-subtag.test.ts +1 -23
  302. package/src/util/fs-watcher-error.ts +36 -0
  303. package/src/util/language-subtag.ts +0 -16
  304. package/src/util/platform.ts +0 -8
  305. package/src/workspace/git-service.ts +54 -5
  306. package/src/workspace/migrations/registry.ts +0 -6
  307. package/scripts/smoke-desktop-window-theme.ts +0 -186
  308. package/src/__tests__/git-porcelain.test.ts +0 -47
  309. package/src/__tests__/helpers/set-workspace-mcp.ts +0 -11
  310. package/src/__tests__/message-deleted-hook-dispatch.test.ts +0 -119
  311. package/src/__tests__/resolve-app.test.ts +0 -135
  312. package/src/__tests__/source-fingerprint.test.ts +0 -184
  313. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +0 -336
  314. package/src/acp/__tests__/helpers/acp-model-option.ts +0 -51
  315. package/src/acp/model-config.test.ts +0 -166
  316. package/src/acp/model-config.ts +0 -138
  317. package/src/api/events/acp-session-model-update.test.ts +0 -88
  318. package/src/api/events/acp-session-model-update.ts +0 -46
  319. package/src/apps/resolve-app.ts +0 -81
  320. package/src/apps/source-fingerprint.ts +0 -266
  321. package/src/calls/__tests__/barge-in-guard.test.ts +0 -81
  322. package/src/calls/__tests__/voice-leg-coordinator.test.ts +0 -229
  323. package/src/calls/__tests__/voice-progress-cadence.test.ts +0 -274
  324. package/src/calls/barge-in-guard.ts +0 -118
  325. package/src/calls/voice-leg-coordinator.ts +0 -189
  326. package/src/calls/voice-progress-cadence.ts +0 -423
  327. package/src/cli/commands/__tests__/apps.test.ts +0 -245
  328. package/src/cli/commands/__tests__/roadmap.help.test.ts +0 -19
  329. package/src/cli/commands/channels/__tests__/send.test.ts +0 -218
  330. package/src/cli/commands/channels/send.ts +0 -128
  331. package/src/config/__tests__/platform-identity.test.ts +0 -117
  332. package/src/config/acp-agent-write.ts +0 -43
  333. package/src/config/acp-schema.test.ts +0 -81
  334. package/src/config/platform-identity.ts +0 -144
  335. package/src/config/schemas/__tests__/voice.test.ts +0 -148
  336. package/src/config/schemas/voice.ts +0 -150
  337. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +0 -133
  338. package/src/daemon/app-change-notify.ts +0 -45
  339. package/src/desktop/__tests__/fake-desktop.ts +0 -119
  340. package/src/desktop/desktop-chrome-policy.test.ts +0 -71
  341. package/src/desktop/desktop-chrome-policy.ts +0 -42
  342. package/src/desktop/desktop-dependencies.test.ts +0 -102
  343. package/src/desktop/desktop-dependencies.ts +0 -306
  344. package/src/desktop/desktop-feature.test.ts +0 -18
  345. package/src/desktop/desktop-feature.ts +0 -19
  346. package/src/desktop/desktop-panel-config.test.ts +0 -135
  347. package/src/desktop/desktop-panel-config.ts +0 -149
  348. package/src/desktop/desktop-session-manager.test.ts +0 -627
  349. package/src/desktop/desktop-session-manager.ts +0 -814
  350. package/src/desktop/desktop-stream-bridge.test.ts +0 -395
  351. package/src/desktop/desktop-stream-bridge.ts +0 -214
  352. package/src/desktop/desktop-wallpaper.test.ts +0 -191
  353. package/src/desktop/desktop-wallpaper.ts +0 -113
  354. package/src/desktop/desktop-window-theme.ts +0 -170
  355. package/src/i18n/AGENTS.md +0 -22
  356. package/src/i18n/__tests__/i18n.test.ts +0 -108
  357. package/src/i18n/index.ts +0 -16
  358. package/src/i18n/locales.ts +0 -93
  359. package/src/i18n/messages.ts +0 -61
  360. package/src/i18n/title.ts +0 -25
  361. package/src/mcp/__tests__/workspace-mcp-config.test.ts +0 -135
  362. package/src/mcp/spec-schema.ts +0 -111
  363. package/src/mcp/workspace-mcp-config.ts +0 -188
  364. package/src/messaging/providers/__tests__/proactive-address.test.ts +0 -95
  365. package/src/persistence/conversation-title-placeholders.ts +0 -23
  366. package/src/persistence/message-cursor.ts +0 -78
  367. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +0 -149
  368. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +0 -118
  369. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +0 -83
  370. package/src/plugins/defaults/memory/hooks/message-deleted.ts +0 -21
  371. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +0 -126
  372. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +0 -79
  373. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +0 -28
  374. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +0 -9
  375. package/src/providers/gemini-thought-signature.test.ts +0 -63
  376. package/src/providers/gemini-thought-signature.ts +0 -44
  377. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +0 -613
  378. package/src/providers/openai/__tests__/google-thought-signature.test.ts +0 -235
  379. package/src/providers/openai/google-thought-signature.ts +0 -320
  380. package/src/runtime/__tests__/channel-send.test.ts +0 -443
  381. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +0 -89
  382. package/src/runtime/__tests__/live-voice-upgrade.test.ts +0 -58
  383. package/src/runtime/__tests__/runtime-ws-test-utils.ts +0 -72
  384. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +0 -59
  385. package/src/runtime/channel-send.ts +0 -316
  386. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +0 -205
  387. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +0 -127
  388. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +0 -107
  389. package/src/runtime/routes/channel-send-routes.ts +0 -85
  390. package/src/runtime/routes/desktop-setup-routes.test.ts +0 -74
  391. package/src/runtime/routes/desktop-setup-routes.ts +0 -35
  392. package/src/security/__tests__/ces-http-record-backend.test.ts +0 -137
  393. package/src/security/ces-http-record-backend.ts +0 -120
  394. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +0 -60
  395. package/src/skills/workspace-skill-md-signature.ts +0 -50
  396. package/src/tools/acp/status.test.ts +0 -94
  397. package/src/util/ca-bundle.ts +0 -13
  398. package/src/workspace/git-porcelain.ts +0 -66
  399. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +0 -258
  400. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +0 -78
  401. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +0 -214
  402. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +0 -102
  403. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +0 -199
  404. /package/src/{calls → live-voice}/progress-phrases.ts +0 -0
@@ -17,8 +17,6 @@ import {
17
17
  getRequestByPendingQuestionOrNull,
18
18
  listGuardianRequestDeliveriesOrEmpty,
19
19
  } from "../channels/gateway-guardian-requests.js";
20
- import { loadConfig } from "../config/loader.js";
21
- import type { VoiceProgressConfig } from "../config/schemas/voice.js";
22
20
  import type { TrustContext } from "../daemon/trust-context-types.js";
23
21
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
24
22
  import { computeToolApprovalDigest } from "../security/tool-approval-digest.js";
@@ -34,14 +32,11 @@ import type { TtsProvider, TtsProviderId } from "../tts/types.js";
34
32
  import { getLogger } from "../util/logger.js";
35
33
  import type { CallAudioFormat } from "./audio-store.js";
36
34
  import {
37
- BARGE_IN_TEARDOWN_WAIT_MS,
38
- DURATION_WARNING_LEAD_MS,
39
35
  getEndCallDrainMaxWaitMs,
40
36
  getEndCallListenWindowMs,
41
37
  getMaxCallDurationMs,
42
38
  getSilenceTimeoutMs,
43
39
  getUserConsultationTimeoutMs,
44
- POST_GOODBYE_HANGUP_DELAY_MS,
45
40
  } from "./call-constants.js";
46
41
  import {
47
42
  formatDuration,
@@ -65,15 +60,12 @@ import type { CallTransport } from "./call-transport.js";
65
60
  import { finalizeCall } from "./finalize-call.js";
66
61
  import { sendGuardianExpiryNotices } from "./guardian-action-sweep.js";
67
62
  import { dispatchGuardianQuestion } from "./guardian-dispatch.js";
68
- import {
69
- createVoiceProgressNarrator,
70
- type VoiceProgressNarrator,
71
- } from "./progress-narration.js";
72
63
  import {
73
64
  findPlayableTelephonyTtsFallbackProvider,
74
65
  resolveCallTtsProvider,
75
66
  resolveSynthesisFormats,
76
67
  } from "./resolve-call-tts-provider.js";
68
+ import type { PromptSpeakerContext } from "./speaker-identification.js";
77
69
  import {
78
70
  resolveTelephonyLanguageVoice,
79
71
  resolveTelephonySynthesisLanguage,
@@ -84,24 +76,16 @@ import {
84
76
  CALL_OPENING_ACK_MARKER,
85
77
  CALL_OPENING_MARKER,
86
78
  CALL_VERIFICATION_COMPLETE_MARKER,
87
- createControlMarkerHoldback,
79
+ couldBeControlMarker,
88
80
  END_CALL_MARKER,
89
81
  extractBalancedJson,
90
82
  stripInternalSpeechMarkers,
91
83
  } from "./voice-control-protocol.js";
92
- import {
93
- createFrontDoorLegCoordinator,
94
- type FrontDoorLegCoordinator,
95
- type SpokenEscalationBridge,
96
- } from "./voice-leg-coordinator.js";
97
- import { createProgressCadence } from "./voice-progress-cadence.js";
98
84
  import {
99
85
  CONVERSATION_BUSY_MESSAGE,
100
86
  startVoiceTurn,
101
- type VoiceTurnCallbacks,
102
87
  type VoiceTurnHandle,
103
88
  } from "./voice-session-bridge.js";
104
- import { type VoiceRoutingLeg } from "./voice-triage-escalate.js";
105
89
 
106
90
  const log = getLogger("call-controller");
107
91
 
@@ -133,32 +117,6 @@ interface PendingEndCall {
133
117
  wake: (() => void) | null;
134
118
  }
135
119
 
136
- /**
137
- * What one voice turn produced, across both of its legs when it escalated.
138
- * Feeds post-turn marker detection and the call event log.
139
- */
140
- interface VoiceTurnOutcome {
141
- /**
142
- * The filtered response text: what the caller heard, plus the control
143
- * markers the model emitted for the controller to act on.
144
- */
145
- text: string;
146
- /** Persisted row of the caller's utterance, when the bridge reported it. */
147
- userMessageId: string | null;
148
- /**
149
- * Persisted row of the leg that answered: the escalated leg's when the
150
- * turn escalated, else the front-door leg's own.
151
- */
152
- assistantMessageId: string | null;
153
- }
154
-
155
- /** One leg of a triaged phone turn, as handed to the session bridge. */
156
- interface PhoneVoiceLeg {
157
- content: string;
158
- routingLeg: VoiceRoutingLeg;
159
- spokenEscalationBridge?: string;
160
- }
161
-
162
120
  export class CallController {
163
121
  private callSessionId: string;
164
122
  private transport: CallTransport;
@@ -243,11 +201,6 @@ export class CallController {
243
201
  * resolution only.
244
202
  */
245
203
  private resolveSynthesisLanguage: () => string | undefined;
246
- /** Spoken progress narration (voice.frontModel.progress), null when off. */
247
- private readonly progressNarrator: VoiceProgressNarrator | null;
248
- private readonly progressConfig: VoiceProgressConfig;
249
- /** Rotates the static narration fallback across turns. */
250
- private progressPhraseCounter = 0;
251
204
 
252
205
  constructor(
253
206
  callSessionId: string,
@@ -258,9 +211,6 @@ export class CallController {
258
211
  assistantId?: string;
259
212
  trustContext?: TrustContext;
260
213
  resolveSynthesisLanguage?: () => string | undefined;
261
- /** Test seam: an explicit null disables narration for the call. */
262
- progressNarrator?: VoiceProgressNarrator | null;
263
- progressConfig?: VoiceProgressConfig;
264
214
  },
265
215
  ) {
266
216
  this.callSessionId = callSessionId;
@@ -273,12 +223,6 @@ export class CallController {
273
223
  this.resolveSynthesisLanguage =
274
224
  opts?.resolveSynthesisLanguage ??
275
225
  (() => resolveTelephonySynthesisLanguage());
276
- this.progressConfig =
277
- opts?.progressConfig ?? loadConfig().voice.frontModel.progress;
278
- this.progressNarrator =
279
- opts?.progressNarrator !== undefined
280
- ? opts.progressNarrator
281
- : createVoiceProgressNarrator({ config: this.progressConfig });
282
226
 
283
227
  // Resolve the conversation ID and skipDisclosure from the call session
284
228
  const session = getCallSession(callSessionId);
@@ -369,7 +313,10 @@ export class CallController {
369
313
  * Caller utterances always trigger normal turns, even when a guardian
370
314
  * consultation is pending — the consultation is tracked separately.
371
315
  */
372
- async handleCallerUtterance(transcript: string): Promise<void> {
316
+ async handleCallerUtterance(
317
+ transcript: string,
318
+ speaker?: PromptSpeakerContext,
319
+ ): Promise<void> {
373
320
  // If the caller speaks while an END_CALL teardown is pending (during the
374
321
  // drain wait or the listen window), this is a deferral — the caller is
375
322
  // re-engaging after we tried to hang up. Track it so we can cap repeats.
@@ -396,15 +343,13 @@ export class CallController {
396
343
  this.currentTurnPromise = null;
397
344
  await Promise.race([
398
345
  teardownPromise.catch(() => {}),
399
- new Promise<void>((resolve) =>
400
- setTimeout(resolve, BARGE_IN_TEARDOWN_WAIT_MS),
401
- ),
346
+ new Promise<void>((resolve) => setTimeout(resolve, 2000)),
402
347
  ]);
403
348
  }
404
349
 
405
350
  this.state = "processing";
406
351
  this.resetSilenceTimer();
407
- const callerContent = transcript;
352
+ const callerContent = this.formatCallerUtterance(transcript, speaker);
408
353
  const shouldMarkOpeningAck = this.awaitingOpeningAck;
409
354
  if (shouldMarkOpeningAck) {
410
355
  this.awaitingOpeningAck = false;
@@ -488,35 +433,37 @@ export class CallController {
488
433
  }
489
434
 
490
435
  /**
491
- * Handle a barge-in from sustained inbound caller speech.
436
+ * Handle a barge-in attempt from inbound caller audio.
492
437
  *
493
- * Interrupts the in-flight turn whether the assistant is audibly speaking
494
- * or still thinking (processing, no audio yet), so a caller can cut in
495
- * before the assistant starts talking. With no turn in flight the
496
- * barge-in is ignored. The transport's sustained-speech guard is what
497
- * keeps a stray frame from cancelling a still-starting turn; the state
498
- * gate only says whether there is anything to cancel.
438
+ * Only interrupts the in-flight turn when the assistant is actively
439
+ * speaking. When the controller is idle or still processing (no TTS
440
+ * output yet), the barge-in is ignored — this prevents false
441
+ * interruption on initial inbound media frames that arrive before
442
+ * the assistant has had a chance to produce its first response.
499
443
  *
500
- * @param onAccepted Invoked synchronously after the gate passes but
501
- * before {@link handleInterrupt} runs. Transports use this to flush
502
- * queued outbound audio without wiping the end-of-turn mark that
503
- * handleInterrupt enqueues, and without flushing at all when the
504
- * barge-in is ignored.
505
- * @returns `true` if the barge-in was accepted (a turn was in flight),
506
- * `false` if it was ignored (assistant idle).
444
+ * @param onAccepted Invoked synchronously after the speaking gate
445
+ * passes but before {@link handleInterrupt} runs. Transports use this
446
+ * to flush queued outbound audio without wiping the end-of-turn mark
447
+ * that handleInterrupt enqueues — and without flushing at all when
448
+ * the barge-in is ignored.
449
+ * @returns `true` if the barge-in was accepted (assistant was speaking),
450
+ * `false` if it was ignored (assistant idle or processing).
507
451
  */
508
452
  handleBargeIn(onAccepted?: () => void): boolean {
509
- if (this.state === "idle") {
453
+ if (this.state !== "speaking") {
510
454
  log.debug(
511
- { callSessionId: this.callSessionId },
512
- "Barge-in ignored: no turn in flight",
455
+ {
456
+ callSessionId: this.callSessionId,
457
+ state: this.state,
458
+ },
459
+ "Barge-in ignored — assistant not speaking",
513
460
  );
514
461
  return false;
515
462
  }
516
463
 
517
464
  log.info(
518
- { callSessionId: this.callSessionId, state: this.state },
519
- "Barge-in accepted: interrupting the assistant's turn",
465
+ { callSessionId: this.callSessionId },
466
+ "Barge-in accepted — interrupting assistant speech",
520
467
  );
521
468
  onAccepted?.();
522
469
  this.handleInterrupt();
@@ -524,12 +471,11 @@ export class CallController {
524
471
  }
525
472
 
526
473
  /**
527
- * Handle caller interrupting the assistant's turn.
474
+ * Handle caller interrupting the assistant's speech.
528
475
  *
529
476
  * This is the hard interrupt path used for explicit teardown and
530
477
  * internal abort scenarios. For barge-in from inbound audio, prefer
531
- * {@link handleBargeIn}, which ignores an idle controller and relies on
532
- * the transport's sustained-speech guard to vouch for the interruption.
478
+ * {@link handleBargeIn} which gates on the speaking state.
533
479
  */
534
480
  handleInterrupt(): void {
535
481
  const wasSpeaking = this.state === "speaking";
@@ -639,6 +585,22 @@ export class CallController {
639
585
  }
640
586
  }
641
587
 
588
+ private formatCallerUtterance(
589
+ transcript: string,
590
+ speaker?: PromptSpeakerContext,
591
+ ): string {
592
+ if (!speaker) {
593
+ return transcript;
594
+ }
595
+ const safeId = speaker.speakerId.replaceAll('"', "'");
596
+ const safeLabel = speaker.speakerLabel.replaceAll('"', "'");
597
+ const confidencePart =
598
+ speaker.speakerConfidence != null
599
+ ? ` confidence="${speaker.speakerConfidence.toFixed(2)}"`
600
+ : "";
601
+ return `[SPEAKER id="${safeId}" label="${safeLabel}" source="${speaker.source}"${confidencePart}] ${transcript}`;
602
+ }
603
+
642
604
  /**
643
605
  * Execute a single voice turn through the conversation pipeline and stream
644
606
  * the response back through the relay.
@@ -655,9 +617,6 @@ export class CallController {
655
617
  }
656
618
  const runVersion = ++this.llmRunVersion;
657
619
  const runSignal = this.abortController.signal;
658
- // Stamped before any pre-bridge work so the bridge's dispatch-timing log
659
- // attributes the whole turn, TTS provider resolution included.
660
- const launchedAtMs = Date.now();
661
620
 
662
621
  // Clear silence timer while actively processing. The caller said
663
622
  // something (or a turn was triggered), so silence detection should
@@ -670,21 +629,19 @@ export class CallController {
670
629
  try {
671
630
  // Stay in `processing` through the lock-wait and LLM generation; flip to
672
631
  // `speaking` only when real outbound audio/tokens start (see
673
- // beginSpeaking). The transport reads the state to know whether audio
674
- // is audible; either phase is interruptible by sustained caller speech.
632
+ // beginSpeaking). This keeps barge-in from aborting a silent turn.
675
633
  this.state = "processing";
676
634
 
677
- const outcome = await this.streamTtsTokens(
635
+ const fullResponseText = await this.streamTtsTokens(
678
636
  content,
679
637
  runVersion,
680
638
  runSignal,
681
- launchedAtMs,
682
639
  );
683
640
  if (!this.isCurrentRun(runVersion)) {
684
641
  return;
685
642
  }
686
643
 
687
- await this.handleTurnCompletion(outcome);
644
+ await this.handleTurnCompletion(fullResponseText);
688
645
  } catch (err: unknown) {
689
646
  this.currentTurnHandle = null;
690
647
  // Aborted requests are expected (interruptions, rapid utterances)
@@ -743,25 +700,15 @@ export class CallController {
743
700
  }
744
701
 
745
702
  /**
746
- * Run one turn through the conversation pipeline and stream its speech to
747
- * the transport, buffering to strip control markers before they reach the
748
- * relay.
749
- *
750
- * Every turn opens on the front-door leg: a fast, toolless model that
751
- * either answers outright or hands off with a spoken holding phrase to an
752
- * escalated leg on the conversation's own model. Both legs stream through
753
- * the same TTS machinery and share this turn's single end-of-turn signal,
754
- * so the caller never gets a listening window between the bridge and the
755
- * answer. Phone turns commit on the STT provider's utterance-boundary
756
- * final and carry no partial transcript, so the front-door leg never
757
- * holds: routing is escalate-only.
703
+ * Stream TTS tokens from the conversation pipeline, buffering to strip
704
+ * control markers before they reach the relay. Returns the full
705
+ * accumulated response text for post-turn marker detection.
758
706
  */
759
707
  private async streamTtsTokens(
760
708
  content: string,
761
709
  runVersion: number,
762
710
  runSignal: AbortSignal,
763
- launchedAtMs: number,
764
- ): Promise<VoiceTurnOutcome> {
711
+ ): Promise<string> {
765
712
  // Resolve the active TTS provider through the global abstraction.
766
713
  // The catalog's callMode determines the call path: synthesized-play
767
714
  // providers synthesize each speakable segment via the provider API as
@@ -780,18 +727,14 @@ export class CallController {
780
727
  // Buffer incoming tokens so we can strip control markers ([ASK_GUARDIAN:...], [END_CALL])
781
728
  // before they reach TTS. We hold text whenever an unmatched '[' appears, since it
782
729
  // could be the start of a control marker.
730
+ let ttsBuffer = "";
783
731
  let fullResponseText = "";
784
- const outcome: VoiceTurnOutcome = {
785
- text: "",
786
- userMessageId: null,
787
- assistantMessageId: null,
788
- };
789
- // Audio bookkeeping for progress narration: segments handed to synthesis
790
- // but not yet emitted, text deltas seen (a narration generated while the
791
- // model was already speaking is stale), and whether the turn is over.
792
- let segmentsInFlight = 0;
793
- let deltaEpoch = 0;
794
- let turnSettled = false;
732
+ // Reasoning models can inline <think> spans in the content stream when a
733
+ // profile has not opted into parseThinkTags. Neither the spoken path nor
734
+ // the post-turn consumers of fullResponseText (transcripts,
735
+ // assistant_spoke, END_CALL/ASK_GUARDIAN detection) may see them: both
736
+ // are fed only filtered text.
737
+ const reasoningFilter = createReasoningTagFilter();
795
738
 
796
739
  // Synthesized path: text is split at speakable boundaries as it streams
797
740
  // and each segment is synthesized while the LLM keeps generating. The
@@ -827,7 +770,6 @@ export class CallController {
827
770
  const speakSegmentViaPcmFallback = async (
828
771
  failedProviderId: string,
829
772
  segment: string,
830
- language: string | undefined,
831
773
  ): Promise<void> => {
832
774
  if (pcmFallbackProvider === undefined) {
833
775
  pcmFallbackProvider =
@@ -859,7 +801,6 @@ export class CallController {
859
801
  segment,
860
802
  runVersion,
861
803
  audioFormat,
862
- language,
863
804
  );
864
805
  if (fallbackStatus !== "ok") {
865
806
  // The fallback provider is failing too — stop retrying.
@@ -867,12 +808,9 @@ export class CallController {
867
808
  }
868
809
  };
869
810
 
870
- // `language` overrides the turn's synthesis language for a fixed phrase
871
- // whose text is not in the caller's language; undefined rides the turn's.
872
811
  const enqueueSynthesisSegments = (
873
812
  ttsProvider: TtsProvider,
874
813
  segments: string[],
875
- language?: string,
876
814
  ): void => {
877
815
  for (const rawSegment of segments) {
878
816
  // Sanitized per segment (not per delta) so markdown spanning deltas
@@ -882,23 +820,21 @@ export class CallController {
882
820
  continue;
883
821
  }
884
822
  firstSynthSegmentEnqueued = true;
885
- segmentsInFlight += 1;
886
823
  synthesisChain = synthesisChain.then(async () => {
824
+ if (
825
+ !this.isCurrentRun(runVersion) ||
826
+ synthesisFailure ||
827
+ synthesisCancelled
828
+ ) {
829
+ return;
830
+ }
887
831
  try {
888
- if (
889
- !this.isCurrentRun(runVersion) ||
890
- synthesisFailure ||
891
- synthesisCancelled
892
- ) {
893
- return;
894
- }
895
832
  if (!synthesisFellBack) {
896
833
  const status = await this.synthesizeAndStreamAudio(
897
834
  ttsProvider,
898
835
  segment,
899
836
  runVersion,
900
837
  audioFormat,
901
- language,
902
838
  );
903
839
  if (status === "ok") {
904
840
  return;
@@ -924,14 +860,9 @@ export class CallController {
924
860
  this.transport.sendTextToken(`${segment} `, false);
925
861
  return;
926
862
  }
927
- await speakSegmentViaPcmFallback(ttsProvider.id, segment, language);
863
+ await speakSegmentViaPcmFallback(ttsProvider.id, segment);
928
864
  } catch (err) {
929
865
  synthesisFailure = { err };
930
- } finally {
931
- segmentsInFlight -= 1;
932
- // The segment's audio has been handed off; the transport's
933
- // playback tail covers what is still playing.
934
- cadence.noteAudioSettled();
935
866
  }
936
867
  });
937
868
  }
@@ -957,367 +888,150 @@ export class CallController {
957
888
  }
958
889
  this.beginSpeakingOnAudioStart(runVersion);
959
890
  this.transport.sendTextToken(cleaned, false);
960
- cadence.noteAudioSettled();
961
891
  }
962
892
  };
963
893
 
964
- /**
965
- * Speak a fixed phrase from a localized table (already one complete
966
- * sentence, no markers) outside the model-text stream. `language` is the
967
- * hint the phrase must carry: "en" when the table lacked the caller's
968
- * language, so the English text is not rendered under a ko/ar/ta hint
969
- * (on the native route that is the transport's system-copy contract);
970
- * undefined rides the turn's language like model text.
971
- */
972
- const speakFixedPhrase = (
973
- text: string,
974
- language: string | undefined,
975
- ): boolean => {
976
- const cleaned = sanitizeForTts(text).trim();
977
- if (cleaned.length === 0) {
978
- return false;
979
- }
980
- if (synthProvider) {
981
- enqueueSynthesisSegments(synthProvider, [cleaned], language);
982
- return true;
983
- }
984
- this.beginSpeakingOnAudioStart(runVersion);
985
- this.transport.sendTextToken(`${cleaned} `, false, {
986
- systemCopy: language !== undefined,
987
- });
988
- cadence.noteAudioSettled();
989
- return true;
990
- };
991
-
992
- // Spoken progress narration for the turn's dead air (tool loops on the
993
- // escalated leg). The shared cadence owns the timing; this host supplies
994
- // the phone's view of audible silence: nothing buffered toward a
995
- // sentence, no segment mid-synthesis, and the transport's queue empty
996
- // with its playback-tail estimate expired.
997
- const callerTranscript =
998
- content === CALL_OPENING_MARKER ||
999
- content === CALL_VERIFICATION_COMPLETE_MARKER
1000
- ? ""
1001
- : content;
1002
- const cadence = createProgressCadence({
1003
- config: this.progressConfig,
1004
- narrator: this.progressNarrator,
1005
- nextFallbackPhraseIndex: () => this.progressPhraseCounter++,
1006
- host: {
1007
- turnId: `${this.callSessionId}#${runVersion}`,
1008
- launchedAtMs,
1009
- signal: runSignal,
1010
- canNarrate: () => this.isCurrentRun(runVersion) && !turnSettled,
1011
- isAudioIdle: () =>
1012
- pendingSynthText.length === 0 &&
1013
- segmentsInFlight === 0 &&
1014
- (this.transport.isPlaybackIdle?.() ?? true),
1015
- playbackTailUntilMs: () => this.transport.playbackTailUntilMs?.() ?? 0,
1016
- transcriptSoFar: () => callerTranscript,
1017
- language: () => this.resolveSynthesisLanguage(),
1018
- deltaEpoch: () => deltaEpoch,
1019
- speak: speakFixedPhrase,
1020
- },
1021
- });
1022
- const settleTurn = (): void => {
1023
- turnSettled = true;
1024
- cadence.clear();
1025
- };
1026
- runSignal.addEventListener("abort", settleTurn, { once: true });
1027
-
1028
- // Speech goes out through the shared control-marker holdback: text up to
1029
- // a possibly-streaming marker flushes, the marker itself is stripped, and
1030
- // ordinary bracketed text ("[A]", "[note]") never stalls TTS. Marker
1031
- // detection for actions (END_CALL, ASK_GUARDIAN) reads fullResponseText
1032
- // separately, after the turn.
1033
- const flushSafeText = createControlMarkerHoldback((chunk) => {
894
+ const flushSafeText = (): void => {
1034
895
  if (!this.isCurrentRun(runVersion)) {
1035
896
  return;
1036
897
  }
1037
- emitSafeChunk(chunk);
1038
- });
1039
-
1040
- // Transcript row ids reported by the bridge. The escalated leg persists
1041
- // a hidden continuation prompt rather than the caller's words, so only
1042
- // the front-door leg's user row is the utterance; and a front-door leg
1043
- // that handed off owns the bridge row, not the answer, so its assistant
1044
- // row is not the turn's.
1045
- const legCallbacks = (
1046
- routingLeg: VoiceRoutingLeg,
1047
- handedOff: () => boolean,
1048
- ): VoiceTurnCallbacks => ({
1049
- persisted_user_message_id: (messageId) => {
1050
- if (!this.isCurrentRun(runVersion) || routingLeg === "escalated") {
1051
- return;
1052
- }
1053
- outcome.userMessageId = messageId;
1054
- },
1055
- persisted_assistant_message_id: (messageId) => {
1056
- if (!this.isCurrentRun(runVersion) || handedOff()) {
1057
- return;
898
+ if (ttsBuffer.length === 0) {
899
+ return;
900
+ }
901
+ const bracketIdx = ttsBuffer.indexOf("[");
902
+ if (bracketIdx === -1) {
903
+ // No bracket at all — safe to flush everything
904
+ emitSafeChunk(ttsBuffer);
905
+ ttsBuffer = "";
906
+ } else {
907
+ // Flush everything before the bracket
908
+ if (bracketIdx > 0) {
909
+ emitSafeChunk(ttsBuffer.slice(0, bracketIdx));
910
+ ttsBuffer = ttsBuffer.slice(bracketIdx);
1058
911
  }
1059
- outcome.assistantMessageId = messageId;
1060
- },
1061
- // Tool activity lands in the call event log, the call's own record of
1062
- // what happened on the line: the call summary and status surfaces
1063
- // read it, and it is what the caller sat in silence through.
1064
- tool_use_start: (toolName, detail) => {
1065
- if (!this.isCurrentRun(runVersion)) {
1066
- return;
912
+
913
+ // Only hold the buffer if the bracket text could be the start of a
914
+ // known control marker. Otherwise flush immediately so ordinary
915
+ // bracketed text (e.g. "[A]", "[note]") doesn't stall TTS.
916
+ const afterBracket = ttsBuffer;
917
+ const couldBeControl = couldBeControlMarker(afterBracket);
918
+
919
+ if (!couldBeControl) {
920
+ // Not a control marker prefix — flush up to the next '[' (if any)
921
+ const nextBracket = ttsBuffer.indexOf("[", 1);
922
+ if (nextBracket === -1) {
923
+ emitSafeChunk(ttsBuffer);
924
+ ttsBuffer = "";
925
+ } else {
926
+ emitSafeChunk(ttsBuffer.slice(0, nextBracket));
927
+ ttsBuffer = ttsBuffer.slice(nextBracket);
928
+ }
1067
929
  }
1068
- recordCallEvent(this.callSessionId, "tool_use_started", {
1069
- toolName,
1070
- ...(detail?.toolUseId !== undefined
1071
- ? { toolUseId: detail.toolUseId }
1072
- : {}),
1073
- });
1074
- cadence.toolStarted(toolName, detail?.toolUseId);
1075
- },
1076
- tool_result: (event) => {
930
+ // Otherwise hold it — might be a control marker still being streamed
931
+ }
932
+ };
933
+
934
+ // Use a promise to track completion of the voice turn
935
+ const turnComplete = new Promise<void>((resolve, reject) => {
936
+ const onTextDelta = (text: string): void => {
1077
937
  if (!this.isCurrentRun(runVersion)) {
1078
938
  return;
1079
939
  }
1080
- recordCallEvent(this.callSessionId, "tool_use_completed", {
1081
- toolName: event.toolName,
1082
- ...(event.toolUseId !== undefined
1083
- ? { toolUseId: event.toolUseId }
1084
- : {}),
1085
- ...(event.isError !== undefined ? { isError: event.isError } : {}),
1086
- resultPreview: event.resultPreview,
1087
- });
1088
- cadence.toolFinished(event);
1089
- },
1090
- });
1091
-
1092
- // Speak the escalation bridge so the strong-model call has no dead air.
1093
- // The model's own holding phrase is real assistant speech and stays in
1094
- // the turn's text, matching the row the bridge's transcript hygiene
1095
- // keeps for it; the canned fallback is audio-only, matching the row it
1096
- // deletes.
1097
- const speakEscalationBridge = (bridge: SpokenEscalationBridge): void => {
1098
- if (bridge.usesFallback) {
1099
- speakFixedPhrase(bridge.spokenBridge, bridge.language);
1100
- return;
1101
- }
1102
- fullResponseText += `${bridge.spokenBridge} `;
1103
- flushSafeText(fullResponseText, { force: true });
1104
- // Force-synthesize the bridge now. On the synthesized-TTS path text is
1105
- // held in pendingSynthText until a sentence boundary, so an
1106
- // unpunctuated bridge would otherwise sit unspoken until the post-leg
1107
- // drain, leaving the caller in silence during the escalated model's
1108
- // call: the exact gap the bridge exists to mask.
1109
- if (synthProvider) {
1110
- const { segments } = extractSpeakableSegments(pendingSynthText, true);
1111
- pendingSynthText = "";
1112
- enqueueSynthesisSegments(synthProvider, segments);
1113
- }
1114
- };
940
+ // One filter feeds both consumers: the spoken stream and the text
941
+ // used for transcripts, assistant_spoke, and END_CALL/ASK_GUARDIAN
942
+ // marker detection. A control marker inside a reasoning span must
943
+ // never trigger a real action the caller did not hear.
944
+ const speakable = reasoningFilter.push(text);
945
+ fullResponseText += speakable;
946
+ ttsBuffer += speakable;
947
+ ttsBuffer = stripInternalSpeechMarkers(ttsBuffer);
948
+ flushSafeText();
949
+ };
1115
950
 
1116
- // The escalated leg, once the front-door leg hands off. Started from
1117
- // inside the hand-off; awaited by the turn after the front-door leg
1118
- // settles.
1119
- let escalatedLeg: Promise<void> | null = null;
1120
-
1121
- // Run one leg through the session bridge, streaming its deltas through
1122
- // the shared TTS closures above. Errors cancel and drain this turn's
1123
- // synthesis before propagating.
1124
- const runVoiceLeg = async (leg: PhoneVoiceLeg): Promise<void> => {
1125
- // Reasoning models can inline <think> spans in the content stream when
1126
- // a profile has not opted into parseThinkTags. Neither the spoken path
1127
- // nor the post-turn consumers of fullResponseText (transcripts,
1128
- // assistant_spoke, END_CALL/ASK_GUARDIAN detection) may see them: both
1129
- // are fed only filtered text. One filter per leg: a span never straddles
1130
- // the hand-off.
1131
- const reasoningFilter = createReasoningTagFilter();
1132
- let legHandle: VoiceTurnHandle | null = null;
1133
- // Verdict-first: a front-door leg's leading tokens decide the turn's
1134
- // fate, so its raw stream is a control plane until they classify. The
1135
- // shared coordinator reads it and sequences the hand-off; only text it
1136
- // releases is spoken or recorded. Phone has no partial transcripts,
1137
- // so the hold verdict is never taught.
1138
- const coordinator: FrontDoorLegCoordinator | null =
1139
- leg.routingLeg === "front-door"
1140
- ? createFrontDoorLegCoordinator({
1141
- holdEnabled: false,
1142
- host: {
1143
- isLive: () =>
1144
- this.isCurrentRun(runVersion) && !runSignal.aborted,
1145
- language: () => this.resolveSynthesisLanguage(),
1146
- progress: cadence,
1147
- onAnswerText: (text) => {
1148
- fullResponseText += text;
1149
- flushSafeText(fullResponseText);
1150
- },
1151
- abortLeg: () => legHandle?.abort(),
1152
- speakBridge: speakEscalationBridge,
1153
- startEscalatedLeg: (escalated) => {
1154
- const started = runVoiceLeg(escalated);
1155
- // Awaited by the turn; the rejection is observed there.
1156
- started.catch(() => {});
1157
- escalatedLeg = started;
1158
- },
1159
- },
1160
- })
1161
- : null;
951
+ const onComplete = (): void => {
952
+ resolve();
953
+ };
1162
954
 
1163
- const legComplete = new Promise<void>((resolve, reject) => {
1164
- const ingest = (speakable: string): void => {
1165
- if (speakable.length > 0) {
1166
- deltaEpoch += 1;
1167
- }
1168
- if (coordinator !== null) {
1169
- coordinator.push(speakable);
1170
- return;
1171
- }
1172
- fullResponseText += speakable;
1173
- flushSafeText(fullResponseText);
1174
- };
955
+ const onError = (message: string): void => {
956
+ reject(new Error(message));
957
+ };
1175
958
 
1176
- const onTextDelta = (text: string): void => {
1177
- // Once the front-door leg hands off, nothing more from it is
1178
- // spoken, recorded, or acted on.
1179
- if (!this.isCurrentRun(runVersion) || coordinator?.handedOff) {
1180
- return;
959
+ // Start the voice turn through the session bridge
960
+ startVoiceTurn({
961
+ conversationId: this.conversationId,
962
+ callSessionId: this.callSessionId,
963
+ content,
964
+ assistantId: this.assistantId,
965
+ trustContext: this.trustContext ?? undefined,
966
+ isInbound: this.isInbound,
967
+ task: this.task,
968
+ skipDisclosure: this.skipDisclosure,
969
+ onTextDelta,
970
+ onComplete,
971
+ onError,
972
+ signal: runSignal,
973
+ })
974
+ .then((handle) => {
975
+ if (this.isCurrentRun(runVersion)) {
976
+ this.currentTurnHandle = handle;
977
+ } else {
978
+ // Turn was superseded before handle arrived; abort immediately
979
+ handle.abort();
1181
980
  }
1182
- ingest(reasoningFilter.push(text));
1183
- };
981
+ })
982
+ .catch((err) => {
983
+ reject(err);
984
+ });
1184
985
 
1185
- const onComplete = (): void => {
1186
- // A cancelled leg (barge-in, teardown) never hands off: a dead turn
1187
- // must not spawn an escalated leg. The bridge reports its own
1188
- // abort here too, which the hand-off already settled.
1189
- if (
1190
- !this.isCurrentRun(runVersion) ||
1191
- runSignal.aborted ||
1192
- coordinator?.handedOff
1193
- ) {
1194
- resolve();
1195
- return;
1196
- }
1197
- // A held "[..." tail that never completed a marker is real text,
1198
- // released to both consumers before the leg settles.
1199
- ingest(reasoningFilter.flush());
1200
- coordinator?.complete();
986
+ // Defensive: if the turn is aborted (e.g. barge-in) and the event
987
+ // sink callbacks are never invoked, resolve the promise so it
988
+ // doesn't hang forever.
989
+ runSignal.addEventListener(
990
+ "abort",
991
+ () => {
1201
992
  resolve();
1202
- };
1203
-
1204
- const onError = (message: string): void => {
1205
- reject(new Error(message));
1206
- };
1207
-
1208
- startVoiceTurn({
1209
- conversationId: this.conversationId,
1210
- callSessionId: this.callSessionId,
1211
- // The call session is the phone analogue of a live-voice session:
1212
- // its id joins the turn's telemetry row to the call, and the entry
1213
- // names the direction the call came from.
1214
- voiceTelemetry: {
1215
- sessionId: this.callSessionId,
1216
- entry: this.isInbound ? "phone_inbound" : "phone_outbound",
1217
- },
1218
- content: leg.content,
1219
- assistantId: this.assistantId,
1220
- trustContext: this.trustContext ?? undefined,
1221
- isInbound: this.isInbound,
1222
- task: this.task,
1223
- skipDisclosure: this.skipDisclosure,
1224
- launchedAtMs,
1225
- routingLeg: leg.routingLeg,
1226
- ...(leg.spokenEscalationBridge !== undefined
1227
- ? { spokenEscalationBridge: leg.spokenEscalationBridge }
1228
- : {}),
1229
- onTextDelta,
1230
- onComplete,
1231
- onError,
1232
- callbacks: legCallbacks(
1233
- leg.routingLeg,
1234
- () => coordinator?.handedOff === true,
1235
- ),
1236
- signal: runSignal,
1237
- })
1238
- .then((handle) => {
1239
- legHandle = handle;
1240
- if (this.isCurrentRun(runVersion) && !coordinator?.handedOff) {
1241
- this.currentTurnHandle = handle;
1242
- } else {
1243
- // Superseded, or the front-door leg handed off before its
1244
- // handle arrived: abort immediately.
1245
- handle.abort();
1246
- }
1247
- })
1248
- .catch((err) => {
1249
- reject(err);
1250
- });
1251
-
1252
- // Defensive: if the turn is aborted (e.g. barge-in) and the event
1253
- // sink callbacks are never invoked, resolve the promise so it
1254
- // doesn't hang forever.
1255
- runSignal.addEventListener(
1256
- "abort",
1257
- () => {
1258
- resolve();
1259
- },
1260
- { once: true },
1261
- );
1262
- });
993
+ },
994
+ { once: true },
995
+ );
996
+ });
1263
997
 
1264
- // Eagerly mark the rejection as handled so runtimes (e.g. bun) don't
1265
- // flag it as an unhandled rejection when onError fires synchronously
1266
- // inside the Promise constructor before this await adds its handler.
1267
- // The await below still re-throws, caught by the outer try-catch.
1268
- legComplete.catch(() => {});
1269
- try {
1270
- await legComplete;
1271
- } catch (err) {
1272
- // Cancel and settle this turn's synthesis before the error reaches
1273
- // the outer handler, so no straggling segment plays the partial
1274
- // answer over the recovery prompt. While this run is current no
1275
- // newer run's synthesis can be in flight, so the abort is safe.
1276
- if (this.isCurrentRun(runVersion)) {
1277
- synthesisCancelled = true;
1278
- this.abortActiveSynthesis();
1279
- }
1280
- settleTurn();
1281
- await synthesisChain.catch(() => {});
1282
- throw err;
998
+ // Eagerly mark the rejection as handled so runtimes (e.g. bun) don't
999
+ // flag it as an unhandled rejection when onError fires synchronously
1000
+ // inside the Promise constructor before this await adds its handler.
1001
+ // The await below still re-throws, caught by the outer try-catch.
1002
+ turnComplete.catch(() => {});
1003
+ try {
1004
+ await turnComplete;
1005
+ } catch (err) {
1006
+ // Cancel and settle this turn's synthesis before the error reaches
1007
+ // the outer handler, so no straggling segment plays the partial
1008
+ // answer over the recovery prompt. While this run is current no
1009
+ // newer run's synthesis can be in flight, so the abort is safe.
1010
+ if (this.isCurrentRun(runVersion)) {
1011
+ synthesisCancelled = true;
1012
+ this.abortActiveSynthesis();
1283
1013
  }
1284
- };
1285
-
1286
- // Superseded mid-stream (barge-in): drain the segment chain (queued
1287
- // links short-circuit on staleness) so this turn's synthesis fully
1288
- // settles instead of racing the next turn.
1289
- const settleSuperseded = async (): Promise<VoiceTurnOutcome> => {
1290
- settleTurn();
1291
1014
  await synthesisChain.catch(() => {});
1292
- outcome.text = fullResponseText;
1293
- return outcome;
1294
- };
1295
-
1296
- // Narration speaks into the turn's audible dead air on a cadence wherever
1297
- // in the turn it occurs; the front-door leg is toolless and quick, so in
1298
- // practice the escalated leg's tool loops are what it covers.
1299
- cadence.arm();
1300
- await runVoiceLeg({ content, routingLeg: "front-door" });
1015
+ throw err;
1016
+ }
1301
1017
  if (!this.isCurrentRun(runVersion)) {
1302
- return settleSuperseded();
1018
+ // Superseded mid-stream (barge-in): drain the segment chain — queued
1019
+ // links short-circuit on staleness — so this turn's synthesis fully
1020
+ // settles instead of racing the next turn.
1021
+ await synthesisChain.catch(() => {});
1022
+ return fullResponseText;
1303
1023
  }
1304
1024
 
1305
- // The front-door model handed off: the coordinator spoke the bridge and
1306
- // started the escalated leg, which answers for real.
1307
- if (escalatedLeg !== null) {
1308
- await escalatedLeg;
1309
- if (!this.isCurrentRun(runVersion)) {
1310
- return settleSuperseded();
1311
- }
1025
+ // Final sweep: release any held-back partial tag to both consumers,
1026
+ // then strip any remaining control markers from the buffer.
1027
+ const filterTail = reasoningFilter.flush();
1028
+ fullResponseText += filterTail;
1029
+ ttsBuffer += filterTail;
1030
+ ttsBuffer = stripInternalSpeechMarkers(ttsBuffer);
1031
+ if (ttsBuffer.length > 0) {
1032
+ emitSafeChunk(ttsBuffer);
1312
1033
  }
1313
1034
 
1314
- // The turn's legs are done: no narration past this point.
1315
- settleTurn();
1316
-
1317
- // Final sweep: a held "[..." tail that never completed a marker is real
1318
- // text, so the forced flush speaks it instead of dropping it.
1319
- flushSafeText(fullResponseText, { force: true });
1320
-
1321
1035
  // Synthesized path: force-extract whatever never reached a speakable
1322
1036
  // boundary, then drain the chain so every segment's audio (or its
1323
1037
  // native fallback) is enqueued before the end-of-turn signal. The
@@ -1338,8 +1052,7 @@ export class CallController {
1338
1052
  // doesn't inject its end-of-turn marker (or fallback text) into the next
1339
1053
  // turn's output stream.
1340
1054
  if (!this.isCurrentRun(runVersion)) {
1341
- outcome.text = fullResponseText;
1342
- return outcome;
1055
+ return fullResponseText;
1343
1056
  }
1344
1057
 
1345
1058
  // Signal end of this turn's speech. An empty token with `last: true`
@@ -1356,8 +1069,7 @@ export class CallController {
1356
1069
  this.lastSentWasOpener = false;
1357
1070
  }
1358
1071
 
1359
- outcome.text = fullResponseText;
1360
- return outcome;
1072
+ return fullResponseText;
1361
1073
  }
1362
1074
 
1363
1075
  /**
@@ -1381,7 +1093,6 @@ export class CallController {
1381
1093
  text: string,
1382
1094
  runVersion: number,
1383
1095
  format: CallAudioFormat = "mp3",
1384
- languageOverride?: string,
1385
1096
  ): Promise<SegmentSynthesisStatus> {
1386
1097
  let sink: AudioStoreSink | null = null;
1387
1098
  let playUrlSent = false;
@@ -1403,9 +1114,7 @@ export class CallController {
1403
1114
 
1404
1115
  this.activeSynthesisAbort = abortController;
1405
1116
 
1406
- // A fixed phrase whose text is not in the caller's language carries
1407
- // its own hint; model text rides the turn's resolved language.
1408
- const language = languageOverride ?? this.resolveSynthesisLanguage();
1117
+ const language = this.resolveSynthesisLanguage();
1409
1118
  // A language-known segment may select the synthesizing provider's
1410
1119
  // configured per-language voice; no entry keeps the provider default.
1411
1120
  const voiceId = resolveTelephonyLanguageVoice(provider.id, language);
@@ -1493,19 +1202,12 @@ export class CallController {
1493
1202
  * (ASK_GUARDIAN_APPROVAL / ASK_GUARDIAN), call finalization (END_CALL),
1494
1203
  * and normal idle transition.
1495
1204
  */
1496
- private async handleTurnCompletion(outcome: VoiceTurnOutcome): Promise<void> {
1497
- const responseText = outcome.text;
1205
+ private async handleTurnCompletion(fullResponseText: string): Promise<void> {
1206
+ const responseText = fullResponseText;
1498
1207
 
1499
- // Record the assistant response event, keyed to the conversation rows it
1500
- // mirrors when the bridge reported them.
1208
+ // Record the assistant response event
1501
1209
  recordCallEvent(this.callSessionId, "assistant_spoke", {
1502
1210
  text: responseText,
1503
- ...(outcome.userMessageId !== null
1504
- ? { userMessageId: outcome.userMessageId }
1505
- : {}),
1506
- ...(outcome.assistantMessageId !== null
1507
- ? { assistantMessageId: outcome.assistantMessageId }
1508
- : {}),
1509
1211
  });
1510
1212
  const spokenText = sanitizeForTts(
1511
1213
  stripInternalSpeechMarkers(responseText),
@@ -1910,11 +1612,9 @@ export class CallController {
1910
1612
  /**
1911
1613
  * Flip from the pre-speech `processing` phase to `speaking` at the moment the
1912
1614
  * first real outbound audio/token is emitted. Guarded so a superseded or
1913
- * aborted (idle) turn never (re)enters `speaking`. The state tells the
1914
- * transport whether audio is audible (it decides what a barge-in has to
1915
- * clear and whether an end-of-turn mark is owed); a barge-in itself may
1916
- * abort a turn in either phase once the transport's sustained-speech
1917
- * guard has vouched for it.
1615
+ * aborted (idle) turn never (re)enters `speaking`, and so barge-in
1616
+ * (handleBargeIn, gated on `speaking`) can't abort a turn that is still
1617
+ * waiting for the processing lock or generating with no audio yet.
1918
1618
  */
1919
1619
  private beginSpeaking(runVersion: number): void {
1920
1620
  if (!this.isCurrentRun(runVersion)) {
@@ -2133,7 +1833,7 @@ export class CallController {
2133
1833
 
2134
1834
  private startDurationTimer(): void {
2135
1835
  const maxDurationMs = getMaxCallDurationMs();
2136
- const warningMs = maxDurationMs - DURATION_WARNING_LEAD_MS;
1836
+ const warningMs = maxDurationMs - 2 * 60 * 1000; // 2 minutes before max
2137
1837
 
2138
1838
  if (warningMs > 0) {
2139
1839
  this.durationWarningTimer = setTimeout(() => {
@@ -2194,7 +1894,7 @@ export class CallController {
2194
1894
  },
2195
1895
  );
2196
1896
  }
2197
- }, POST_GOODBYE_HANGUP_DELAY_MS);
1897
+ }, 3000);
2198
1898
  }, maxDurationMs);
2199
1899
  }
2200
1900