@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
@@ -70,8 +70,8 @@ import {
70
70
  CALL_VERIFICATION_COMPLETE_MARKER,
71
71
  ESCALATE_VERDICT_TOKEN,
72
72
  HOLD_VERDICT_TOKEN,
73
- MINIMIZE_ROOM_MARKER,
74
73
  stripInternalSpeechMarkers,
74
+ terminalControlMarkerLength,
75
75
  } from "./voice-control-protocol.js";
76
76
  import {
77
77
  createFrontDoorStreamGate,
@@ -188,6 +188,34 @@ function frontDoorRuleWithDigest(
188
188
  });
189
189
  }
190
190
 
191
+ /**
192
+ * The triage-and-escalate rule a turn's leg adds to its control prompt: the
193
+ * front-door leg decides and may hand off, the escalated leg continues the
194
+ * answer after a holding phrase was already spoken. Null when routing is off.
195
+ * One rule for both prompt shapes, the auto-built phone prompt and a
196
+ * caller-supplied one, so the front-door model is anchored to the same
197
+ * caller words on every transport.
198
+ */
199
+ function routingLegRuleFor(
200
+ opts: Pick<
201
+ VoiceTurnOptions,
202
+ "routingLeg" | "unifiedVerdict" | "spokenEscalationBridge"
203
+ >,
204
+ callerUtterance: string,
205
+ ): string | null {
206
+ switch (opts.routingLeg) {
207
+ case "front-door":
208
+ return frontDoorRuleWithDigest(
209
+ opts.unifiedVerdict === true,
210
+ callerUtterance,
211
+ );
212
+ case "escalated":
213
+ return escalatedContinuationRule(opts.spokenEscalationBridge);
214
+ default:
215
+ return null;
216
+ }
217
+ }
218
+
191
219
  /**
192
220
  * Exact message thrown when `opts.signal` aborts while the turn is waiting
193
221
  * for the conversation to become available. The call controller's abort
@@ -373,8 +401,9 @@ export interface VoiceTurnOptions {
373
401
  *
374
402
  * Deliberately separate from {@link userMessageInterface}: that field feeds
375
403
  * `resolveChannelCapabilities` and decides what the turn may do, so it is not
376
- * free to carry attribution. Absent for phone calls and for clients that send
377
- * no identity on the start frame.
404
+ * free to carry attribution. A phone call passes its call session id and a
405
+ * `phone_*` entry naming the call's direction, with no client. Absent for
406
+ * clients that send no identity on the start frame.
378
407
  */
