@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
@@ -0,0 +1,189 @@
1
+ /**
2
+ * The front-door leg coordinator, shared by the live-voice session and the
3
+ * phone call controller.
4
+ *
5
+ * A triaged voice turn opens on a fast, toolless front-door leg whose
6
+ * leading tokens carry its verdict (see `voice-triage-escalate.ts`). The
7
+ * coordinator reads that leg's raw stream through the verdict machine and
8
+ * sequences what follows: an answer is released as speech, a hold discards
9
+ * a speculative turn, and an escalate verdict hands the turn off to the
10
+ * escalated leg with a spoken bridge. The hand-off order is the part that
11
+ * must not drift between drivers: pause narration, abort the front-door
12
+ * leg, resolve and speak the bridge, mark it as the floor holder, start the
13
+ * escalated leg, re-arm narration. A leg that was cancelled never hands off.
14
+ *
15
+ * Everything transport-specific comes in through {@link FrontDoorLegHost}:
16
+ * how answer text and the bridge are spoken, how a leg is started or
17
+ * aborted, and the speculative hold and commit that only live voice has.
18
+ */
19
+
20
+ import type { ProgressCadence } from "./voice-progress-cadence.js";
21
+ import {
22
+ createFrontDoorVerdictMachine,
23
+ ESCALATION_CONTINUATION_CONTENT,
24
+ resolveSpokenEscalationBridge,
25
+ } from "./voice-triage-escalate.js";
26
+
27
+ /** The phrase spoken across the hand-off, as {@link resolveSpokenEscalationBridge} settles it. */
28
+ export type SpokenEscalationBridge = ReturnType<
29
+ typeof resolveSpokenEscalationBridge
30
+ >;
31
+
32
+ /** The leg that answers for real after the front-door leg handed off. */
33
+ export interface EscalatedLeg {
34
+ content: string;
35
+ routingLeg: "escalated";
36
+ /**
37
+ * The bridge the listener heard, quoted to the escalated continuation
38
+ * rule so the quality model does not re-announce it.
39
+ */
40
+ spokenEscalationBridge: string;
41
+ }
42
+
43
+ /**
44
+ * The escalated leg for a hand-off whose bridge was `spokenBridge`. The
45
+ * bridge pins this leg to the conversation's own profile, so no profile
46
+ * override rides along.
47
+ */
48
+ export function escalatedLegFor(spokenBridge: string): EscalatedLeg {
49
+ return {
50
+ content: ESCALATION_CONTINUATION_CONTENT,
51
+ routingLeg: "escalated",
52
+ spokenEscalationBridge: spokenBridge,
53
+ };
54
+ }
55
+
56
+ /** The driver's half of a front-door leg. */
57
+ export interface FrontDoorLegHost {
58
+ /**
59
+ * Whether the turn is still live: not cancelled, not finalized. A dead
60
+ * turn must not spawn an escalated leg.
61
+ */
62
+ isLive(): boolean;
63
+ /** The turn's spoken language, for the canned bridge. */
64
+ language(): string | undefined;
65
+ /**
66
+ * Progress narration to pause across the hand-off: the bridge holds the
67
+ * floor, and audible-silence gating keeps narration quiet until it has
68
+ * played on the escalated leg.
69
+ */
70
+ progress?: Pick<ProgressCadence, "clear" | "noteFloorHolder" | "arm">;
71
+ /**
72
+ * Speculative legs only: the model judged the speaker mid-thought, so the
73
+ * leg is discarded and listening continues.
74
+ */
75
+ onHold?(): void;
76
+ /**
77
+ * Speculative legs only: commit the turn before acting on an answer or
78
+ * escalate verdict. Returns false when the turn is gone, which stops the
79
+ * verdict from being acted on.
80
+ */
81
+ commit?(): boolean;
82
+ /**
83
+ * Answer text released by the verdict machine, in stream order. On the
84
+ * transition it includes the leading text held while the verdict was
85
+ * pending. The driver runs its own control-marker holdback over it.
86
+ */
87
+ onAnswerText(text: string): void;
88
+ /**
89
+ * Abort the front-door leg so a model that keeps generating past the
90
+ * bridge cap adds no latency before the escalated leg starts.
91
+ */
92
+ abortLeg(): void;
93
+ /**
94
+ * Speak the bridge so the strong-model call has no dead air. The model's
95
+ * own bridge is real assistant speech; the canned fallback is audio-only,
96
+ * matching the row the bridge's transcript hygiene deletes for it.
97
+ */
98
+ speakBridge(bridge: SpokenEscalationBridge): void;
99
+ /** Start the escalated leg on the same turn. */
100
+ startEscalatedLeg(leg: EscalatedLeg): void;
101
+ }
102
+
103
+ export interface FrontDoorLegCoordinator {
104
+ /** Whether this leg handed the turn off to the escalated leg. */
105
+ readonly handedOff: boolean;
106
+ /** Feed one raw delta of the front-door leg's stream. */
107
+ push(text: string): void;
108
+ /**
109
+ * The leg completed normally (not cancelled). A leg that stopped
110
+ * mid-bridge (a bare escalate verdict, or a holding phrase with no
111
+ * sentence terminator) hands off now with what arrived; the canned
112
+ * fallback covers an empty bridge. Returns whether the leg handed off.
113
+ */
114
+ complete(): boolean;
115
+ }
116
+
117
+ export function createFrontDoorLegCoordinator(options: {
118
+ /**
119
+ * Whether the leg's decision rule teaches the hold token: true only for
120
+ * speculative legs, whose leading tokens are interpreted.
121
+ */
122
+ holdEnabled: boolean;
123
+ host: FrontDoorLegHost;
124
+ }): FrontDoorLegCoordinator {
125
+ const { host } = options;
126
+ const verdict = createFrontDoorVerdictMachine(options.holdEnabled);
127
+ let handedOff = false;
128
+
129
+ const handOff = (cappedBridge: string): void => {
130
+ if (handedOff || !host.isLive()) {
131
+ return;
132
+ }
133
+ handedOff = true;
134
+ host.progress?.clear();
135
+ host.abortLeg();
136
+ const bridge = resolveSpokenEscalationBridge(cappedBridge, host.language());
137
+ host.speakBridge(bridge);
138
+ // The bridge is the turn's spoken acknowledgement: narration keeps its
139
+ // minimum gap from it rather than following it back to back.
140
+ host.progress?.noteFloorHolder();
141
+ host.startEscalatedLeg(escalatedLegFor(bridge.spokenBridge));
142
+ // The escalated leg runs the slowest work in the system, and the bridge
143
+ // only covers its first couple of seconds: the dead air narration
144
+ // exists for.
145
+ host.progress?.arm();
146
+ };
147
+
148
+ return {
149
+ get handedOff() {
150
+ return handedOff;
151
+ },
152
+ push(text) {
153
+ if (handedOff) {
154
+ return;
155
+ }
156
+ const step = verdict.push(text);
157
+ if (step.kind === "hold") {
158
+ host.onHold?.();
159
+ return;
160
+ }
161
+ if (
162
+ (step.kind === "escalate" || step.kind === "answer") &&
163
+ host.commit !== undefined &&
164
+ !host.commit()
165
+ ) {
166
+ return;
167
+ }
168
+ if (step.kind === "answer") {
169
+ host.onAnswerText(step.text);
170
+ return;
171
+ }
172
+ if (
173
+ (step.kind === "escalate" || step.kind === "bridge") &&
174
+ step.bridge !== null
175
+ ) {
176
+ handOff(step.bridge);
177
+ }
178
+ },
179
+ complete() {
180
+ if (!handedOff && host.isLive()) {
181
+ const step = verdict.finish();
182
+ if (step.kind === "bridge") {
183
+ handOff(step.bridge);
184
+ }
185
+ }
186
+ return handedOff;
187
+ },
188
+ };
189
+ }
@@ -0,0 +1,423 @@
1
+ /**
2
+ * Progress narration cadence for one voice turn, shared by the live-voice
3
+ * session and the phone call controller.
4
+ *
5
+ * A voice turn that runs tools is silent while they run. The cadence speaks
6
+ * short audio-only progress updates into that dead air: a generated
7
+ * sentence about what the turn is doing (see `progress-narration.ts`), or a
8
+ * static fallback phrase when generation yields nothing. It follows the
9
+ * turn's tool activity rather than the clock: an "ops" burst, a long
10
+ * operation completing, or a full interval of audible silence with news
11
+ * since the last update, with `maxSilenceMs` as the heartbeat ceiling.
12
+ *
13
+ * The cadence owns the per-turn state (the tool-activity log, epochs,
14
+ * spacing, the self-re-arming idle timer). Everything transport-specific
15
+ * comes in through {@link ProgressCadenceHost}: whether the turn's audio is
16
+ * idle, when it last reached the listener, and how to speak a phrase.
17
+ */
18
+
19
+ import type { VoiceProgressConfig } from "../config/schemas/voice.js";
20
+ import { fixedPhraseLanguage } from "../util/language-subtag.js";
21
+ import { getLogger } from "../util/logger.js";
22
+ import type {
23
+ VoiceProgressNarrator,
24
+ VoiceProgressTextInput,
25
+ } from "./progress-narration.js";
26
+ import {
27
+ pickProgressPhrase,
28
+ PROGRESS_FALLBACK_PHRASES_BY_LANGUAGE,
29
+ } from "./progress-phrases.js";
30
+
31
+ const log = getLogger("voice-progress-cadence");
32
+
33
+ /**
34
+ * One tool operation observed on a turn, fed by the bridge's structured tool
35
+ * callbacks. `completedAtMs`/`isError`/`resultPreview` land with tool_result.
36
+ */
37
+ export interface TurnProgressOp {
38
+ toolName: string;
39
+ toolUseId?: string;
40
+ startedAtMs: number;
41
+ completedAtMs?: number;
42
+ isError?: boolean;
43
+ resultPreview?: string;
44
+ }
45
+
46
+ /** What prompted a narration attempt. */
47
+ export type ProgressNarrationTrigger = "ops" | "idle" | "op_complete";
48
+
49
+ /** A tool completion as the bridge reports it. */
50
+ export interface ProgressToolResult {
51
+ toolName: string;
52
+ toolUseId?: string;
53
+ isError?: boolean;
54
+ resultPreview?: string;
55
+ }
56
+
57
+ /**
58
+ * Newest incomplete op, optionally restricted to a tool name (the
59
+ * tool_result fallback match when no toolUseId correlates).
60
+ */
61
+ export function findLastIncompleteOp(
62
+ ops: readonly TurnProgressOp[],
63
+ toolName?: string,
64
+ ): TurnProgressOp | undefined {
65
+ for (let i = ops.length - 1; i >= 0; i -= 1) {
66
+ const op = ops[i];
67
+ if (
68
+ op.completedAtMs === undefined &&
69
+ (toolName === undefined || op.toolName === toolName)
70
+ ) {
71
+ return op;
72
+ }
73
+ }
74
+ return undefined;
75
+ }
76
+
77
+ /**
78
+ * The transport-specific half of progress narration, supplied by the driver
79
+ * that owns the turn's audio.
80
+ */
81
+ export interface ProgressCadenceHost {
82
+ /** Identifies the turn in logs. */
83
+ turnId: string;
84
+ /** Dispatch timestamp of the turn (`Date.now()` at launch). */
85
+ launchedAtMs: number;
86
+ /** Aborts an in-flight narration generation with the turn. */
87
+ signal: AbortSignal;
88
+ /**
89
+ * Whether the turn may still be narrated: it is the live turn, it has not
90
+ * completed, and it is not waiting on a decision (a turn waiting on an
91
+ * approval says so once and is quiet after that).
92
+ */
93
+ canNarrate(): boolean;
94
+ /**
95
+ * No assistant audio is buffered, queued, in flight, or (estimatedly)
96
+ * still playing to the listener.
97
+ */
98
+ isAudioIdle(): boolean;
99
+ /**
100
+ * Wall-clock estimate of when audio already handed to the transport
101
+ * finishes reaching the listener, 0 when nothing is playing. Together with
102
+ * the cadence's own last-emitted stamp this anchors the dead-air countdown
103
+ * to when the listener last heard something.
104
+ */
105
+ playbackTailUntilMs(): number;
106
+ /** The caller's words this turn answers, for the narrator. */
107
+ transcriptSoFar(): string;
108
+ /** The turn's spoken language, when known. */
109
+ language(): string | undefined;
110
+ /**
111
+ * Bumped on every assistant text delta. A narration generated while the
112
+ * model was already speaking is stale and is dropped.
113
+ */
114
+ deltaEpoch(): number;
115
+ /**
116
+ * Speak one sanitized phrase, audio-only. `language` is the TTS hint the
117
+ * phrase must carry ("en" for a static fallback the localized table lacks
118
+ * in the turn's language); undefined rides the turn's language. Returns
119
+ * false when nothing was spoken.
120
+ */
121
+ speak(phrase: string, language: string | undefined): boolean;
122
+ /** Called after a narration was actually enqueued (metrics). */
123
+ onNarrationSpoken?(): void;
124
+ }
125
+
126
+ export interface ProgressCadenceOptions {
127
+ config: VoiceProgressConfig;
128
+ /** Null disables narration for the turn (no TTS, or narration off). */
129
+ narrator: VoiceProgressNarrator | null;
130
+ host: ProgressCadenceHost;
131
+ /**
132
+ * Rotates the static fallback phrase across turns so a session with
133
+ * repeated dead air does not repeat one sentence.
134
+ */
135
+ nextFallbackPhraseIndex: () => number;
136
+ }
137
+
138
+ export interface ProgressCadence {
139
+ /** Tool operations this turn, in start order. */
140
+ readonly ops: readonly TurnProgressOp[];
141
+ /** Narrations actually spoken this turn. */
142
+ readonly updatesSpoken: number;
143
+ /** A tool started: counts toward the "ops" trigger and is news for idle. */
144
+ toolStarted(toolName: string, toolUseId?: string): void;
145
+ /**
146
+ * A tool finished. A long operation completing narrates on its own beat;
147
+ * a quick one only counts as news.
148
+ */
149
+ toolFinished(event: ProgressToolResult): void;
150
+ /** The turn's audio last finished emitting now: restarts the dead-air clock. */
151
+ noteAudioSettled(): void;
152
+ /**
153
+ * Something else held the floor now (a spoken approval line, a bridge):
154
+ * narration keeps `minGapMs` from it.
155
+ */
156
+ noteFloorHolder(): void;
157
+ /**
158
+ * Arm (or re-arm) the dead-air timer. A no-op when narration is off for
159
+ * the turn.
160
+ */
161
+ arm(): void;
162
+ /** Stop the dead-air timer; the turn ended, was cancelled, or handed off. */
163
+ clear(): void;
164
+ }
165
+
166
+ export function createProgressCadence(
167
+ options: ProgressCadenceOptions,
168
+ ): ProgressCadence {
169
+ const { config, narrator, host, nextFallbackPhraseIndex } = options;
170
+ const ops: TurnProgressOp[] = [];
171
+ // Ops accumulated toward the next ops-triggered narration. Counted once
172
+ // per op, on start (not completion), so a burst of slow tools still trips
173
+ // the threshold while they run.
174
+ let opsSinceNarration = 0;
175
+ // Bumped by every observable change to the turn's tool activity. The idle
176
+ // trigger compares it against `narratedEpoch` so a tick with nothing new
177
+ // to report stays silent.
178
+ let stateEpoch = 0;
179
+ let narratedEpoch = 0;
180
+ let updatesSpoken = 0;
181
+ // When the last spoken floor-holder enqueued; gates `minGapMs`.
182
+ let lastFloorHolderAtMs: number | null = null;
183
+ // When the turn's audio last finished emitting (turn launch until anything
184
+ // speaks). Idle time is measured from when the listener last heard
185
+ // something, not from when the turn started.
186
+ let lastAudibleAtMs = Date.now();
187
+ let idleTimer: ReturnType<typeof setTimeout> | null = null;
188
+ // A narration generation is awaiting the narrator; serializes narrations.
189
+ let narrationInFlight = false;
190
+
191
+ const enabled = config.enabled && narrator !== null;
192
+
193
+ // When the turn's current audible silence began: the latest of the last
194
+ // emitted segment, the estimated playback end, and the last floor holder.
195
+ const silenceSinceMs = (): number =>
196
+ Math.max(
197
+ lastAudibleAtMs,
198
+ host.playbackTailUntilMs(),
199
+ lastFloorHolderAtMs ?? 0,
200
+ );
201
+
202
+ const canNarrate = (): boolean => host.canNarrate() && host.isAudioIdle();
203
+
204
+ const withinMinGap = (): boolean =>
205
+ lastFloorHolderAtMs !== null &&
206
+ Date.now() - lastFloorHolderAtMs < config.minGapMs;
207
+
208
+ // The idle tick has something worth saying when the turn's tool activity
209
+ // has moved since the last narration described it, or when the silence has
210
+ // run past `maxSilenceMs`, the heartbeat ceiling that proves the assistant
211
+ // is still alive on a turn with no observable activity at all.
212
+ const idleHasSomethingToSay = (): boolean => {
213
+ if (stateEpoch !== narratedEpoch) {
214
+ return true;
215
+ }
216
+ const silentForMs = Date.now() - silenceSinceMs();
217
+ if (silentForMs >= config.maxSilenceMs) {
218
+ return true;
219
+ }
220
+ log.debug(
221
+ { turnId: host.turnId, silentForMs },
222
+ "Voice progress narration held: nothing new since the last update",
223
+ );
224
+ return false;
225
+ };
226
+
227
+ const clearTimer = (): void => {
228
+ if (idleTimer !== null) {
229
+ clearTimeout(idleTimer);
230
+ idleTimer = null;
231
+ }
232
+ };
233
+
234
+ // Arms the dead-air timer. The countdown measures audible silence, not
235
+ // time since launch, so it covers mid-turn silences for the whole turn. On
236
+ // expiry with audio still pending, or with the silence not yet a full
237
+ // interval old, it re-arms for the remainder; only a full interval of
238
+ // audible silence reaches the gatekeeper. The interval is a polling
239
+ // cadence, not a speaking cadence: most ticks find nothing new and stay
240
+ // quiet.
241
+ const armTimer = (delayMs?: number): void => {
242
+ clearTimer();
243
+ idleTimer = setTimeout(() => {
244
+ idleTimer = null;
245
+ if (!host.canNarrate()) {
246
+ return;
247
+ }
248
+ if (!host.isAudioIdle()) {
249
+ armTimer();
250
+ return;
251
+ }
252
+ const remaining = silenceSinceMs() + config.idleIntervalMs - Date.now();
253
+ if (remaining > 0) {
254
+ armTimer(remaining);
255
+ return;
256
+ }
257
+ maybeNarrate("idle");
258
+ armTimer();
259
+ }, delayMs ?? config.idleIntervalMs);
260
+ };
261
+
262
+ // Generate and speak one audio-only progress narration. On a null result,
263
+ // idle narration falls back to a static phrase while activity-triggered
264
+ // narration stays silent.
265
+ const speakUpdate = async (
266
+ activeNarrator: VoiceProgressNarrator,
267
+ trigger: ProgressNarrationTrigger,
268
+ ): Promise<void> => {
269
+ narrationInFlight = true;
270
+ const deltaEpochAtLaunch = host.deltaEpoch();
271
+ // The activity this update describes. Tool events that land
272
+ // mid-generation are news the generated text cannot carry, so they must
273
+ // leave the idle trigger armed rather than count as already narrated.
274
+ const stateEpochAtLaunch = stateEpoch;
275
+ try {
276
+ const now = Date.now();
277
+ const currentOp = findLastIncompleteOp(ops);
278
+ const language = host.language();
279
+ const input: VoiceProgressTextInput = {
280
+ transcriptSoFar: host.transcriptSoFar(),
281
+ completedOps: ops
282
+ .filter(
283
+ (op): op is TurnProgressOp & { completedAtMs: number } =>
284
+ op.completedAtMs !== undefined,
285
+ )
286
+ // `ops` is in start order; the narrator wants completion order,
287
+ // which differs when parallel tools finish out of order.
288
+ .sort((a, b) => a.completedAtMs - b.completedAtMs)
289
+ .map((op) => ({
290
+ toolName: op.toolName,
291
+ ...(op.isError !== undefined ? { isError: op.isError } : {}),
292
+ ...(op.resultPreview !== undefined
293
+ ? { resultPreview: op.resultPreview }
294
+ : {}),
295
+ })),
296
+ currentOp: currentOp
297
+ ? {
298
+ toolName: currentOp.toolName,
299
+ elapsedMs: now - currentOp.startedAtMs,
300
+ }
301
+ : null,
302
+ turnElapsedMs: now - host.launchedAtMs,
303
+ updateIndex: updatesSpoken + 1,
304
+ ...(language !== undefined ? { languageHint: language } : {}),
305
+ };
306
+ const generated = await activeNarrator
307
+ .generateProgressText(input, host.signal)
308
+ // The narrator contract never rejects. Keep test stubs fail-soft too.
309
+ .catch(() => null);
310
+ // Re-check liveness and staleness after the provider call.
311
+ if (
312
+ !canNarrate() ||
313
+ host.deltaEpoch() !== deltaEpochAtLaunch ||
314
+ withinMinGap()
315
+ ) {
316
+ return;
317
+ }
318
+ let raw = generated;
319
+ // Generated text is in the turn's language; only the static fallback
320
+ // comes from a localized table and may need the "en" override.
321
+ let fillerLanguage: string | undefined;
322
+ if (raw === null) {
323
+ if (trigger !== "idle") {
324
+ return;
325
+ }
326
+ raw = pickProgressPhrase(nextFallbackPhraseIndex(), language);
327
+ fillerLanguage = fixedPhraseLanguage(
328
+ PROGRESS_FALLBACK_PHRASES_BY_LANGUAGE,
329
+ language,
330
+ );
331
+ }
332
+ if (!host.speak(raw, fillerLanguage)) {
333
+ return;
334
+ }
335
+ lastFloorHolderAtMs = Date.now();
336
+ opsSinceNarration = 0;
337
+ narratedEpoch = stateEpochAtLaunch;
338
+ updatesSpoken += 1;
339
+ host.onNarrationSpoken?.();
340
+ // Restart the dead-air countdown from this narration.
341
+ armTimer();
342
+ } finally {
343
+ narrationInFlight = false;
344
+ }
345
+ };
346
+
347
+ // Gatekeeper. Speaks only while the turn is audibly silent, with one
348
+ // generation at a time and the configured spacing.
349
+ const maybeNarrate = (trigger: ProgressNarrationTrigger): void => {
350
+ if (
351
+ !enabled ||
352
+ narrator === null ||
353
+ !canNarrate() ||
354
+ narrationInFlight ||
355
+ withinMinGap() ||
356
+ (trigger === "ops" && opsSinceNarration < config.opsThreshold) ||
357
+ (trigger === "idle" && !idleHasSomethingToSay())
358
+ ) {
359
+ return;
360
+ }
361
+ void speakUpdate(narrator, trigger);
362
+ };
363
+
364
+ return {
365
+ get ops() {
366
+ return ops;
367
+ },
368
+ get updatesSpoken() {
369
+ return updatesSpoken;
370
+ },
371
+ toolStarted(toolName, toolUseId) {
372
+ ops.push({
373
+ toolName,
374
+ ...(toolUseId !== undefined ? { toolUseId } : {}),
375
+ startedAtMs: Date.now(),
376
+ });
377
+ opsSinceNarration += 1;
378
+ stateEpoch += 1;
379
+ maybeNarrate("ops");
380
+ },
381
+ toolFinished(event) {
382
+ // Match by toolUseId when present; otherwise the last-started
383
+ // incomplete op with the same name (parallel same-name ops resolve
384
+ // newest-first).
385
+ const op =
386
+ (event.toolUseId !== undefined
387
+ ? ops.find((o) => o.toolUseId === event.toolUseId)
388
+ : undefined) ?? findLastIncompleteOp(ops, event.toolName);
389
+ // A long-running op finishing is the beat the listener has been
390
+ // waiting through: it narrates immediately rather than waiting for
391
+ // `opsThreshold` more ops, which on a one-slow-tool turn never arrive.
392
+ // Short ops stay on the ops trigger.
393
+ let trigger: ProgressNarrationTrigger = "ops";
394
+ if (op) {
395
+ op.completedAtMs = Date.now();
396
+ if (event.isError !== undefined) {
397
+ op.isError = event.isError;
398
+ }
399
+ if (event.resultPreview !== undefined) {
400
+ op.resultPreview = event.resultPreview;
401
+ }
402
+ if (op.completedAtMs - op.startedAtMs >= config.longOpMs) {
403
+ trigger = "op_complete";
404
+ }
405
+ }
406
+ stateEpoch += 1;
407
+ maybeNarrate(trigger);
408
+ },
409
+ noteAudioSettled() {
410
+ lastAudibleAtMs = Date.now();
411
+ },
412
+ noteFloorHolder() {
413
+ lastFloorHolderAtMs = Date.now();
414
+ },
415
+ arm() {
416
+ if (!enabled) {
417
+ return;
418
+ }
419
+ armTimer();
420
+ },
421
+ clear: clearTimer,
422
+ };
423
+ }