@vellumai/assistant 0.12.1 → 0.12.2-staging.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 (548) 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/openapi.yaml +327 -52
  6. package/package.json +3 -1
  7. package/scripts/smoke-desktop-window-theme.ts +186 -0
  8. package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
  9. package/src/__tests__/anthropic-provider.test.ts +0 -63
  10. package/src/__tests__/app-compiler.test.ts +6 -0
  11. package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
  12. package/src/__tests__/app-dir-path-guard.test.ts +1 -3
  13. package/src/__tests__/assistant-event-hub-machine-name.test.ts +6 -7
  14. package/src/__tests__/assistant-event-hub.test.ts +44 -0
  15. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  16. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
  17. package/src/__tests__/computer-use-tools.test.ts +19 -7
  18. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  19. package/src/__tests__/config-schema.test.ts +28 -29
  20. package/src/__tests__/config-watcher.test.ts +23 -8
  21. package/src/__tests__/connection-model-compat.test.ts +2 -2
  22. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  23. package/src/__tests__/conversation-pairing.test.ts +26 -0
  24. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
  25. package/src/__tests__/conversation-queue.test.ts +48 -207
  26. package/src/__tests__/conversation-retry-discard.test.ts +54 -1
  27. package/src/__tests__/conversation-skill-tools.test.ts +27 -0
  28. package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
  29. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  30. package/src/__tests__/conversation-title-service.test.ts +14 -10
  31. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
  32. package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
  33. package/src/__tests__/credential-record-retire.test.ts +171 -1
  34. package/src/__tests__/credential-routes.test.ts +212 -3
  35. package/src/__tests__/credential-security-invariants.test.ts +4 -2
  36. package/src/__tests__/cu-unified-flow.test.ts +8 -2
  37. package/src/__tests__/document-sync-tags.test.ts +25 -0
  38. package/src/__tests__/drain-kick-guard.test.ts +0 -2
  39. package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
  40. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  41. package/src/__tests__/git-porcelain.test.ts +47 -0
  42. package/src/__tests__/heartbeat-service.test.ts +56 -40
  43. package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
  44. package/src/__tests__/host-bash-proxy.test.ts +26 -45
  45. package/src/__tests__/host-browser-proxy.test.ts +6 -17
  46. package/src/__tests__/host-cu-proxy.test.ts +539 -0
  47. package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
  48. package/src/__tests__/host-file-proxy.test.ts +11 -23
  49. package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
  50. package/src/__tests__/list-messages-camera-frame.test.ts +121 -0
  51. package/src/__tests__/llm-catalog-parity.test.ts +25 -0
  52. package/src/__tests__/mcp-auth-routes.test.ts +6 -9
  53. package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
  54. package/src/__tests__/mcp-health-check.test.ts +8 -11
  55. package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
  56. package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
  57. package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
  58. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  59. package/src/__tests__/messaging-send-tool.test.ts +355 -548
  60. package/src/__tests__/model-intents.test.ts +3 -0
  61. package/src/__tests__/notification-vellum-adapter.test.ts +85 -2
  62. package/src/__tests__/oauth-commands-routes.test.ts +113 -0
  63. package/src/__tests__/oauth-provider-profiles.test.ts +18 -0
  64. package/src/__tests__/openai-provider.test.ts +1 -0
  65. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  66. package/src/__tests__/platform-callback-registration.test.ts +8 -16
  67. package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
  68. package/src/__tests__/plugin-import-boundary-guard.test.ts +14 -0
  69. package/src/__tests__/qdrant-manager.test.ts +132 -0
  70. package/src/__tests__/resolve-app.test.ts +135 -0
  71. package/src/__tests__/scaffold-managed-skill-tool.test.ts +229 -6
  72. package/src/__tests__/script-proxy-certs.test.ts +35 -0
  73. package/src/__tests__/service-account-token.test.ts +247 -0
  74. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  75. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  76. package/src/__tests__/skills.test.ts +9 -4
  77. package/src/__tests__/source-fingerprint.test.ts +184 -0
  78. package/src/__tests__/subagent-tool-gate-mode.test.ts +19 -3
  79. package/src/__tests__/subagent-tools.test.ts +3 -2
  80. package/src/__tests__/telephony-tts-guard.test.ts +38 -3
  81. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  82. package/src/__tests__/unicode.test.ts +13 -0
  83. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
  84. package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
  85. package/src/__tests__/workspace-git-service.test.ts +26 -0
  86. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
  87. package/src/acp/__tests__/agent-process.test.ts +258 -4
  88. package/src/acp/__tests__/auth-required.test.ts +96 -0
  89. package/src/acp/__tests__/client-handler.test.ts +90 -2
  90. package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
  91. package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
  92. package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
  93. package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
  94. package/src/acp/__tests__/session-manager.test.ts +895 -4
  95. package/src/acp/agent-process.test.ts +4 -1
  96. package/src/acp/agent-process.ts +96 -10
  97. package/src/acp/auth-required.ts +50 -1
  98. package/src/acp/auto-install.test.ts +9 -7
  99. package/src/acp/client-handler.ts +16 -3
  100. package/src/acp/model-config.test.ts +166 -0
  101. package/src/acp/model-config.ts +138 -0
  102. package/src/acp/resolve-agent.test.ts +220 -6
  103. package/src/acp/resolve-agent.ts +88 -23
  104. package/src/acp/session-manager.test.ts +196 -3
  105. package/src/acp/session-manager.ts +441 -18
  106. package/src/acp/types.ts +50 -5
  107. package/src/agent/loop.ts +1 -0
  108. package/src/api/events/acp-session-model-update.test.ts +88 -0
  109. package/src/api/events/acp-session-model-update.ts +46 -0
  110. package/src/api/events/app-files-changed.ts +4 -3
  111. package/src/api/events/notification-intent.ts +2 -0
  112. package/src/api/events/user-message-echo.ts +7 -1
  113. package/src/api/index.ts +6 -0
  114. package/src/api/responses/conversation-message.ts +5 -0
  115. package/src/apps/app-store.ts +1 -1
  116. package/src/apps/resolve-app.ts +81 -0
  117. package/src/apps/source-fingerprint.ts +266 -0
  118. package/src/avatar/identity-avatar.ts +7 -2
  119. package/src/bundler/app-compiler.ts +15 -0
  120. package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
  121. package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
  122. package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
  123. package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
  124. package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
  125. package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
  126. package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
  127. package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
  128. package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
  129. package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
  130. package/src/calls/__tests__/voice-control-protocol.test.ts +58 -25
  131. package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
  132. package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
  133. package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
  134. package/src/calls/__tests__/voice-session-bridge.test.ts +37 -1
  135. package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
  136. package/src/calls/barge-in-guard.ts +118 -0
  137. package/src/calls/call-constants.ts +24 -4
  138. package/src/calls/call-controller.ts +499 -199
  139. package/src/calls/call-transport.ts +16 -0
  140. package/src/calls/media-stream-output.ts +32 -0
  141. package/src/calls/media-stream-server.ts +82 -30
  142. package/src/calls/media-stream-stt-session.ts +15 -0
  143. package/src/{live-voice → calls}/progress-narration.ts +2 -2
  144. package/src/calls/resolve-call-tts-provider.ts +33 -2
  145. package/src/calls/tts-call-strategy.ts +9 -5
  146. package/src/calls/types.ts +2 -0
  147. package/src/calls/voice-control-protocol.ts +53 -21
  148. package/src/calls/voice-leg-coordinator.ts +189 -0
  149. package/src/calls/voice-progress-cadence.ts +423 -0
  150. package/src/calls/voice-session-bridge.ts +59 -46
  151. package/src/calls/voice-triage-escalate.ts +175 -56
  152. package/src/cli/commands/__tests__/apps.test.ts +245 -0
  153. package/src/cli/commands/__tests__/bash.test.ts +53 -0
  154. package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
  155. package/src/cli/commands/apps.help.ts +67 -13
  156. package/src/cli/commands/apps.ts +183 -0
  157. package/src/cli/commands/bash.help.ts +8 -13
  158. package/src/cli/commands/bash.ts +7 -25
  159. package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
  160. package/src/cli/commands/channels/index.help.ts +70 -2
  161. package/src/cli/commands/channels/index.ts +16 -3
  162. package/src/cli/commands/channels/send.ts +128 -0
  163. package/src/cli/commands/credentials.help.ts +6 -2
  164. package/src/cli/commands/credentials.ts +2 -2
  165. package/src/cli/commands/mcp.help.ts +8 -8
  166. package/src/cli/commands/mcp.ts +1 -1
  167. package/src/cli/commands/roadmap.help.ts +5 -1
  168. package/src/cli/commands/roadmap.ts +1 -1
  169. package/src/cli/lib/bundled-marketplace.json +103 -90
  170. package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
  171. package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
  172. package/src/config/__tests__/platform-identity.test.ts +255 -0
  173. package/src/config/__tests__/platform-rehydration.test.ts +99 -9
  174. package/src/config/acp-agent-write.ts +43 -0
  175. package/src/config/acp-defaults.test.ts +13 -3
  176. package/src/config/acp-defaults.ts +23 -6
  177. package/src/config/acp-schema.test.ts +81 -0
  178. package/src/config/acp-schema.ts +30 -2
  179. package/src/config/bundled-skills/acp/SKILL.md +22 -14
  180. package/src/config/bundled-skills/acp/TOOLS.json +5 -1
  181. package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
  182. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  183. package/src/config/bundled-skills/computer-use/SKILL.md +20 -2
  184. package/src/config/bundled-skills/computer-use/TOOLS.json +99 -0
  185. package/src/config/bundled-skills/computer-use/tools/computer-use-sequence.ts +12 -0
  186. package/src/config/bundled-skills/media-processing/services/preprocess.ts +2 -1
  187. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +2 -1
  188. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -1
  189. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  190. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  191. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
  192. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  193. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -1
  194. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -1
  195. package/src/config/bundled-skills/schedule/SKILL.md +3 -4
  196. package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
  197. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
  198. package/src/config/bundled-skills/skill-management/TOOLS.json +6 -2
  199. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -2
  200. package/src/config/default-profile-catalog.ts +1 -1
  201. package/src/config/env.ts +2 -2
  202. package/src/config/feature-flag-registry.json +24 -12
  203. package/src/config/loader.ts +1 -0
  204. package/src/config/memory-v3-gate.test.ts +31 -0
  205. package/src/config/memory-v3-gate.ts +20 -16
  206. package/src/config/platform-identity.ts +201 -0
  207. package/src/config/platform-rehydration.ts +11 -27
  208. package/src/config/schema.ts +2 -2
  209. package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
  210. package/src/config/schemas/__tests__/voice.test.ts +148 -0
  211. package/src/config/schemas/calls.ts +1 -28
  212. package/src/config/schemas/live-voice.ts +1 -173
  213. package/src/config/schemas/mcp.ts +1 -1
  214. package/src/config/schemas/memory-retrospective.ts +10 -0
  215. package/src/config/schemas/services.ts +8 -0
  216. package/src/config/schemas/skills.ts +0 -14
  217. package/src/config/schemas/voice.ts +150 -0
  218. package/src/credential-execution/process-manager.test.ts +18 -19
  219. package/src/credential-health/credential-health-service.ts +11 -17
  220. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
  221. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
  222. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
  223. package/src/daemon/app-change-notify.ts +45 -0
  224. package/src/daemon/config-watcher.ts +13 -194
  225. package/src/daemon/conversation-error.ts +3 -2
  226. package/src/daemon/conversation-media-retry.ts +2 -1
  227. package/src/daemon/conversation-process.ts +35 -64
  228. package/src/daemon/conversation-runtime-assembly.ts +2 -1
  229. package/src/daemon/conversation-skill-tools.ts +13 -31
  230. package/src/daemon/conversation-store.ts +1 -1
  231. package/src/daemon/conversation-surfaces.ts +15 -9
  232. package/src/daemon/conversation-tool-setup.ts +37 -43
  233. package/src/daemon/conversation.ts +0 -3
  234. package/src/daemon/dictation-profile-store.ts +2 -1
  235. package/src/daemon/handlers/shared.ts +2 -1
  236. package/src/daemon/handlers/skills.ts +6 -3
  237. package/src/daemon/host-bash-proxy.ts +8 -12
  238. package/src/daemon/host-browser-proxy.ts +15 -1
  239. package/src/daemon/host-cu-proxy.ts +186 -19
  240. package/src/daemon/host-file-proxy.ts +8 -10
  241. package/src/daemon/host-proxy-preactivation.ts +2 -1
  242. package/src/daemon/host-transfer-proxy.ts +14 -18
  243. package/src/daemon/lifecycle.ts +0 -5
  244. package/src/daemon/mcp-reload-service.ts +5 -5
  245. package/src/daemon/message-types/sync.ts +1 -0
  246. package/src/daemon/orphan-reaper.ts +1 -1
  247. package/src/daemon/providers-setup.ts +1 -1
  248. package/src/daemon/reaction-history-render.ts +2 -1
  249. package/src/daemon/shutdown-handlers.ts +1 -9
  250. package/src/daemon/skill-memory-refresh.ts +64 -0
  251. package/src/daemon/tool-side-effects.ts +7 -18
  252. package/src/desktop/__tests__/fake-desktop.ts +122 -0
  253. package/src/desktop/desktop-chrome-policy.test.ts +79 -0
  254. package/src/desktop/desktop-chrome-policy.ts +47 -0
  255. package/src/desktop/desktop-chrome-session.ts +37 -0
  256. package/src/desktop/desktop-dependencies.test.ts +102 -0
  257. package/src/desktop/desktop-dependencies.ts +308 -0
  258. package/src/desktop/desktop-feature.test.ts +18 -0
  259. package/src/desktop/desktop-feature.ts +19 -0
  260. package/src/desktop/desktop-panel-config.test.ts +135 -0
  261. package/src/desktop/desktop-panel-config.ts +149 -0
  262. package/src/desktop/desktop-session-manager.test.ts +730 -0
  263. package/src/desktop/desktop-session-manager.ts +850 -0
  264. package/src/desktop/desktop-stream-bridge.test.ts +395 -0
  265. package/src/desktop/desktop-stream-bridge.ts +214 -0
  266. package/src/desktop/desktop-wallpaper.test.ts +191 -0
  267. package/src/desktop/desktop-wallpaper.ts +113 -0
  268. package/src/desktop/desktop-window-drag.ts +177 -0
  269. package/src/desktop/desktop-window-manager-config.test.ts +230 -0
  270. package/src/desktop/desktop-window-manager-config.ts +238 -0
  271. package/src/desktop/desktop-window-theme.ts +170 -0
  272. package/src/heartbeat/heartbeat-service.ts +21 -3
  273. package/src/home/__tests__/feed-writer.test.ts +22 -0
  274. package/src/home/feed-writer.ts +11 -2
  275. package/src/hooks/types.ts +33 -0
  276. package/src/i18n/AGENTS.md +22 -0
  277. package/src/i18n/__tests__/i18n.test.ts +108 -0
  278. package/src/i18n/index.ts +16 -0
  279. package/src/i18n/locales.ts +93 -0
  280. package/src/i18n/messages.ts +61 -0
  281. package/src/i18n/title.ts +25 -0
  282. package/src/inbound/platform-callback-registration.ts +4 -5
  283. package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
  284. package/src/live-voice/__tests__/live-voice-progress.test.ts +15 -15
  285. package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +238 -0
  286. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  287. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
  288. package/src/live-voice/live-voice-photo.ts +25 -6
  289. package/src/live-voice/live-voice-session.ts +213 -693
  290. package/src/live-voice/protocol.ts +0 -2
  291. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  292. package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
  293. package/src/mcp/effective-config.ts +3 -3
  294. package/src/mcp/mcp-header-store.ts +29 -45
  295. package/src/mcp/spec-schema.ts +111 -0
  296. package/src/mcp/startup.ts +8 -4
  297. package/src/mcp/workspace-mcp-config.ts +188 -0
  298. package/src/messaging/provider.ts +10 -3
  299. package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
  300. package/src/messaging/providers/callback-routing.ts +18 -0
  301. package/src/messaging/providers/channel-transport.ts +56 -0
  302. package/src/messaging/providers/discord/transport.ts +17 -0
  303. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
  304. package/src/messaging/providers/slack/adapter.ts +7 -44
  305. package/src/messaging/providers/slack/transport.ts +16 -0
  306. package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
  307. package/src/messaging/providers/telegram-bot/client.ts +0 -26
  308. package/src/messaging/providers/telegram-bot/transport.ts +20 -0
  309. package/src/messaging/providers/whatsapp/adapter.ts +1 -48
  310. package/src/messaging/providers/whatsapp/transport.ts +18 -0
  311. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
  312. package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
  313. package/src/notifications/__tests__/guardian-feed-projection.test.ts +158 -2
  314. package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
  315. package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
  316. package/src/notifications/adapters/macos.ts +3 -0
  317. package/src/notifications/assistant-reply-producer.ts +8 -3
  318. package/src/notifications/conversation-pairing.ts +36 -16
  319. package/src/notifications/delivered-post-record.ts +11 -1
  320. package/src/notifications/guardian-feed-projection.ts +125 -5
  321. package/src/notifications/home-feed-side-effect.ts +55 -7
  322. package/src/notifications/schedule-result-producer.ts +66 -22
  323. package/src/oauth/AGENTS.md +2 -0
  324. package/src/oauth/__tests__/seed-providers-managed.test.ts +54 -0
  325. package/src/oauth/byo-connection.test.ts +105 -0
  326. package/src/oauth/byo-connection.ts +36 -6
  327. package/src/oauth/connection-resolver.test.ts +73 -0
  328. package/src/oauth/connection-resolver.ts +95 -2
  329. package/src/oauth/scope-utils.ts +24 -0
  330. package/src/oauth/seed-providers.ts +204 -25
  331. package/src/outbound-proxy/certs.ts +3 -5
  332. package/src/permissions/checker.test.ts +11 -6
  333. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  334. package/src/persistence/conversation-crud.ts +71 -63
  335. package/src/persistence/conversation-lineage.ts +12 -4
  336. package/src/persistence/conversation-queries.ts +2 -2
  337. package/src/persistence/conversation-title-placeholders.ts +23 -0
  338. package/src/persistence/conversation-title-service.ts +9 -26
  339. package/src/persistence/embeddings/qdrant-manager.ts +163 -18
  340. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  341. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  342. package/src/persistence/message-cursor.ts +78 -0
  343. package/src/persistence/message-reads.ts +30 -0
  344. package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
  345. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
  346. package/src/persistence/schema/memory-core.ts +5 -0
  347. package/src/platform/client.test.ts +4 -2
  348. package/src/platform/client.ts +2 -10
  349. package/src/plugin-api/constants.ts +2 -0
  350. package/src/plugin-api/index.ts +1 -0
  351. package/src/plugin-api/types.ts +2 -0
  352. package/src/plugins/defaults/index.ts +2 -0
  353. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
  354. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
  355. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
  356. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
  357. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
  358. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +5 -0
  359. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
  360. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
  361. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
  362. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
  363. package/src/plugins/defaults/memory/context-search/agent-runner.ts +3 -2
  364. package/src/plugins/defaults/memory/context-search/format.ts +3 -2
  365. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +6 -3
  366. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +3 -2
  367. package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -7
  368. package/src/plugins/defaults/memory/hooks/init.ts +10 -0
  369. package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
  370. package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
  371. package/src/plugins/defaults/memory/host-utils.ts +10 -0
  372. package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
  373. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
  374. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
  375. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
  376. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
  377. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
  378. package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
  379. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  380. package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
  381. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
  382. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
  383. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
  384. package/src/plugins/defaults/memory/startup.ts +28 -2
  385. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
  386. package/src/plugins/defaults/memory/substrate/page-index.ts +6 -1
  387. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  388. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +4 -2
  389. package/src/plugins/defaults/memory/v1/graph/extraction.ts +4 -2
  390. package/src/plugins/defaults/memory/v1/graph/retriever.ts +4 -1
  391. package/src/plugins/defaults/memory/v2/reranker.ts +6 -1
  392. package/src/plugins/defaults/memory/v3/__tests__/card.test.ts +24 -0
  393. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +125 -0
  394. package/src/plugins/defaults/memory/v3/__tests__/sections.test.ts +26 -0
  395. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +30 -0
  396. package/src/plugins/defaults/memory/v3/card.ts +3 -2
  397. package/src/plugins/defaults/memory/v3/pool-select.ts +5 -3
  398. package/src/plugins/defaults/memory/v3/sections.ts +9 -3
  399. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +5 -2
  400. package/src/plugins/defaults/memory/worker.ts +6 -0
  401. package/src/plugins/mcp-servers.ts +15 -75
  402. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  403. package/src/providers/__tests__/retry-callsite.test.ts +41 -0
  404. package/src/providers/__tests__/retry-outbound-sanitize.test.ts +176 -0
  405. package/src/providers/anthropic/client.ts +0 -47
  406. package/src/providers/gemini/client.ts +9 -23
  407. package/src/providers/gemini-thought-signature.test.ts +63 -0
  408. package/src/providers/gemini-thought-signature.ts +44 -0
  409. package/src/providers/inference/__tests__/endpoint-probe.test.ts +42 -3
  410. package/src/providers/inference/auth.ts +2 -3
  411. package/src/providers/inference/connection-availability.ts +27 -8
  412. package/src/providers/inference/endpoint-probe.ts +10 -1
  413. package/src/providers/inference/resolve-auth.ts +22 -4
  414. package/src/providers/inference/service-account-token.ts +248 -0
  415. package/src/providers/model-catalog.ts +34 -0
  416. package/src/providers/model-intents.ts +10 -5
  417. package/src/providers/openai/__tests__/chat-completions-output-token-limit.test.ts +141 -0
  418. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
  419. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
  420. package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
  421. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
  422. package/src/providers/openai/chat-completions-provider.ts +212 -79
  423. package/src/providers/openai/codex-models.ts +0 -4
  424. package/src/providers/openai/google-thought-signature.ts +320 -0
  425. package/src/providers/opencode/client.test.ts +73 -4
  426. package/src/providers/opencode/client.ts +60 -3
  427. package/src/providers/openrouter/client.ts +20 -0
  428. package/src/providers/outbound-request-sanitize.ts +96 -0
  429. package/src/providers/retry.ts +30 -19
  430. package/src/providers/unparseable-tool-args.ts +5 -1
  431. package/src/runtime/AGENTS.md +3 -1
  432. package/src/runtime/__tests__/channel-send.test.ts +443 -0
  433. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
  434. package/src/runtime/__tests__/host-observe.test.ts +40 -1
  435. package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
  436. package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
  437. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
  438. package/src/runtime/agent-wake.ts +2 -1
  439. package/src/runtime/assistant-event-hub.ts +21 -3
  440. package/src/runtime/auth/same-actor.ts +38 -3
  441. package/src/runtime/channel-send.ts +316 -0
  442. package/src/runtime/host-observe.ts +11 -1
  443. package/src/runtime/http-server.ts +160 -151
  444. package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
  445. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +205 -0
  446. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
  447. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
  448. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
  449. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
  450. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
  451. package/src/runtime/routes/acp-routes.test.ts +15 -0
  452. package/src/runtime/routes/acp-routes.ts +72 -9
  453. package/src/runtime/routes/app-management-routes.ts +73 -0
  454. package/src/runtime/routes/app-routes.ts +1 -1
  455. package/src/runtime/routes/channel-send-routes.ts +85 -0
  456. package/src/runtime/routes/conversation-list-routes.ts +14 -3
  457. package/src/runtime/routes/conversation-query-routes.ts +40 -0
  458. package/src/runtime/routes/conversation-routes.ts +16 -3
  459. package/src/runtime/routes/credential-routes.ts +19 -10
  460. package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
  461. package/src/runtime/routes/desktop-setup-routes.ts +35 -0
  462. package/src/runtime/routes/diagnostics-routes.ts +61 -4
  463. package/src/runtime/routes/events-routes.ts +4 -0
  464. package/src/runtime/routes/host-cu-routes.ts +6 -0
  465. package/src/runtime/routes/index.ts +4 -2
  466. package/src/runtime/routes/log-export-routes.ts +2 -0
  467. package/src/runtime/routes/mcp-auth-routes.ts +39 -47
  468. package/src/runtime/routes/migration-routes.ts +6 -20
  469. package/src/runtime/routes/oauth-commands-routes.ts +36 -0
  470. package/src/runtime/routes/platform-routes.ts +18 -15
  471. package/src/runtime/routes/workspace-routes.test.ts +55 -26
  472. package/src/runtime/routes/workspace-routes.ts +46 -63
  473. package/src/runtime/routes/workspace-tree-walk.test.ts +244 -0
  474. package/src/runtime/routes/workspace-tree-walk.ts +240 -0
  475. package/src/runtime/routes/workspace-utils.ts +17 -0
  476. package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
  477. package/src/runtime/services/conversation-serializer.ts +16 -3
  478. package/src/runtime/sync/resource-sync-events.ts +2 -1
  479. package/src/schedule/run-script.ts +2 -1
  480. package/src/schedule/schedule-store.ts +9 -2
  481. package/src/schedule/worker.ts +1 -1
  482. package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
  483. package/src/security/ces-http-record-backend.ts +120 -0
  484. package/src/security/ces-rpc-record-backend.ts +7 -0
  485. package/src/security/untrusted-content.ts +3 -1
  486. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
  487. package/src/skills/workspace-skill-md-signature.ts +50 -0
  488. package/src/subagent/consult-context.ts +12 -25
  489. package/src/tools/__tests__/side-effect-abort-guard.test.ts +1 -0
  490. package/src/tools/acp/list-agents.test.ts +3 -1
  491. package/src/tools/acp/spawn.test.ts +146 -11
  492. package/src/tools/acp/spawn.ts +21 -3
  493. package/src/tools/acp/status.test.ts +94 -0
  494. package/src/tools/acp/status.ts +40 -3
  495. package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
  496. package/src/tools/browser/browser-execution.ts +1 -1
  497. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +9 -3
  498. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +2 -1
  499. package/src/tools/browser/cdp-client/extension-cdp-client.ts +3 -2
  500. package/src/tools/browser/network-recorder.ts +2 -1
  501. package/src/tools/capability-offer.test.ts +22 -2
  502. package/src/tools/capability-offer.ts +16 -1
  503. package/src/tools/computer-use/definitions.ts +121 -2
  504. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
  505. package/src/tools/credentials/metadata-store.ts +100 -0
  506. package/src/tools/filesystem/search.ts +2 -1
  507. package/src/tools/host-filesystem/edit.test.ts +2 -1
  508. package/src/tools/host-filesystem/edit.ts +2 -2
  509. package/src/tools/host-filesystem/read.test.ts +2 -1
  510. package/src/tools/host-filesystem/read.ts +2 -2
  511. package/src/tools/host-filesystem/transfer.test.ts +2 -1
  512. package/src/tools/host-filesystem/transfer.ts +2 -2
  513. package/src/tools/host-filesystem/write.test.ts +2 -1
  514. package/src/tools/host-filesystem/write.ts +2 -2
  515. package/src/tools/host-terminal/host-shell.test.ts +16 -0
  516. package/src/tools/host-terminal/host-shell.ts +4 -20
  517. package/src/tools/network/web-search.ts +6 -1
  518. package/src/tools/policy-context.ts +6 -7
  519. package/src/tools/skills/scaffold-managed.ts +61 -3
  520. package/src/tools/subagent/spawn.ts +5 -8
  521. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
  522. package/src/tools/terminal/sanitized-bash.ts +105 -0
  523. package/src/tools/watch/watch-retro-report.ts +0 -8
  524. package/src/types/host-capabilities.ts +5 -3
  525. package/src/util/__tests__/language-subtag.test.ts +23 -1
  526. package/src/util/ca-bundle.ts +13 -0
  527. package/src/util/language-subtag.ts +16 -0
  528. package/src/util/platform.ts +8 -0
  529. package/src/util/unicode.ts +15 -1
  530. package/src/watch/watch-timeline.ts +2 -2
  531. package/src/workflows/run-manager.ts +2 -1
  532. package/src/workspace/git-porcelain.ts +66 -0
  533. package/src/workspace/git-service.ts +6 -55
  534. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
  535. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
  536. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
  537. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
  538. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
  539. package/src/workspace/migrations/registry.ts +6 -0
  540. package/src/__tests__/app-source-watcher.test.ts +0 -218
  541. package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
  542. package/src/__tests__/speaker-identification.test.ts +0 -56
  543. package/src/calls/speaker-identification.ts +0 -233
  544. package/src/daemon/app-source-watcher.ts +0 -218
  545. package/src/messaging/providers/whatsapp/client.ts +0 -29
  546. package/src/runtime/routes/debug-bash-routes.ts +0 -173
  547. package/src/util/fs-watcher-error.ts +0 -36
  548. /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
