@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
@@ -29,11 +29,11 @@ export const HOLD_VERDICT_TOKEN = "[0]";
29
29
  export const ESCALATE_VERDICT_TOKEN = "[1]";
30
30
 
31
31
  /**
32
- * Room-minimize token, kept in the marker table so it is stripped from
33
- * speech and from persisted rows. No prompt teaches it: the live-voice
34
- * session decides a minimize from whether a ui-surface tool ran during the
35
- * turn (`revealsUiSurface` on `tool_result`) and sends the `minimize_room`
36
- * frame itself, so a model that emits this token regardless moves nothing.
32
+ * Emitted inline by the main/escalated voice leg to ask the live-voice
33
+ * client to minimize the voice room and reveal the screen behind it
34
+ * (e.g. after creating an app worth showing). Advisory: the daemon
35
+ * translates it into a `minimize_room` server frame after the turn's TTS
36
+ * drains; it is never spoken and never persisted.
37
37
  */
38
38
  export const MINIMIZE_ROOM_MARKER = "[-1]";
39
39
 
@@ -192,9 +192,9 @@ export function stripInternalSpeechMarkers(text: string): string {
192
192
  // ---------------------------------------------------------------------------
193
193
 
194
194
  /**
195
- * All known control marker prefixes. Used by isIncompleteControlMarkerTail to
196
- * detect whether a buffer that starts with `[` might be the beginning of a
197
- * control marker (and should therefore be held rather than flushed to TTS).
195
+ * All known control marker prefixes. Used by couldBeControlMarker to detect
196
+ * whether a buffer that starts with `[` might be the beginning of a control
197
+ * marker (and should therefore be held rather than flushed to TTS).
198
198
  */
199
199
  const CONTROL_MARKER_STRINGS = [
200
200
  "[ASK_GUARDIAN_APPROVAL:",
@@ -211,6 +211,19 @@ const CONTROL_MARKER_STRINGS = [
211
211
  "[GUARDIAN_UNAVAILABLE]",
212
212
  ];
213
213
 
214
+ /**
215
+ * Check whether `text` could be a partial or complete control marker.
216
+ *
217
+ * Returns true if any known marker string is a prefix of `text`
218
+ * (text starts with the marker) or `text` is a prefix of a marker
219
+ * (the marker starts with text — i.e. text is still being streamed).
220
+ */
221
+ export function couldBeControlMarker(text: string): boolean {
222
+ return CONTROL_MARKER_STRINGS.some(
223
+ (marker) => marker.startsWith(text) || text.startsWith(marker),
224
+ );
225
+ }
226
+
214
227
  // Colon-style markers whose bodies terminate at the first "]" — their strip
215
228
  // regexes are non-greedy (`.+?\]`), so the first bracket IS the terminator.
216
229
  // ASK_GUARDIAN_APPROVAL is deliberately absent: its balanced-JSON body may
@@ -254,48 +267,3 @@ export function isIncompleteControlMarkerTail(tail: string): boolean {
254
267
  }
255
268
  return false;
256
269
  }
257
-
258
- /**
259
- * Control-marker hygiene for one model leg's streamed text, shared by the
260
- * phone call controller and the live-voice session. The returned flush is
261
- * called with the leg's cumulative raw text so far and forwards, through
262
- * `emit`, the stripped ({@link stripInternalSpeechMarkers}) prefix that has
263
- * not been emitted yet and cannot contain a still-streaming control marker:
264
- * the flush stops at the first "[" whose tail is an incomplete marker
265
- * ({@link isIncompleteControlMarkerTail}) and holds from there until a later
266
- * delta completes or disproves it. `force` (leg completion) emits the held
267
- * tail so real text that merely resembles a marker prefix is not dropped.
268
- *
269
- * The scan runs forward from the emitted boundary, not from the last "[", so
270
- * brackets INSIDE a streaming marker body (a JSON array or "]"-bearing string
271
- * in ASK_GUARDIAN_APPROVAL) can neither mask the marker's start nor pass as
272
- * its terminator. Markers are stripped, never acted on: a consumer that acts
273
- * on a marker reads the leg's full raw text for it separately.
274
- */
275
- export function createControlMarkerHoldback(
276
- emit: (chunk: string) => void,
277
- ): (raw: string, opts?: { force?: boolean }) => void {
278
- let emitted = 0;
279
- return (raw, opts) => {
280
- let safeEnd = raw.length;
281
- if (opts?.force !== true) {
282
- for (
283
- let i = raw.indexOf("[", emitted);
284
- i !== -1;
285
- i = raw.indexOf("[", i + 1)
286
- ) {
287
- if (isIncompleteControlMarkerTail(raw.slice(i))) {
288
- safeEnd = i;
289
- break;
290
- }
291
- }
292
- }
293
- if (safeEnd > emitted) {
294
- const chunk = stripInternalSpeechMarkers(raw.slice(emitted, safeEnd));
295
- emitted = safeEnd;
296
- if (chunk.length > 0) {
297
- emit(chunk);
298
- }
299
- }
300
- };
301
- }
@@ -188,34 +188,6 @@ 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
-
219
191
  /**
220
192
  * Exact message thrown when `opts.signal` aborts while the turn is waiting
221
193
  * for the conversation to become available. The call controller's abort
@@ -401,9 +373,8 @@ export interface VoiceTurnOptions {
401
373
  *
402
374
  * Deliberately separate from {@link userMessageInterface}: that field feeds
403
375
  * `resolveChannelCapabilities` and decides what the turn may do, so it is not
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.
376
+ * free to carry attribution. Absent for phone calls and for clients that send
377
+ * no identity on the start frame.
407
378
  */
408
379
  voiceTelemetry?: {
409
380
  sessionId: string;
@@ -476,7 +447,7 @@ export interface VoiceTurnOptions {
476
447
  onComplete?: () => void;
477
448
  /** Called when the agent loop encounters an error. */
478
449
  onError?: (message: string) => void;
479
- /** Event-name callbacks: tool activity, persisted row ids, raw stream. */
450
+ /** Event-name callbacks used by non-phone voice clients. */
480
451
  callbacks?: VoiceTurnCallbacks;
481
452
  /**
482
453
  * Called when this turn leaves a confirmation for the user to answer instead
@@ -511,9 +482,10 @@ export interface VoiceTurnOptions {
511
482
  */
512
483
  overrideProfile?: string;
513
484
  /**
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.
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`.
517
489
  */
518
490
  routingLeg?: VoiceRoutingLeg;
519
491
  /**
@@ -639,8 +611,9 @@ function buildVoiceCallControlPrompt(opts: {
639
611
  task?: string | null;
640
612
  isCallerGuardian?: boolean;
641
613
  skipDisclosure?: boolean;
642
- /** The turn's routing-leg rule (see {@link routingLegRuleFor}), if any. */
643
- routingLegRule?: string | null;
614
+ routingLeg?: VoiceRoutingLeg;
615
+ spokenEscalationBridge?: string;
616
+ unifiedVerdict?: boolean;
644
617
  }): string {
645
618
  const config = getConfig();
646
619
  const disclosureEnabled =
@@ -692,18 +665,22 @@ function buildVoiceCallControlPrompt(opts: {
692
665
  );
693
666
  }
694
667
 
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
+
695
672
  if (opts.isInbound) {
696
673
  if (opts.isCallerGuardian) {
697
674
  lines.push(
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.',
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.',
699
676
  );
700
677
  } else {
701
678
  lines.push(
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.',
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.',
703
680
  );
704
681
  }
705
682
  lines.push(
706
- "7. If the latest user turn includes [CALL_OPENING_ACK], treat it as the caller acknowledging your greeting and continue the conversation naturally.",
683
+ "8. If the latest user turn includes [CALL_OPENING_ACK], treat it as the caller acknowledging your greeting and continue the conversation naturally.",
707
684
  );
708
685
  } else {
709
686
  const disclosureReminder =
@@ -711,22 +688,27 @@ function buildVoiceCallControlPrompt(opts: {
711
688
  ? " 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."
712
689
  : "";
713
690
  lines.push(
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.",
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.",
717
694
  );
718
695
  }
719
696
 
720
697
  lines.push(
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}`,
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}`,
726
703
  );
727
704
 
728
- if (opts.routingLegRule) {
729
- lines.push(`13. ${opts.routingLegRule}`);
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)}`);
730
712
  }
731
713
 
732
714
  lines.push("</voice_call_control>");
@@ -974,10 +956,6 @@ export async function startVoiceTurn(
974
956
  // control markers (ASK_GUARDIAN, END_CALL, etc.) and recognize opener turns.
975
957
  const isCallerGuardian = opts.trustContext?.trustClass === "guardian";
976
958
 
977
- // The front-door rule anchors on the words the model sees as the user
978
- // turn, which for a phone sentinel is its neutral persisted form rather
979
- // than the marker itself.
980
- const routingLegRule = routingLegRuleFor(opts, persistedContent);
981
959
  let voiceCallControlPrompt: string | null;
982
960
  if (opts.voiceControlPrompt === undefined) {
983
961
  voiceCallControlPrompt = buildVoiceCallControlPrompt({
@@ -985,14 +963,23 @@ export async function startVoiceTurn(
985
963
  task: opts.task,
986
964
  isCallerGuardian,
987
965
  skipDisclosure: opts.skipDisclosure,
988
- routingLegRule,
966
+ routingLeg: opts.routingLeg,
967
+ spokenEscalationBridge: opts.spokenEscalationBridge,
968
+ unifiedVerdict: opts.unifiedVerdict,
989
969
  });
990
970
  } else {
991
- // A caller-supplied prompt (live voice) replaces the phone prompt, so the
992
- // routing-leg rule is appended to it here. Without it the front-door leg
993
- // would run on the fast profile but never learn the verdict protocol, so
994
- // it could not hold or hand off to the escalated leg.
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.
995
976
  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;
996
983
  if (voiceCallControlPrompt != null && routingLegRule) {
997
984
  voiceCallControlPrompt = `${voiceCallControlPrompt}\n\n${routingLegRule}`;
998
985
  }
@@ -27,10 +27,7 @@
27
27
  */
28
28
 
29
29
  import { NON_LATIN_SENTENCE_ENDING_PUNCTUATION } from "../tts/speakable-segments.js";
30
- import {
31
- fixedPhraseLanguage,
32
- localizedOrDefault,
33
- } from "../util/language-subtag.js";
30
+ import { localizedOrDefault } from "../util/language-subtag.js";
34
31
  import {
35
32
  ESCALATE_VERDICT_TOKEN,
36
33
  HOLD_VERDICT_TOKEN,
@@ -106,36 +103,6 @@ export function fallbackEscalationBridgeFor(language?: string): string {
106
103
  */
107
104
  export const MIN_SPOKEN_BRIDGE_CHARS = 3;
108
105
 
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
-
139
106
  /**
140
107
  * Hard cap on the spoken escalation bridge. The bridge is supposed to be a
141
108
  * single short sentence; the cap bounds the hand-off delay (and the audio)
@@ -192,20 +159,14 @@ export function isEscalationBridgeComplete(rawBridge: string): boolean {
192
159
  * output leads with {@link ESCALATE_VERDICT_TOKEN} (a stray token later in
193
160
  * an answer is not an escalation under the verdict-first protocol), else
194
161
  * the capped bridge that followed the token. Used by transcript hygiene to
195
- * reconstruct what the caller heard from a persisted row, by running the
196
- * same verdict machine the live stream ran through.
162
+ * reconstruct what the caller heard from a persisted row.
197
163
  */
198
164
  export function spokenBridgeText(frontDoorText: string): string {
199
- const machine = createFrontDoorVerdictMachine(false);
200
- const step = machine.push(frontDoorText);
201
- if (step.kind !== "escalate") {
165
+ const leading = frontDoorText.trimStart();
166
+ if (!leading.startsWith(ESCALATE_VERDICT_TOKEN)) {
202
167
  return "";
203
168
  }
204
- if (step.bridge !== null) {
205
- return step.bridge;
206
- }
207
- const finished = machine.finish();
208
- return finished.kind === "bridge" ? finished.bridge : "";
169
+ return capEscalationBridge(leading.slice(ESCALATE_VERDICT_TOKEN.length));
209
170
  }
210
171
 
211
172
  /**
@@ -263,187 +224,107 @@ export function classifyFrontDoorLeading(
263
224
  }
264
225
 
265
226
  /**
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).
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.
299
232
  *
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`.
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`.
304
238
  */
305
- export interface FrontDoorVerdictMachine {
306
- push(deltaText: string): FrontDoorStep;
307
- finish(): FrontDoorStep;
239
+ export interface FrontDoorStreamGate {
240
+ push(deltaText: string): string;
241
+ finish(): string;
308
242
  }
309
243
 
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
-
315
244
  /**
316
- * Build a {@link FrontDoorVerdictMachine}. `holdEnabled` mirrors
245
+ * Build a {@link FrontDoorStreamGate}. `holdEnabled` mirrors
317
246
  * {@link classifyFrontDoorLeading}: true only for speculative (unified
318
247
  * front-door) legs, whose decision rule is the only one that teaches the
319
248
  * hold token.
320
249
  *
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.
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.
325
264
  */
326
- export function createFrontDoorVerdictMachine(
265
+ export function createFrontDoorStreamGate(
327
266
  holdEnabled: boolean,
328
- ): FrontDoorVerdictMachine {
267
+ ): FrontDoorStreamGate {
329
268
  let raw = "";
330
269
  let stage: "deciding" | "answer" | "bridging" | "done" = "deciding";
331
270
  let bridgeRaw = "";
332
271
  let releasedChars = 0;
333
272
 
334
- const completeBridge = (): Extract<FrontDoorStep, { kind: "bridge" }> => {
273
+ const releaseBridge = (): string => {
335
274
  stage = "done";
336
- return { kind: "bridge", bridge: capEscalationBridge(bridgeRaw) };
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;
337
284
  };
338
285
 
339
286
  return {
340
- push(deltaText: string): FrontDoorStep {
287
+ push(deltaText: string): string {
341
288
  raw += deltaText;
342
289
  if (stage === "done") {
343
- return DONE_STEP;
290
+ return "";
344
291
  }
345
292
  if (stage === "bridging") {
346
293
  bridgeRaw += deltaText;
347
- return isEscalationBridgeComplete(bridgeRaw)
348
- ? completeBridge()
349
- : BRIDGING_STEP;
294
+ return isEscalationBridgeComplete(bridgeRaw) ? releaseBridge() : "";
350
295
  }
351
296
  if (stage === "deciding") {
352
297
  const verdict = classifyFrontDoorLeading(raw.trimStart(), holdEnabled);
353
298
  if (verdict === "pending") {
354
- return PENDING_STEP;
299
+ return "";
355
300
  }
356
301
  if (verdict === "hold") {
357
302
  stage = "done";
358
- return HOLD_STEP;
303
+ return "";
359
304
  }
360
305
  if (verdict === "escalate") {
361
306
  stage = "bridging";
362
307
  bridgeRaw = raw.trimStart().slice(ESCALATE_VERDICT_TOKEN.length);
363
- return {
364
- kind: "escalate",
365
- bridge: isEscalationBridgeComplete(bridgeRaw)
366
- ? completeBridge().bridge
367
- : null,
368
- };
308
+ return isEscalationBridgeComplete(bridgeRaw) ? releaseBridge() : "";
369
309
  }
370
310
  stage = "answer";
371
311
  }
372
312
  // Answer stage: release everything not yet released, which on the
373
313
  // transition includes the leading text the pending verdict held.
374
- const text = raw.slice(releasedChars);
314
+ const chunk = raw.slice(releasedChars);
375
315
  releasedChars = raw.length;
376
- return { kind: "answer", text };
316
+ return chunk;
377
317
  },
378
- finish(): FrontDoorStep {
318
+ finish(): string {
379
319
  if (stage === "bridging") {
380
- return completeBridge();
320
+ return releaseBridge();
381
321
  }
382
322
  stage = "done";
383
- return DONE_STEP;
323
+ return "";
384
324
  },
385
325
  };
386
326
  }
387
327
 
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
-
447
328
  /**
448
329
  * The escalated leg runs as its own voice turn. Rather than re-persist the
449
330
  * caller's utterance (it is already in history from the front-door leg), the