379
408
  voiceTelemetry?: {
380
409
  sessionId: string;
@@ -447,7 +476,7 @@ export interface VoiceTurnOptions {
447
476
  onComplete?: () => void;
448
477
  /** Called when the agent loop encounters an error. */
449
478
  onError?: (message: string) => void;
450
- /** Event-name callbacks used by non-phone voice clients. */
479
+ /** Event-name callbacks: tool activity, persisted row ids, raw stream. */
451
480
  callbacks?: VoiceTurnCallbacks;
452
481
  /**
453
482
  * Called when this turn leaves a confirmation for the user to answer instead
@@ -482,10 +511,9 @@ export interface VoiceTurnOptions {
482
511
  */
483
512
  overrideProfile?: string;
484
513
  /**
485
- * Which leg of a triaged turn this is, so the auto-built phone control prompt
486
- * can add the front-door triage rule or the escalated continuation rule.
487
- * Undefined = routing off; no routing rules are added. Ignored when a caller
488
- * supplies its own `voiceControlPrompt`.
514
+ * Which leg of a triaged turn this is, so the control prompt (auto-built or
515
+ * caller-supplied) carries the front-door triage rule or the escalated
516
+ * continuation rule. Undefined = routing off; no routing rules are added.
489
517
  */
490
518
  routingLeg?: VoiceRoutingLeg;
491
519
  /**
@@ -611,9 +639,8 @@ function buildVoiceCallControlPrompt(opts: {
611
639
  task?: string | null;
612
640
  isCallerGuardian?: boolean;
613
641
  skipDisclosure?: boolean;
614
- routingLeg?: VoiceRoutingLeg;
615
- spokenEscalationBridge?: string;
616
- unifiedVerdict?: boolean;
642
+ /** The turn's routing-leg rule (see {@link routingLegRuleFor}), if any. */
643
+ routingLegRule?: string | null;
617
644
  }): string {
618
645
  const config = getConfig();
619
646
  const disclosureEnabled =
@@ -665,22 +692,18 @@ function buildVoiceCallControlPrompt(opts: {
665
692
  );
666
693
  }
667
694
 
668
- lines.push(
669
- '6. When caller text includes [SPEAKER id="..." label="..."], treat each speaker as a distinct person and personalize responses using that speaker\'s prior context in this call.',
670
- );
671
-
672
695
  if (opts.isInbound) {
673
696
  if (opts.isCallerGuardian) {
674
697
  lines.push(
675
- '7. If the latest user turn is "(call connected — deliver opening greeting)", this is your user calling you. Answer casually and briefly, like picking up a call from someone you know well. For example: "Hey!" or "What\'s up?" Do NOT introduce yourself, do NOT say you are calling on behalf of anyone, and do NOT ask how you can help in a formal way. Keep it short and natural.',
698
+ '6. If the latest user turn is "(call connected — deliver opening greeting)", this is your user calling you. Answer casually and briefly, like picking up a call from someone you know well. For example: "Hey!" or "What\'s up?" Do NOT introduce yourself, do NOT say you are calling on behalf of anyone, and do NOT ask how you can help in a formal way. Keep it short and natural.',
676
699
  );
677
700
  } else {
678
701
  lines.push(
679
- '7. If the latest user turn is "(call connected — deliver opening greeting)", this is an inbound call you are answering (not a call you initiated). Greet the caller warmly and ask how you can help. Introduce yourself once at the start using your assistant name if you know it (for example: "Hey there, this is Ava, Sam\'s assistant. How can I help?"). If your assistant name is not known, skip the name and just identify yourself as the guardian\'s assistant. Never use a UUID-shaped internal assistant ID as your spoken name. Do NOT say "I\'m calling" or "I\'m calling on behalf of". Vary the wording; do not use a fixed template.',
702
+ '6. If the latest user turn is "(call connected — deliver opening greeting)", this is an inbound call you are answering (not a call you initiated). Greet the caller warmly and ask how you can help. Introduce yourself once at the start using your assistant name if you know it (for example: "Hey there, this is Ava, Sam\'s assistant. How can I help?"). If your assistant name is not known, skip the name and just identify yourself as the guardian\'s assistant. Never use a UUID-shaped internal assistant ID as your spoken name. Do NOT say "I\'m calling" or "I\'m calling on behalf of". Vary the wording; do not use a fixed template.',
680
703
  );
681
704
  }
682
705
  lines.push(
683
- "8. If the latest user turn includes [CALL_OPENING_ACK], treat it as the caller acknowledging your greeting and continue the conversation naturally.",
706
+ "7. If the latest user turn includes [CALL_OPENING_ACK], treat it as the caller acknowledging your greeting and continue the conversation naturally.",
684
707
  );
685
708
  } else {
686
709
  const disclosureReminder =
@@ -688,27 +711,22 @@ function buildVoiceCallControlPrompt(opts: {
688
711
  ? " However, the disclosure text from rule 0 is separate from self-introduction and must always be included in your opening greeting, even if the Task does not mention introducing yourself."
689
712
  : "";
690
713
  lines.push(
691
- '7. If the latest user turn is "(verification completed — transitioning into conversation)", the caller just completed a phone verification code challenge on this call. Greet them naturally and ask if there is anything you can help with. Keep it casual and brief.',
692
- `If the latest user turn is "(call connected — deliver opening greeting)", deliver your opening greeting based solely on the Task context above. The Task already describes how to open the call follow it directly without adding any extra introduction on top. If the Task says to introduce yourself, do so once. If the Task does not mention introducing yourself, skip the introduction.${disclosureReminder} Vary the wording naturally; do not use a fixed template.`,
693
- "8. If the latest user turn includes [CALL_OPENING_ACK], treat it as the callee acknowledging your opener and continue the conversation naturally without re-introducing yourself or repeating the initial check-in question.",
714
+ '6. If the latest user turn is "(verification completed — transitioning into conversation)", the caller just completed a phone verification code challenge on this call. Greet them naturally and ask if there is anything you can help with. Keep it casual and brief.',
715
+ `If the latest user turn is "(call connected — deliver opening greeting)", deliver your opening greeting based solely on the Task context above. The Task already describes how to open the call, so follow it directly without adding any extra introduction on top. If the Task says to introduce yourself, do so once. If the Task does not mention introducing yourself, skip the introduction.${disclosureReminder} Vary the wording naturally; do not use a fixed template.`,
716
+ "7. If the latest user turn includes [CALL_OPENING_ACK], treat it as the callee acknowledging your opener and continue the conversation naturally without re-introducing yourself or repeating the initial check-in question.",
694
717
  );
695
718
  }
696
719
 
697
720
  lines.push(
698
- "9. After the opening greeting turn, treat the Task field as background context only do not re-execute its instructions on subsequent turns.",
699
- '10. Do not make up information. If you are unsure, use [ASK_GUARDIAN: your question] to consult your guardian. For tool permission requests, use [ASK_GUARDIAN_APPROVAL: {"question":"...","toolName":"...","input":{...}}].',
700
- `11. ${SPOKEN_REPLY_PLAIN_TEXT_RULE} Protocol markers like ${opts.isCallerGuardian ? "[END_CALL]" : "[ASK_GUARDIAN: ...] and [END_CALL]"} are not spoken text and should still be used normally.`,
701
- `12. Speak the caller's language: reply in the language of the caller's most recent actual speech, and follow them if they switch languages mid-call. Synthetic user turns (parenthetical markers like the call-connected and verification-completed notices) are not caller speech and never set the language. Before the caller has spoken, such as on the opening greeting turn, ${preSpeechLanguageRuleFragment(config.services.stt.language, sttCatalogKeyForRole(config.services.stt, "telephony"))}.`,
702
- `13. ${PHONE_NO_SETUP_FLOWS_RULE}`,
721
+ "8. After the opening greeting turn, treat the Task field as background context only. Do not re-execute its instructions on subsequent turns.",
722
+ '9. Do not make up information. If you are unsure, use [ASK_GUARDIAN: your question] to consult your guardian. For tool permission requests, use [ASK_GUARDIAN_APPROVAL: {"question":"...","toolName":"...","input":{...}}].',
723
+ `10. ${SPOKEN_REPLY_PLAIN_TEXT_RULE} Protocol markers like ${opts.isCallerGuardian ? "[END_CALL]" : "[ASK_GUARDIAN: ...] and [END_CALL]"} are not spoken text and should still be used normally.`,
724
+ `11. Speak the caller's language: reply in the language of the caller's most recent actual speech, and follow them if they switch languages mid-call. Synthetic user turns (parenthetical markers like the call-connected and verification-completed notices) are not caller speech and never set the language. Before the caller has spoken, such as on the opening greeting turn, ${preSpeechLanguageRuleFragment(config.services.stt.language, sttCatalogKeyForRole(config.services.stt, "telephony"))}.`,
725
+ `12. ${PHONE_NO_SETUP_FLOWS_RULE}`,
703
726
  );
704
727
 
705
- // Triage-and-escalate routing rules. The front-door leg decides and may
706
- // hand off; the escalated leg continues the answer after a holding phrase
707
- // was already spoken.
708
- if (opts.routingLeg === "front-door") {
709
- lines.push(`14. ${frontDoorRuleWithDigest(opts.unifiedVerdict === true)}`);
710
- } else if (opts.routingLeg === "escalated") {
711
- lines.push(`14. ${escalatedContinuationRule(opts.spokenEscalationBridge)}`);
728
+ if (opts.routingLegRule) {
729
+ lines.push(`13. ${opts.routingLegRule}`);
712
730
  }
713
731
 
714
732
  lines.push("</voice_call_control>");
@@ -749,17 +767,22 @@ function stripMarkersFromBlocks(blocks: ContentBlock[]): ContentBlock[] {
749
767
  }
750
768
 
751
769
  /**
752
- * Remove the terminal MINIMIZE_ROOM_MARKER from the end of a row's text,
753
- * walking text blocks from the last one backward so a marker split across
754
- * block boundaries (e.g. `"Done [-"` + `"1]"`) is removed whole the
755
- * per-block strip in {@link stripMarkersFromBlocks} only sees fragments and
756
- * would leave both halves in place. Callers must have established that the
757
- * row's joined text ends with the marker after trimming trailing whitespace.
770
+ * Remove a terminal control marker (the minimize marker or a session control,
771
+ * `markerLength` characters long) from the end of a row's text, walking text
772
+ * blocks from the last one backward so a marker split across block boundaries
773
+ * (e.g. `"Done [-"` + `"1]"`) is removed whole the per-block strip in
774
+ * {@link stripMarkersFromBlocks} only sees fragments and would leave both
775
+ * halves in place. Callers must have established, with
776
+ * {@link terminalControlMarkerLength}, that the row's joined text ends with
777
+ * the marker after trimming trailing whitespace.
758
778
  */
759
- function stripTerminalMinimizeMarker(blocks: ContentBlock[]): ContentBlock[] {
779
+ function stripTerminalControlMarker(
780
+ blocks: ContentBlock[],
781
+ markerLength: number,
782
+ ): ContentBlock[] {
760
783
  const result = blocks.map((block) => ({ ...block }));
761
784
  const joined = joinedTextOfBlocks(result);
762
- const cutAt = joined.trimEnd().length - MINIMIZE_ROOM_MARKER.length;
785
+ const cutAt = joined.trimEnd().length - markerLength;
763
786
  let blockEnd = joined.length;
764
787
  for (let i = result.length - 1; i >= 0 && blockEnd > cutAt; i--) {
765
788
  const block = result[i]!;
@@ -956,6 +979,10 @@ export async function startVoiceTurn(
956
979
  // control markers (ASK_GUARDIAN, END_CALL, etc.) and recognize opener turns.
957
980
  const isCallerGuardian = opts.trustContext?.trustClass === "guardian";
958
981
 
982
+ // The front-door rule anchors on the words the model sees as the user
983
+ // turn, which for a phone sentinel is its neutral persisted form rather
984
+ // than the marker itself.
985
+ const routingLegRule = routingLegRuleFor(opts, persistedContent);
959
986
  let voiceCallControlPrompt: string | null;
960
987
  if (opts.voiceControlPrompt === undefined) {
961
988
  voiceCallControlPrompt = buildVoiceCallControlPrompt({
@@ -963,23 +990,14 @@ export async function startVoiceTurn(
963
990
  task: opts.task,
964
991
  isCallerGuardian,
965
992
  skipDisclosure: opts.skipDisclosure,
966
- routingLeg: opts.routingLeg,
967
- spokenEscalationBridge: opts.spokenEscalationBridge,
968
- unifiedVerdict: opts.unifiedVerdict,
993
+ routingLegRule,
969
994
  });
970
995
  } else {
971
- // A caller-supplied prompt (e.g. live-voice) bypasses
972
- // buildVoiceCallControlPrompt, which is where the triage-and-escalate rule
973
- // is normally injected from `routingLeg`. Append it here too without it
974
- // the front-door leg would run on the fast profile but never learn the
975
- // verdict protocol, so it could not hold or hand off to the escalated leg.
996
+ // A caller-supplied prompt (live voice) replaces the phone prompt, so the
997
+ // routing-leg rule is appended to it here. Without it the front-door leg
998
+ // would run on the fast profile but never learn the verdict protocol, so
999
+ // it could not hold or hand off to the escalated leg.
976
1000
  voiceCallControlPrompt = opts.voiceControlPrompt;
977
- const routingLegRule =
978
- opts.routingLeg === "front-door"
979
- ? frontDoorRuleWithDigest(opts.unifiedVerdict === true, opts.content)
980
- : opts.routingLeg === "escalated"
981
- ? escalatedContinuationRule(opts.spokenEscalationBridge)
982
- : null;
983
1001
  if (voiceCallControlPrompt != null && routingLegRule) {
984
1002
  voiceCallControlPrompt = `${voiceCallControlPrompt}\n\n${routingLegRule}`;
985
1003
  }
@@ -1797,14 +1815,14 @@ export async function startVoiceTurn(
1797
1815
  * never the verdict token or the text streamed past the cap (issue
1798
1816
  * #37850). A row with no spoken bridge (canned-fallback case — that
1799
1817
  * bridge is audio-only) is deleted.
1800
- * - Any leg whose row ENDS with the `[-1]` minimize marker (swallowed
1801
- * before TTS on the live path) has its text blocks rewritten through
1818
+ * - Any leg whose row ENDS with the `[-1]` minimize marker or a session
1819
+ * control marker (`[END_CALL]`, `[MUTE]`, `[MUTE:<seconds>]`), all
1820
+ * swallowed before TTS, has its text blocks rewritten through
1802
1821
  * `stripInternalSpeechMarkers` so the marker never renders in the chat
1803
- * transcript. This covers front-door answers too: that leg is never
1804
- * taught the marker, but it can parrot one from visible conversation
1805
- * history, and the parroted marker is never spoken and never minimizes
1806
- * the room. Deliberately scoped to that marker: rows without it
1807
- * persist byte-identical.
1822
+ * transcript. This covers front-door answers too: a front-door answer
1823
+ * may end with a session control, and it can parrot `[-1]` from visible
1824
+ * conversation history. Deliberately scoped to terminal markers: rows
1825
+ * without one persist byte-identical.
1808
1826
  *
1809
1827
  * After a rewrite, in-memory history is reloaded from the clean DB before
1810
1828
  * the escalated leg — blocked on this turn's teardown — snapshots it, so
@@ -1838,6 +1856,9 @@ export async function startVoiceTurn(
1838
1856
  action = "delete_discarded";
1839
1857
  } else {
1840
1858
  const row = getMessageById(reservedAssistantRowId, opts.conversationId);
1859
+ const terminalMarkerLength = row
1860
+ ? terminalControlMarkerLength(joinedTextOfBlocks(row.content))
1861
+ : 0;
1841
1862
  const cut =
1842
1863
  row && opts.routingLeg === "front-door"
1843
1864
  ? cutFrontDoorContentAtVerdict(row.content)
@@ -1856,19 +1877,18 @@ export async function startVoiceTurn(
1856
1877
  action = "delete_empty";
1857
1878
  }
1858
1879
  } else if (
1859
- // Terminal position only — mirrors the live latch in
1860
- // createControlMarkerHoldback: a reply whose CONTENT contains
1861
- // "[-1]" mid-text never minimized the room, so its transcript
1862
- // keeps that content untouched too. Front-door answer rows (no
1863
- // verdict token to cut) take this branch as well.
1864
- joinedTextOfBlocks(row.content)
1865
- .trimEnd()
1866
- .endsWith(MINIMIZE_ROOM_MARKER)
1880
+ // Terminal position only — mirrors parseTerminalSessionControl: a
1881
+ // reply whose CONTENT contains a marker mid-text never acted on it,
1882
+ // so its transcript keeps that content untouched too. Front-door
1883
+ // answer rows (no verdict token to cut) take this branch as well.
1884
+ terminalMarkerLength > 0
1867
1885
  ) {
1868
1886
  // Terminal marker first (boundary-aware — it may span text blocks),
1869
1887
  // then the per-block strip for any interior complete markers.
1870
1888
  const cleaned = trimOuterTextEdges(
1871
- stripMarkersFromBlocks(stripTerminalMinimizeMarker(row.content)),
1889
+ stripMarkersFromBlocks(
1890
+ stripTerminalControlMarker(row.content, terminalMarkerLength),
1891
+ ),
1872
1892
  );
1873
1893
  // A marker-only reply (the model said nothing beyond "[-1]") strips
1874
1894
  // to nothing at all; keeping the row would render a blank assistant
@@ -1882,7 +1902,7 @@ export async function startVoiceTurn(
1882
1902
  reservedAssistantRowId,
1883
1903
  JSON.stringify(cleaned),
1884
1904
  );
1885
- action = "strip_minimize_marker";
1905
+ action = "strip_control_marker";
1886
1906
  }
1887
1907
  }
1888
1908
  }
@@ -27,7 +27,10 @@
27
27
  */
28
28
 
29
29
  import { NON_LATIN_SENTENCE_ENDING_PUNCTUATION } from "../tts/speakable-segments.js";
30
- import { localizedOrDefault } from "../util/language-subtag.js";
30
+ import {
31
+ fixedPhraseLanguage,
32
+ localizedOrDefault,
33
+ } from "../util/language-subtag.js";
31
34
  import {
32
35
  ESCALATE_VERDICT_TOKEN,
33
36
  HOLD_VERDICT_TOKEN,
@@ -103,6 +106,36 @@ export function fallbackEscalationBridgeFor(language?: string): string {
103
106
  */
104
107
  export const MIN_SPOKEN_BRIDGE_CHARS = 3;
105
108
 
109
+ /**
110
+ * The phrase spoken across an escalation hand-off, for both voice drivers:
111
+ * the front-door leg's own capped bridge when it is a real bridge, else the
112
+ * canned fallback in the caller's language. `usesFallback` marks the canned
113
+ * phrase as audio-only: the model never produced it, so no transcript row
114
+ * carries it (the bridge's hygiene pass deletes the leg's row), and the
115
+ * driver keeps it out of the turn's recorded text. `language` is the TTS
116
+ * hint the phrase must carry: "en" when the canned table has no entry for
117
+ * the caller's language (the phrase is English text then), undefined when
118
+ * the phrase rides the turn's own language.
119
+ */
120
+ export function resolveSpokenEscalationBridge(
121
+ cappedBridge: string,
122
+ language?: string,
123
+ ): { spokenBridge: string; usesFallback: boolean; language?: string } {
124
+ const usesFallback = cappedBridge.length < MIN_SPOKEN_BRIDGE_CHARS;
125
+ if (!usesFallback) {
126
+ return { spokenBridge: cappedBridge, usesFallback };
127
+ }
128
+ const fixedLanguage = fixedPhraseLanguage(
129
+ FALLBACK_ESCALATION_BRIDGE_BY_LANGUAGE,
130
+ language,
131
+ );
132
+ return {
133
+ spokenBridge: fallbackEscalationBridgeFor(language),
134
+ usesFallback,
135
+ ...(fixedLanguage !== undefined ? { language: fixedLanguage } : {}),
136
+ };
137
+ }
138
+
106
139
  /**
107
140
  * Hard cap on the spoken escalation bridge. The bridge is supposed to be a
108
141
  * single short sentence; the cap bounds the hand-off delay (and the audio)
@@ -159,14 +192,20 @@ export function isEscalationBridgeComplete(rawBridge: string): boolean {
159
192
  * output leads with {@link ESCALATE_VERDICT_TOKEN} (a stray token later in
160
193
  * an answer is not an escalation under the verdict-first protocol), else
161
194
  * the capped bridge that followed the token. Used by transcript hygiene to
162
- * reconstruct what the caller heard from a persisted row.
195
+ * reconstruct what the caller heard from a persisted row, by running the
196
+ * same verdict machine the live stream ran through.
163
197
  */
164
198
  export function spokenBridgeText(frontDoorText: string): string {
165
- const leading = frontDoorText.trimStart();
166
- if (!leading.startsWith(ESCALATE_VERDICT_TOKEN)) {
199
+ const machine = createFrontDoorVerdictMachine(false);
200
+ const step = machine.push(frontDoorText);
201
+ if (step.kind !== "escalate") {
167
202
  return "";
168
203
  }
169
- return capEscalationBridge(leading.slice(ESCALATE_VERDICT_TOKEN.length));
204
+ if (step.bridge !== null) {
205
+ return step.bridge;
206
+ }
207
+ const finished = machine.finish();
208
+ return finished.kind === "bridge" ? finished.bridge : "";
170
209
  }
171
210
 
172
211
  /**
@@ -224,107 +263,187 @@ export function classifyFrontDoorLeading(
224
263
  }
225
264
 
226
265
  /**
227
- * Verdict-first gate over a front-door leg's delta stream: given the leg's
228
- * raw deltas in order, it releases only the text the caller actually hears.
229
- * A front-door leg's raw stream is a control plane, not assistant speech,
230
- * until its leading tokens classify, so anything downstream of the model
231
- * that shows text to a person reads the stream through this gate.
266
+ * One step of a front-door leg's stream under the verdict-first protocol,
267
+ * as returned by {@link FrontDoorVerdictMachine.push}.
268
+ */
269
+ export type FrontDoorStep =
270
+ /** The leading tokens could still become a verdict token: keep buffering. */
271
+ | { readonly kind: "pending" }
272
+ /** Hold verdict: the leg is discarded and listening continues. */
273
+ | { readonly kind: "hold" }
274
+ /**
275
+ * The leg's output is the answer. `text` is the raw text this delta
276
+ * released; on the transition it includes the leading text held while the
277
+ * verdict was pending.
278
+ */
279
+ | { readonly kind: "answer"; readonly text: string }
280
+ /**
281
+ * Escalate verdict, decided by this delta. `bridge` is the capped holding
282
+ * phrase when this delta already completed it, else null while the phrase
283
+ * keeps streaming.
284
+ */
285
+ | { readonly kind: "escalate"; readonly bridge: string | null }
286
+ /** Post-verdict text still buffering toward the capped bridge. */
287
+ | { readonly kind: "bridging" }
288
+ /** The capped bridge is complete: hand off to the escalated leg. */
289
+ | { readonly kind: "bridge"; readonly bridge: string }
290
+ /** The leg already held, handed off, or finished; nothing more happens. */
291
+ | { readonly kind: "done" };
292
+
293
+ /**
294
+ * The verdict-first state machine over a front-door leg's delta stream. One
295
+ * implementation drives every consumer of that stream: the live-voice
296
+ * session (which acts on the steps), the conversation-hub stream gate
297
+ * (which releases only the text the caller hears), and transcript hygiene
298
+ * (which replays a persisted row through it).
232
299
  *
233
- * `push` returns the text released by that delta (empty while the gate is
234
- * holding). `finish` is called when the leg completes normally and releases
235
- * a bridge that stopped short of a sentence terminator. A leg that is
236
- * cancelled instead spoke nothing past what `push` already released, so it
237
- * simply never calls `finish`.
300
+ * `push` feeds one delta and reports what it decided. `finish` is called
301
+ * when the leg completes normally: a bridge that stopped short of a
302
+ * sentence terminator hands off with what arrived. A leg that is cancelled
303
+ * instead never calls `finish`.
238
304
  */
239
- export interface FrontDoorStreamGate {
240
- push(deltaText: string): string;
241
- finish(): string;
305
+ export interface FrontDoorVerdictMachine {
306
+ push(deltaText: string): FrontDoorStep;
307
+ finish(): FrontDoorStep;
242
308
  }
243
309
 
310
+ const PENDING_STEP: FrontDoorStep = { kind: "pending" };
311
+ const HOLD_STEP: FrontDoorStep = { kind: "hold" };
312
+ const BRIDGING_STEP: FrontDoorStep = { kind: "bridging" };
313
+ const DONE_STEP: FrontDoorStep = { kind: "done" };
314
+
244
315
  /**
245
- * Build a {@link FrontDoorStreamGate}. `holdEnabled` mirrors
316
+ * Build a {@link FrontDoorVerdictMachine}. `holdEnabled` mirrors
246
317
  * {@link classifyFrontDoorLeading}: true only for speculative (unified
247
318
  * front-door) legs, whose decision rule is the only one that teaches the
248
319
  * hold token.
249
320
  *
250
- * The three verdicts release differently, matching what the caller hears:
251
- *
252
- * - `hold`: the leg is discarded and its row deleted, so nothing is ever
253
- * released.
254
- * - `escalate`: the only spoken text is the capped holding phrase, released
255
- * in one piece once the bridge is complete (exactly what
256
- * {@link capEscalationBridge} yields, so the released text, the audio, and
257
- * the persisted row agree). The verdict token and anything streamed past
258
- * the cap are dropped. A bridge shorter than
259
- * {@link MIN_SPOKEN_BRIDGE_CHARS} releases nothing at all: the session
260
- * substitutes an audio-only canned fallback for it, so there is no
261
- * displayed text for the gate to agree with.
262
- * - `answer`: the leg's output IS the reply, so every delta passes through,
263
- * including the leading text held back while the verdict was pending.
321
+ * The bridge handed off is exactly what {@link capEscalationBridge} yields,
322
+ * so the audio, the released text, the persisted row, and the phrase quoted
323
+ * to the escalated leg all agree. The verdict token and anything streamed
324
+ * past the cap are never released.
264
325
  */
265
- export function createFrontDoorStreamGate(
326
+ export function createFrontDoorVerdictMachine(
266
327
  holdEnabled: boolean,
267
- ): FrontDoorStreamGate {
328
+ ): FrontDoorVerdictMachine {
268
329
  let raw = "";
269
330
  let stage: "deciding" | "answer" | "bridging" | "done" = "deciding";
270
331
  let bridgeRaw = "";
271
332
  let releasedChars = 0;
272
333
 
273
- const releaseBridge = (): string => {
334
+ const completeBridge = (): Extract<FrontDoorStep, { kind: "bridge" }> => {
274
335
  stage = "done";
275
- const capped = capEscalationBridge(bridgeRaw);
276
- // Below the spoken threshold the session throws the model's bridge away
277
- // and plays a canned fallback that is audio-only, deleting the row rather
278
- // than persisting a phrase the model never really produced (see
279
- // `usesFallbackBridge` in `live-voice-session.ts`). Releasing the capped
280
- // text here would put words on a subscriber's screen that the caller never
281
- // heard, which is the same spoken/displayed divergence this gate exists to
282
- // prevent.
283
- return capped.length < MIN_SPOKEN_BRIDGE_CHARS ? "" : capped;
336
+ return { kind: "bridge", bridge: capEscalationBridge(bridgeRaw) };
284
337
  };
285
338
 
286
339
  return {
287
- push(deltaText: string): string {
340
+ push(deltaText: string): FrontDoorStep {
288
341
  raw += deltaText;
289
342
  if (stage === "done") {
290
- return "";
343
+ return DONE_STEP;
291
344
  }
292
345
  if (stage === "bridging") {
293
346
  bridgeRaw += deltaText;
294
- return isEscalationBridgeComplete(bridgeRaw) ? releaseBridge() : "";
347
+ return isEscalationBridgeComplete(bridgeRaw)
348
+ ? completeBridge()
349
+ : BRIDGING_STEP;
295
350
  }
296
351
  if (stage === "deciding") {
297
352
  const verdict = classifyFrontDoorLeading(raw.trimStart(), holdEnabled);
298
353
  if (verdict === "pending") {
299
- return "";
354
+ return PENDING_STEP;
300
355
  }
301
356
  if (verdict === "hold") {
302
357
  stage = "done";
303
- return "";
358
+ return HOLD_STEP;
304
359
  }
305
360
  if (verdict === "escalate") {
306
361
  stage = "bridging";
307
362
  bridgeRaw = raw.trimStart().slice(ESCALATE_VERDICT_TOKEN.length);
308
- return isEscalationBridgeComplete(bridgeRaw) ? releaseBridge() : "";
363
+ return {
364
+ kind: "escalate",
365
+ bridge: isEscalationBridgeComplete(bridgeRaw)
366
+ ? completeBridge().bridge
367
+ : null,
368
+ };
309
369
  }
310
370
  stage = "answer";
311
371
  }
312
372
  // Answer stage: release everything not yet released, which on the
313
373
  // transition includes the leading text the pending verdict held.
314
- const chunk = raw.slice(releasedChars);
374
+ const text = raw.slice(releasedChars);
315
375
  releasedChars = raw.length;
316
- return chunk;
376
+ return { kind: "answer", text };
317
377
  },
318
- finish(): string {
378
+ finish(): FrontDoorStep {
319
379
  if (stage === "bridging") {
320
- return releaseBridge();
380
+ return completeBridge();
321
381
  }
322
382
  stage = "done";
323
- return "";
383
+ return DONE_STEP;
324
384
  },
325
385
  };
326
386
  }
327
387
 
388
+ /**
389
+ * Verdict-first gate over a front-door leg's delta stream: given the leg's
390
+ * raw deltas in order, it releases only the text the caller actually hears.
391
+ * A front-door leg's raw stream is a control plane, not assistant speech,
392
+ * until its leading tokens classify, so anything downstream of the model
393
+ * that shows text to a person reads the stream through this gate.
394
+ *
395
+ * `push` returns the text released by that delta (empty while the gate is
396
+ * holding). `finish` is called when the leg completes normally and releases
397
+ * a bridge that stopped short of a sentence terminator. A leg that is
398
+ * cancelled instead spoke nothing past what `push` already released, so it
399
+ * simply never calls `finish`.
400
+ */
401
+ export interface FrontDoorStreamGate {
402
+ push(deltaText: string): string;
403
+ finish(): string;
404
+ }
405
+
406
+ /**
407
+ * Build a {@link FrontDoorStreamGate} over a {@link FrontDoorVerdictMachine}.
408
+ *
409
+ * The three verdicts release differently, matching what the caller hears:
410
+ *
411
+ * - `hold`: the leg is discarded and its row deleted, so nothing is ever
412
+ * released.
413
+ * - `escalate`: the only spoken text is the capped holding phrase, released
414
+ * in one piece once the bridge is complete. A bridge shorter than
415
+ * {@link MIN_SPOKEN_BRIDGE_CHARS} releases nothing at all: the session
416
+ * substitutes an audio-only canned fallback for it (see
417
+ * `usesFallbackBridge` in `live-voice-session.ts`) and deletes the row
418
+ * rather than persisting a phrase the model never really produced, so
419
+ * there is no displayed text for the gate to agree with.
420
+ * - `answer`: the leg's output IS the reply, so every delta passes through,
421
+ * including the leading text held back while the verdict was pending.
422
+ */
423
+ export function createFrontDoorStreamGate(
424
+ holdEnabled: boolean,
425
+ ): FrontDoorStreamGate {
426
+ const machine = createFrontDoorVerdictMachine(holdEnabled);
427
+ const releasable = (bridge: string): string =>
428
+ bridge.length < MIN_SPOKEN_BRIDGE_CHARS ? "" : bridge;
429
+ const release = (step: FrontDoorStep): string => {
430
+ switch (step.kind) {
431
+ case "answer":
432
+ return step.text;
433
+ case "escalate":
434
+ return step.bridge === null ? "" : releasable(step.bridge);
435
+ case "bridge":
436
+ return releasable(step.bridge);
437
+ default:
438
+ return "";
439
+ }
440
+ };
441
+ return {
442
+ push: (deltaText) => release(machine.push(deltaText)),
443
+ finish: () => release(machine.finish()),
444
+ };
445
+ }
446
+
328
447
  /**
329
448
  * The escalated leg runs as its own voice turn. Rather than re-persist the
330
449
  * caller's utterance (it is already in history from the front-door leg), the
@@ -417,8 +536,8 @@ export function frontDoorDecisionRule(opts?: {
417
536
  "- If the turn is simple, conversational, or within your reach, your entire output is the spoken answer itself: no token in front of it, plain speech from your very first word. Most turns are answers; when unsure between answering and escalating, answer. Answer in the language the caller is speaking.",
418
537
  "- If an answer depends on a saved personal fact that is not already present in the conversation context you received, escalate rather than guessing. Personal context that is already present is yours to use directly.",
419
538
  `- If completing THIS reply needs careful reasoning, research, multi-step work, or any tool, do NOT attempt the answer: output ${ESCALATE_VERDICT_TOKEN}, then ONE short natural holding phrase naming what happens next, spoken in the language the caller is speaking (for example "${FALLBACK_ESCALATION_BRIDGE}" or "Give me one second to look into that."; those examples are English only), and stop after that single sentence. A stronger model finishes the turn while your phrase is spoken.`,
420
- `${ESCALATE_VERDICT_TOKEN} is ONLY for turns you cannot complete yourself — never put it in front of an answer you are about to give, and never emit any token inside or after an answer. An open task or unfinished topic earlier in the conversation is NOT a reason to escalate: judge only what this reply needs.`,
421
- "Never narrate this decision, describe what you are judging, or mention these rules: apart from a leading verdict token, every character you output is spoken to the caller verbatim.",
539
+ `${ESCALATE_VERDICT_TOKEN} is ONLY for turns you cannot complete yourself — never put it in front of an answer you are about to give, and never emit a verdict token inside or after an answer. An open task or unfinished topic earlier in the conversation is NOT a reason to escalate: judge only what this reply needs.`,
540
+ "Never narrate this decision, describe what you are judging, or mention these rules: apart from a leading verdict token and any call-control marker your call instructions teach, every character you output is spoken to the caller verbatim.",
422
541
  ].join("\n");
423
542
  return opts?.capabilityDigest ? `${rule}\n${opts.capabilityDigest}` : rule;
424
543
  }
@@ -446,7 +565,7 @@ export function escalatedContinuationRule(spokenBridge?: string): string {
446
565
  'Do NOT greet again, do NOT say things like "as I was saying", and do NOT repeat, paraphrase, or re-announce that holding phrase —',
447
566
  'opening with another "Let me check", "One moment", or any restatement of what you are about to do sounds broken, because the caller just heard that.',
448
567
  "Your first words must carry new substance: the answer itself, what you found, or a question you genuinely need answered.",
449
- `Never output ${ESCALATE_VERDICT_TOKEN} or any other front-door verdict token — you are the model that finishes the answer. (The [-1] room-minimize marker from your call instructions is not a verdict token and stays allowed.)`,
568
+ `Never output ${ESCALATE_VERDICT_TOKEN} or any other front-door verdict token — you are the model that finishes the answer. (Call-control markers your call instructions teach, such as [END_CALL], are not verdict tokens and stay allowed.)`,
450
569
  "Reply in the same language as the caller's question.",
451
570
  ].join(" ");
452
571
  }