@@ -27,7 +27,10 @@
27
27
  */
28
28
 
29
29
  import { NON_LATIN_SENTENCE_ENDING_PUNCTUATION } from "../tts/speakable-segments.js";
30
- import { localizedOrDefault } from "../util/language-subtag.js";
30
+ import {
31
+ fixedPhraseLanguage,
32
+ localizedOrDefault,
33
+ } from "../util/language-subtag.js";
31
34
  import {
32
35
  ESCALATE_VERDICT_TOKEN,
33
36
  HOLD_VERDICT_TOKEN,
@@ -103,6 +106,36 @@ export function fallbackEscalationBridgeFor(language?: string): string {
103
106
  */
104
107
  export const MIN_SPOKEN_BRIDGE_CHARS = 3;
105
108
 
109
+ /**
110
+ * The phrase spoken across an escalation hand-off, for both voice drivers:
111
+ * the front-door leg's own capped bridge when it is a real bridge, else the
112
+ * canned fallback in the caller's language. `usesFallback` marks the canned
113
+ * phrase as audio-only: the model never produced it, so no transcript row
114
+ * carries it (the bridge's hygiene pass deletes the leg's row), and the
115
+ * driver keeps it out of the turn's recorded text. `language` is the TTS
116
+ * hint the phrase must carry: "en" when the canned table has no entry for
117
+ * the caller's language (the phrase is English text then), undefined when
118
+ * the phrase rides the turn's own language.
119
+ */
120
+ export function resolveSpokenEscalationBridge(
121
+ cappedBridge: string,
122
+ language?: string,
123
+ ): { spokenBridge: string; usesFallback: boolean; language?: string } {
124
+ const usesFallback = cappedBridge.length < MIN_SPOKEN_BRIDGE_CHARS;
125
+ if (!usesFallback) {
126
+ return { spokenBridge: cappedBridge, usesFallback };
127
+ }
128
+ const fixedLanguage = fixedPhraseLanguage(
129
+ FALLBACK_ESCALATION_BRIDGE_BY_LANGUAGE,
130
+ language,
131
+ );
132
+ return {
133
+ spokenBridge: fallbackEscalationBridgeFor(language),
134
+ usesFallback,
135
+ ...(fixedLanguage !== undefined ? { language: fixedLanguage } : {}),
136
+ };
137
+ }
138
+
106
139
  /**
107
140
  * Hard cap on the spoken escalation bridge. The bridge is supposed to be a
108
141
  * single short sentence; the cap bounds the hand-off delay (and the audio)
@@ -159,14 +192,20 @@ export function isEscalationBridgeComplete(rawBridge: string): boolean {
159
192
  * output leads with {@link ESCALATE_VERDICT_TOKEN} (a stray token later in
160
193
  * an answer is not an escalation under the verdict-first protocol), else
161
194
  * the capped bridge that followed the token. Used by transcript hygiene to
162
- * reconstruct what the caller heard from a persisted row.
195
+ * reconstruct what the caller heard from a persisted row, by running the
196
+ * same verdict machine the live stream ran through.
163
197
  */
164
198
  export function spokenBridgeText(frontDoorText: string): string {
165
- const leading = frontDoorText.trimStart();
166
- if (!leading.startsWith(ESCALATE_VERDICT_TOKEN)) {
199
+ const machine = createFrontDoorVerdictMachine(false);
200
+ const step = machine.push(frontDoorText);
201
+ if (step.kind !== "escalate") {
167
202
  return "";
168
203
  }
169
- return capEscalationBridge(leading.slice(ESCALATE_VERDICT_TOKEN.length));
204
+ if (step.bridge !== null) {
205
+ return step.bridge;
206
+ }
207
+ const finished = machine.finish();
208
+ return finished.kind === "bridge" ? finished.bridge : "";
170
209
  }
171
210
 
172
211
  /**
@@ -224,107 +263,187 @@ export function classifyFrontDoorLeading(
224
263
  }
225
264
 
226
265
  /**
227
- * Verdict-first gate over a front-door leg's delta stream: given the leg's
228
- * raw deltas in order, it releases only the text the caller actually hears.
229
- * A front-door leg's raw stream is a control plane, not assistant speech,
230
- * until its leading tokens classify, so anything downstream of the model
231
- * that shows text to a person reads the stream through this gate.
266
+ * One step of a front-door leg's stream under the verdict-first protocol,
267
+ * as returned by {@link FrontDoorVerdictMachine.push}.
268
+ */
269
+ export type FrontDoorStep =
270
+ /** The leading tokens could still become a verdict token: keep buffering. */
271
+ | { readonly kind: "pending" }
272
+ /** Hold verdict: the leg is discarded and listening continues. */
273
+ | { readonly kind: "hold" }
274
+ /**
275
+ * The leg's output is the answer. `text` is the raw text this delta
276
+ * released; on the transition it includes the leading text held while the
277
+ * verdict was pending.
278
+ */
279
+ | { readonly kind: "answer"; readonly text: string }
280
+ /**
281
+ * Escalate verdict, decided by this delta. `bridge` is the capped holding
282
+ * phrase when this delta already completed it, else null while the phrase
283
+ * keeps streaming.
284
+ */
285
+ | { readonly kind: "escalate"; readonly bridge: string | null }
286
+ /** Post-verdict text still buffering toward the capped bridge. */
287
+ | { readonly kind: "bridging" }
288
+ /** The capped bridge is complete: hand off to the escalated leg. */
289
+ | { readonly kind: "bridge"; readonly bridge: string }
290
+ /** The leg already held, handed off, or finished; nothing more happens. */
291
+ | { readonly kind: "done" };
292
+
293
+ /**
294
+ * The verdict-first state machine over a front-door leg's delta stream. One
295
+ * implementation drives every consumer of that stream: the live-voice
296
+ * session (which acts on the steps), the conversation-hub stream gate
297
+ * (which releases only the text the caller hears), and transcript hygiene
298
+ * (which replays a persisted row through it).
232
299
  *
233
- * `push` returns the text released by that delta (empty while the gate is
234
- * holding). `finish` is called when the leg completes normally and releases
235
- * a bridge that stopped short of a sentence terminator. A leg that is
236
- * cancelled instead spoke nothing past what `push` already released, so it
237
- * simply never calls `finish`.
300
+ * `push` feeds one delta and reports what it decided. `finish` is called
301
+ * when the leg completes normally: a bridge that stopped short of a
302
+ * sentence terminator hands off with what arrived. A leg that is cancelled
303
+ * instead never calls `finish`.
238
304
  */
239
- export interface FrontDoorStreamGate {
240
- push(deltaText: string): string;
241
- finish(): string;
305
+ export interface FrontDoorVerdictMachine {
306
+ push(deltaText: string): FrontDoorStep;
307
+ finish(): FrontDoorStep;
242
308
  }
243
309
 
310
+ const PENDING_STEP: FrontDoorStep = { kind: "pending" };
311
+ const HOLD_STEP: FrontDoorStep = { kind: "hold" };
312
+ const BRIDGING_STEP: FrontDoorStep = { kind: "bridging" };
313
+ const DONE_STEP: FrontDoorStep = { kind: "done" };
314
+
244
315
  /**
245
- * Build a {@link FrontDoorStreamGate}. `holdEnabled` mirrors
316
+ * Build a {@link FrontDoorVerdictMachine}. `holdEnabled` mirrors
246
317
  * {@link classifyFrontDoorLeading}: true only for speculative (unified
247
318
  * front-door) legs, whose decision rule is the only one that teaches the
248
319
  * hold token.
249
320
  *
250
- * The three verdicts release differently, matching what the caller hears:
251
- *
252
- * - `hold`: the leg is discarded and its row deleted, so nothing is ever
253
- * released.
254
- * - `escalate`: the only spoken text is the capped holding phrase, released
255
- * in one piece once the bridge is complete (exactly what
256
- * {@link capEscalationBridge} yields, so the released text, the audio, and
257
- * the persisted row agree). The verdict token and anything streamed past
258
- * the cap are dropped. A bridge shorter than
259
- * {@link MIN_SPOKEN_BRIDGE_CHARS} releases nothing at all: the session
260
- * substitutes an audio-only canned fallback for it, so there is no
261
- * displayed text for the gate to agree with.
262
- * - `answer`: the leg's output IS the reply, so every delta passes through,
263
- * including the leading text held back while the verdict was pending.
321
+ * The bridge handed off is exactly what {@link capEscalationBridge} yields,
322
+ * so the audio, the released text, the persisted row, and the phrase quoted
323
+ * to the escalated leg all agree. The verdict token and anything streamed
324
+ * past the cap are never released.
264
325
  */
265
- export function createFrontDoorStreamGate(
326
+ export function createFrontDoorVerdictMachine(
266
327
  holdEnabled: boolean,
267
- ): FrontDoorStreamGate {
328
+ ): FrontDoorVerdictMachine {
268
329
  let raw = "";
269
330
  let stage: "deciding" | "answer" | "bridging" | "done" = "deciding";
270
331
  let bridgeRaw = "";
271
332
  let releasedChars = 0;
272
333
 
273
- const releaseBridge = (): string => {
334
+ const completeBridge = (): Extract<FrontDoorStep, { kind: "bridge" }> => {
274
335
  stage = "done";
275
- const capped = capEscalationBridge(bridgeRaw);
276
- // Below the spoken threshold the session throws the model's bridge away
277
- // and plays a canned fallback that is audio-only, deleting the row rather
278
- // than persisting a phrase the model never really produced (see
279
- // `usesFallbackBridge` in `live-voice-session.ts`). Releasing the capped
280
- // text here would put words on a subscriber's screen that the caller never
281
- // heard, which is the same spoken/displayed divergence this gate exists to
282
- // prevent.
283
- return capped.length < MIN_SPOKEN_BRIDGE_CHARS ? "" : capped;
336
+ return { kind: "bridge", bridge: capEscalationBridge(bridgeRaw) };
284
337
  };
285
338
 
286
339
  return {
287
- push(deltaText: string): string {
340
+ push(deltaText: string): FrontDoorStep {
288
341
  raw += deltaText;
289
342
  if (stage === "done") {
290
- return "";
343
+ return DONE_STEP;
291
344
  }
292
345
  if (stage === "bridging") {
293
346
  bridgeRaw += deltaText;
294
- return isEscalationBridgeComplete(bridgeRaw) ? releaseBridge() : "";
347
+ return isEscalationBridgeComplete(bridgeRaw)
348
+ ? completeBridge()
349
+ : BRIDGING_STEP;
295
350
  }
296
351
  if (stage === "deciding") {
297
352
  const verdict = classifyFrontDoorLeading(raw.trimStart(), holdEnabled);
298
353
  if (verdict === "pending") {
299
- return "";
354
+ return PENDING_STEP;
300
355
  }
301
356
  if (verdict === "hold") {
302
357
  stage = "done";
303
- return "";
358
+ return HOLD_STEP;
304
359
  }
305
360
  if (verdict === "escalate") {
306
361
  stage = "bridging";
307
362
  bridgeRaw = raw.trimStart().slice(ESCALATE_VERDICT_TOKEN.length);
308
- return isEscalationBridgeComplete(bridgeRaw) ? releaseBridge() : "";
363
+ return {
364
+ kind: "escalate",
365
+ bridge: isEscalationBridgeComplete(bridgeRaw)
366
+ ? completeBridge().bridge
367
+ : null,
368
+ };
309
369
  }
310
370
  stage = "answer";
311
371
  }
312
372
  // Answer stage: release everything not yet released, which on the
313
373
  // transition includes the leading text the pending verdict held.
314
- const chunk = raw.slice(releasedChars);
374
+ const text = raw.slice(releasedChars);
315
375
  releasedChars = raw.length;
316
- return chunk;
376
+ return { kind: "answer", text };
317
377
  },
318
- finish(): string {
378
+ finish(): FrontDoorStep {
319
379
  if (stage === "bridging") {
320
- return releaseBridge();
380
+ return completeBridge();
321
381
  }
322
382
  stage = "done";
323
- return "";
383
+ return DONE_STEP;
324
384
  },
325
385
  };
326
386
  }
327
387
 
388
+ /**
389
+ * Verdict-first gate over a front-door leg's delta stream: given the leg's
390
+ * raw deltas in order, it releases only the text the caller actually hears.
391
+ * A front-door leg's raw stream is a control plane, not assistant speech,
392
+ * until its leading tokens classify, so anything downstream of the model
393
+ * that shows text to a person reads the stream through this gate.
394
+ *
395
+ * `push` returns the text released by that delta (empty while the gate is
396
+ * holding). `finish` is called when the leg completes normally and releases
397
+ * a bridge that stopped short of a sentence terminator. A leg that is
398
+ * cancelled instead spoke nothing past what `push` already released, so it
399
+ * simply never calls `finish`.
400
+ */
401
+ export interface FrontDoorStreamGate {
402
+ push(deltaText: string): string;
403
+ finish(): string;
404
+ }
405
+
406
+ /**
407
+ * Build a {@link FrontDoorStreamGate} over a {@link FrontDoorVerdictMachine}.
408
+ *
409
+ * The three verdicts release differently, matching what the caller hears:
410
+ *
411
+ * - `hold`: the leg is discarded and its row deleted, so nothing is ever
412
+ * released.
413
+ * - `escalate`: the only spoken text is the capped holding phrase, released
414
+ * in one piece once the bridge is complete. A bridge shorter than
415
+ * {@link MIN_SPOKEN_BRIDGE_CHARS} releases nothing at all: the session
416
+ * substitutes an audio-only canned fallback for it (see
417
+ * `usesFallbackBridge` in `live-voice-session.ts`) and deletes the row
418
+ * rather than persisting a phrase the model never really produced, so
419
+ * there is no displayed text for the gate to agree with.
420
+ * - `answer`: the leg's output IS the reply, so every delta passes through,
421
+ * including the leading text held back while the verdict was pending.
422
+ */
423
+ export function createFrontDoorStreamGate(
424
+ holdEnabled: boolean,
425
+ ): FrontDoorStreamGate {
426
+ const machine = createFrontDoorVerdictMachine(holdEnabled);
427
+ const releasable = (bridge: string): string =>
428
+ bridge.length < MIN_SPOKEN_BRIDGE_CHARS ? "" : bridge;
429
+ const release = (step: FrontDoorStep): string => {
430
+ switch (step.kind) {
431
+ case "answer":
432
+ return step.text;
433
+ case "escalate":
434
+ return step.bridge === null ? "" : releasable(step.bridge);
435
+ case "bridge":
436
+ return releasable(step.bridge);
437
+ default:
438
+ return "";
439
+ }
440
+ };
441
+ return {
442
+ push: (deltaText) => release(machine.push(deltaText)),
443
+ finish: () => release(machine.finish()),
444
+ };
445
+ }
446
+
328
447
  /**
329
448
  * The escalated leg runs as its own voice turn. Rather than re-persist the
330
449
  * caller's utterance (it is already in history from the front-door leg), the
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Tests for `assistant apps inspect` and `assistant apps refresh`.
3
+ */
4
+
5
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { join } from "node:path";
8
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
9
+
10
+ import { createApp, getAppDirPath } from "../../../apps/app-store.js";
11
+ import { writeAppSourceFingerprint } from "../../../apps/source-fingerprint.js";
12
+ import { getWorkspacePluginsDir } from "../../../util/platform.js";
13
+ import { runCliCommand } from "./cli-test-harness.js";
14
+
15
+ let lastIpcCall: {
16
+ method: string;
17
+ params?: Record<string, unknown>;
18
+ } | null = null;
19
+
20
+ let mockIpcResult: {
21
+ ok: boolean;
22
+ result?: unknown;
23
+ error?: string;
24
+ statusCode?: number;
25
+ } = { ok: true, result: { ok: true, compiled: true } };
26
+
27
+ mock.module("../../../ipc/cli-client.js", () => ({
28
+ cliIpcCall: async (method: string, params?: Record<string, unknown>) => {
29
+ lastIpcCall = { method, params };
30
+ return mockIpcResult;
31
+ },
32
+ exitCodeFromIpcResult: (r: { statusCode?: number }) => {
33
+ if (r.statusCode === undefined) {
34
+ return 10;
35
+ }
36
+ if (r.statusCode >= 500) {
37
+ return 3;
38
+ }
39
+ if (r.statusCode >= 400) {
40
+ return 2;
41
+ }
42
+ return 1;
43
+ },
44
+ }));
45
+
46
+ const { registerAppsCommand } = await import("../apps.js");
47
+
48
+ let workspaceDir: string;
49
+ let previousWorkspaceDir: string | undefined;
50
+
51
+ function freshWorkspace(): string {
52
+ return join(
53
+ tmpdir(),
54
+ `vellum-apps-cli-${Date.now()}-${Math.random().toString(36).slice(2)}`,
55
+ );
56
+ }
57
+
58
+ function uniqueAppName(prefix: string): string {
59
+ return `${prefix} ${Math.random().toString(36).slice(2, 8)}`;
60
+ }
61
+
62
+ beforeEach(() => {
63
+ previousWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR;
64
+ workspaceDir = freshWorkspace();
65
+ process.env.VELLUM_WORKSPACE_DIR = workspaceDir;
66
+ lastIpcCall = null;
67
+ mockIpcResult = {
68
+ ok: true,
69
+ result: {
70
+ ok: true,
71
+ appId: "app-1",
72
+ name: "Budget",
73
+ compiled: true,
74
+ compile_duration_ms: 12,
75
+ },
76
+ };
77
+ });
78
+
79
+ afterEach(() => {
80
+ rmSync(workspaceDir, { recursive: true, force: true });
81
+ if (previousWorkspaceDir === undefined) {
82
+ delete process.env.VELLUM_WORKSPACE_DIR;
83
+ } else {
84
+ process.env.VELLUM_WORKSPACE_DIR = previousWorkspaceDir;
85
+ }
86
+ });
87
+
88
+ function parseJson(stdout: string): Record<string, unknown> {
89
+ return JSON.parse(stdout.trim()) as Record<string, unknown>;
90
+ }
91
+
92
+ describe("assistant apps inspect", () => {
93
+ test("reports never_compiled for a new workspace app", async () => {
94
+ const name = uniqueAppName("Inspect Fresh");
95
+ const created = createApp({
96
+ name,
97
+ schemaJson: "{}",
98
+ htmlDefinition: "<h1>Budget</h1>",
99
+ });
100
+
101
+ const result = await runCliCommand(registerAppsCommand, [
102
+ "apps",
103
+ "inspect",
104
+ created.id,
105
+ "--json",
106
+ ]);
107
+
108
+ expect(result.exitCode).toBe(0);
109
+ const body = parseJson(result.stdout);
110
+ expect(body.ok).toBe(true);
111
+ const compile = body.compile as { status: string };
112
+ expect(compile.status).toBe("never_compiled");
113
+ });
114
+
115
+ test("reports stale after source changes since the last fingerprint", async () => {
116
+ const name = uniqueAppName("Inspect Stale");
117
+ const created = createApp({
118
+ name,
119
+ schemaJson: "{}",
120
+ htmlDefinition: "<h1>Budget</h1>",
121
+ });
122
+ const appDir = getAppDirPath(created.id);
123
+ expect(appDir.length).toBeGreaterThan(0);
124
+ mkdirSync(join(appDir, "src"), { recursive: true });
125
+ writeFileSync(join(appDir, "src", "main.tsx"), "export const n = 1;\n");
126
+ mkdirSync(join(appDir, "dist"), { recursive: true });
127
+ writeFileSync(join(appDir, "dist", "index.html"), "<html></html>");
128
+ writeAppSourceFingerprint(appDir, join(appDir, "dist"));
129
+ writeFileSync(join(appDir, "src", "main.tsx"), "export const n = 2;\n");
130
+
131
+ const result = await runCliCommand(registerAppsCommand, [
132
+ "apps",
133
+ "inspect",
134
+ created.id,
135
+ "--json",
136
+ ]);
137
+
138
+ expect(result.exitCode).toBe(0);
139
+ const body = parseJson(result.stdout);
140
+ const app = body.app as { id: string; source: string };
141
+ expect(app.id).toBe(created.id);
142
+ expect(app.source).toBe(appDir);
143
+ const compile = body.compile as {
144
+ status: string;
145
+ modified: string[];
146
+ };
147
+ expect(compile.status).toBe("stale");
148
+ expect(compile.modified).toEqual(["src/main.tsx"]);
149
+ });
150
+
151
+ test("exits 1 for an unknown app", async () => {
152
+ const result = await runCliCommand(registerAppsCommand, [
153
+ "apps",
154
+ "inspect",
155
+ "missing",
156
+ "--json",
157
+ ]);
158
+ expect(result.exitCode).toBe(1);
159
+ const body = parseJson(result.stdout);
160
+ expect(body.ok).toBe(false);
161
+ expect(String(body.error)).toContain("assistant apps list");
162
+ });
163
+ });
164
+
165
+ describe("assistant apps refresh", () => {
166
+ test("sends apps_refresh IPC for a workspace app", async () => {
167
+ const name = uniqueAppName("Refresh");
168
+ const created = createApp({
169
+ name,
170
+ schemaJson: "{}",
171
+ htmlDefinition: "<h1>Budget</h1>",
172
+ });
173
+
174
+ const result = await runCliCommand(registerAppsCommand, [
175
+ "apps",
176
+ "refresh",
177
+ created.id,
178
+ "--json",
179
+ ]);
180
+
181
+ expect(result.exitCode).toBe(0);
182
+ expect(lastIpcCall).toEqual({
183
+ method: "apps_refresh",
184
+ params: { pathParams: { id: created.id } },
185
+ });
186
+ const body = parseJson(result.stdout);
187
+ expect(body.compiled).toBe(true);
188
+ });
189
+
190
+ test("sends apps_refresh IPC for a plugin app", async () => {
191
+ const pluginName = `charts-${Math.random().toString(36).slice(2, 8)}`;
192
+ const pluginDir = join(getWorkspacePluginsDir(), pluginName);
193
+ mkdirSync(pluginDir, { recursive: true });
194
+ writeFileSync(
195
+ join(pluginDir, "package.json"),
196
+ JSON.stringify({ name: pluginName, version: "1.0.0" }),
197
+ );
198
+ mkdirSync(join(pluginDir, "apps", "viewer"), { recursive: true });
199
+ writeFileSync(join(pluginDir, "apps", "viewer", "index.html"), "<h1></h1>");
200
+
201
+ const result = await runCliCommand(registerAppsCommand, [
202
+ "apps",
203
+ "refresh",
204
+ "viewer",
205
+ "--json",
206
+ ]);
207
+
208
+ expect(result.exitCode).toBe(0);
209
+ expect(lastIpcCall).toEqual({
210
+ method: "apps_refresh",
211
+ params: { pathParams: { id: `plugins~${pluginName}~viewer` } },
212
+ });
213
+ });
214
+
215
+ test("exits 1 when compile fails", async () => {
216
+ const name = uniqueAppName("Refresh Fail");
217
+ const created = createApp({
218
+ name,
219
+ schemaJson: "{}",
220
+ htmlDefinition: "<h1>Budget</h1>",
221
+ });
222
+ mockIpcResult = {
223
+ ok: true,
224
+ result: {
225
+ ok: true,
226
+ appId: created.id,
227
+ name,
228
+ compiled: false,
229
+ compile_duration_ms: 8,
230
+ compile_errors: [{ text: "Could not resolve foo" }],
231
+ },
232
+ };
233
+
234
+ const result = await runCliCommand(registerAppsCommand, [
235
+ "apps",
236
+ "refresh",
237
+ created.id,
238
+ "--json",
239
+ ]);
240
+
241
+ expect(result.exitCode).toBe(1);
242
+ const body = parseJson(result.stdout);
243
+ expect(body.compiled).toBe(false);
244
+ });
245
+ });
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Tests for `assistant bash`: local sanitized spawn, no IPC round-trip.
3
+ */
4
+
5
+ import { describe, expect, mock, test } from "bun:test";
6
+
7
+ import { runCliCommand } from "./cli-test-harness.js";
8
+
9
+ const ipcCalls: Array<{ method: string }> = [];
10
+
11
+ mock.module("../../../ipc/cli-client.js", () => ({
12
+ cliIpcCall: async (method: string) => {
13
+ ipcCalls.push({ method });
14
+ return { ok: false, error: "IPC should not be used" };
15
+ },
16
+ }));
17
+
18
+ mock.module("../../logger.js", () => ({
19
+ log: {
20
+ info: () => {},
21
+ warn: () => {},
22
+ error: (message: string) => {
23
+ process.stderr.write(`${message}\n`);
24
+ },
25
+ debug: () => {},
26
+ },
27
+ }));
28
+
29
+ const { registerBashCommand } = await import("../bash.js");
30
+
31
+ describe("assistant bash", () => {
32
+ test("spawns locally and prints stdout without IPC", async () => {
33
+ ipcCalls.length = 0;
34
+ const result = await runCliCommand(registerBashCommand, [
35
+ "bash",
36
+ "echo hello",
37
+ ]);
38
+ expect(ipcCalls).toEqual([]);
39
+ expect(result.stdout.trim()).toBe("hello");
40
+ expect(result.exitCode).toBe(0);
41
+ });
42
+
43
+ test("rejects a non-positive timeout", async () => {
44
+ const result = await runCliCommand(registerBashCommand, [
45
+ "bash",
46
+ "echo hello",
47
+ "--timeout",
48
+ "0",
49
+ ]);
50
+ expect(result.stderr).toContain("Invalid timeout value");
51
+ expect(result.exitCode).toBe(1);
52
+ });
53
+ });
@@ -0,0 +1,19 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { roadmapHelp } from "../roadmap.help.js";
4
+
5
+ const DISCORD_URL = "https://vellum.ai/community";
6
+
7
+ describe("roadmap help is feature-proposal-only", () => {
8
+ test("top-level help describes feature proposals and routes bugs to Discord", () => {
9
+ const description = roadmapHelp.description;
10
+ const helpText = roadmapHelp.helpText ?? "";
11
+ const combined = `${description}\n${helpText}`.toLowerCase();
12
+
13
+ expect(combined).toMatch(/feature proposals?/);
14
+ expect(description.split(DISCORD_URL)).toHaveLength(2);
15
+ expect(helpText).toContain(DISCORD_URL);
16
+ expect(combined).toContain("instruct the user");
17
+ expect(combined).not.toMatch(/support@|github\.com|share feedback/);
18
+ });
19
+ });