@vellumai/assistant 0.12.0 → 0.12.1-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 (406) hide show
  1. package/AGENTS.md +5 -1
  2. package/ARCHITECTURE.md +11 -12
  3. package/docs/architecture/turn-actor.md +15 -8
  4. package/openapi.yaml +292 -2
  5. package/package.json +1 -1
  6. package/scripts/smoke-desktop-window-theme.ts +186 -0
  7. package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
  8. package/src/__tests__/app-compiler.test.ts +6 -0
  9. package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
  10. package/src/__tests__/app-dir-path-guard.test.ts +1 -3
  11. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
  12. package/src/__tests__/computer-use-tools.test.ts +17 -5
  13. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  14. package/src/__tests__/config-schema.test.ts +28 -29
  15. package/src/__tests__/config-watcher.test.ts +23 -8
  16. package/src/__tests__/connection-model-compat.test.ts +2 -2
  17. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  18. package/src/__tests__/conversation-pairing.test.ts +26 -0
  19. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
  20. package/src/__tests__/conversation-queue.test.ts +48 -207
  21. package/src/__tests__/conversation-retry-discard.test.ts +54 -1
  22. package/src/__tests__/conversation-skill-tools.test.ts +27 -0
  23. package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
  24. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  25. package/src/__tests__/conversation-title-service.test.ts +14 -10
  26. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
  27. package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
  28. package/src/__tests__/credential-record-retire.test.ts +171 -1
  29. package/src/__tests__/credential-routes.test.ts +212 -3
  30. package/src/__tests__/credential-security-invariants.test.ts +2 -1
  31. package/src/__tests__/cu-unified-flow.test.ts +8 -2
  32. package/src/__tests__/drain-kick-guard.test.ts +0 -2
  33. package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
  34. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  35. package/src/__tests__/git-porcelain.test.ts +47 -0
  36. package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
  37. package/src/__tests__/host-bash-proxy.test.ts +26 -45
  38. package/src/__tests__/host-browser-proxy.test.ts +6 -17
  39. package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
  40. package/src/__tests__/host-file-proxy.test.ts +11 -23
  41. package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
  42. package/src/__tests__/llm-catalog-parity.test.ts +25 -0
  43. package/src/__tests__/mcp-auth-routes.test.ts +6 -9
  44. package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
  45. package/src/__tests__/mcp-health-check.test.ts +8 -11
  46. package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
  47. package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
  48. package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
  49. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  50. package/src/__tests__/messaging-send-tool.test.ts +355 -548
  51. package/src/__tests__/model-intents.test.ts +3 -0
  52. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  53. package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
  54. package/src/__tests__/plugin-import-boundary-guard.test.ts +13 -0
  55. package/src/__tests__/resolve-app.test.ts +135 -0
  56. package/src/__tests__/script-proxy-certs.test.ts +35 -0
  57. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  58. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  59. package/src/__tests__/source-fingerprint.test.ts +184 -0
  60. package/src/__tests__/subagent-tool-gate-mode.test.ts +19 -3
  61. package/src/__tests__/subagent-tools.test.ts +3 -2
  62. package/src/__tests__/telephony-tts-guard.test.ts +38 -3
  63. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
  64. package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
  65. package/src/__tests__/workspace-git-service.test.ts +26 -0
  66. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
  67. package/src/acp/__tests__/agent-process.test.ts +258 -4
  68. package/src/acp/__tests__/auth-required.test.ts +96 -0
  69. package/src/acp/__tests__/client-handler.test.ts +90 -2
  70. package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
  71. package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
  72. package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
  73. package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
  74. package/src/acp/__tests__/session-manager.test.ts +895 -4
  75. package/src/acp/agent-process.test.ts +4 -1
  76. package/src/acp/agent-process.ts +94 -9
  77. package/src/acp/auth-required.ts +50 -1
  78. package/src/acp/auto-install.test.ts +9 -7
  79. package/src/acp/client-handler.ts +16 -3
  80. package/src/acp/model-config.test.ts +166 -0
  81. package/src/acp/model-config.ts +138 -0
  82. package/src/acp/resolve-agent.test.ts +220 -6
  83. package/src/acp/resolve-agent.ts +88 -23
  84. package/src/acp/session-manager.test.ts +196 -3
  85. package/src/acp/session-manager.ts +441 -18
  86. package/src/acp/types.ts +50 -5
  87. package/src/api/events/acp-session-model-update.test.ts +88 -0
  88. package/src/api/events/acp-session-model-update.ts +46 -0
  89. package/src/api/events/app-files-changed.ts +4 -3
  90. package/src/api/index.ts +6 -0
  91. package/src/apps/app-store.ts +1 -1
  92. package/src/apps/resolve-app.ts +81 -0
  93. package/src/apps/source-fingerprint.ts +266 -0
  94. package/src/bundler/app-compiler.ts +15 -0
  95. package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
  96. package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
  97. package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
  98. package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
  99. package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
  100. package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
  101. package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
  102. package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
  103. package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
  104. package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
  105. package/src/calls/__tests__/voice-control-protocol.test.ts +58 -25
  106. package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
  107. package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
  108. package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
  109. package/src/calls/__tests__/voice-session-bridge.test.ts +37 -1
  110. package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
  111. package/src/calls/barge-in-guard.ts +118 -0
  112. package/src/calls/call-constants.ts +24 -4
  113. package/src/calls/call-controller.ts +499 -199
  114. package/src/calls/call-transport.ts +16 -0
  115. package/src/calls/media-stream-output.ts +32 -0
  116. package/src/calls/media-stream-server.ts +82 -30
  117. package/src/calls/media-stream-stt-session.ts +15 -0
  118. package/src/{live-voice → calls}/progress-narration.ts +2 -2
  119. package/src/calls/resolve-call-tts-provider.ts +33 -2
  120. package/src/calls/tts-call-strategy.ts +9 -5
  121. package/src/calls/types.ts +2 -0
  122. package/src/calls/voice-control-protocol.ts +53 -21
  123. package/src/calls/voice-leg-coordinator.ts +189 -0
  124. package/src/calls/voice-progress-cadence.ts +423 -0
  125. package/src/calls/voice-session-bridge.ts +59 -46
  126. package/src/calls/voice-triage-escalate.ts +175 -56
  127. package/src/cli/commands/__tests__/apps.test.ts +245 -0
  128. package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
  129. package/src/cli/commands/apps.help.ts +67 -13
  130. package/src/cli/commands/apps.ts +183 -0
  131. package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
  132. package/src/cli/commands/channels/index.help.ts +70 -2
  133. package/src/cli/commands/channels/index.ts +16 -3
  134. package/src/cli/commands/channels/send.ts +128 -0
  135. package/src/cli/commands/credentials.help.ts +6 -2
  136. package/src/cli/commands/credentials.ts +2 -2
  137. package/src/cli/commands/mcp.help.ts +8 -8
  138. package/src/cli/commands/mcp.ts +1 -1
  139. package/src/cli/commands/roadmap.help.ts +5 -1
  140. package/src/cli/commands/roadmap.ts +1 -1
  141. package/src/cli/lib/bundled-marketplace.json +103 -90
  142. package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
  143. package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
  144. package/src/config/__tests__/platform-identity.test.ts +117 -0
  145. package/src/config/__tests__/platform-rehydration.test.ts +93 -0
  146. package/src/config/acp-agent-write.ts +43 -0
  147. package/src/config/acp-defaults.test.ts +13 -3
  148. package/src/config/acp-defaults.ts +23 -6
  149. package/src/config/acp-schema.test.ts +81 -0
  150. package/src/config/acp-schema.ts +30 -2
  151. package/src/config/bundled-skills/acp/SKILL.md +22 -14
  152. package/src/config/bundled-skills/acp/TOOLS.json +5 -1
  153. package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
  154. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  155. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  156. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
  157. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  158. package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
  159. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
  160. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  161. package/src/config/default-profile-catalog.ts +1 -1
  162. package/src/config/env.ts +2 -2
  163. package/src/config/feature-flag-registry.json +8 -12
  164. package/src/config/memory-v3-gate.test.ts +31 -0
  165. package/src/config/memory-v3-gate.ts +20 -16
  166. package/src/config/platform-identity.ts +144 -0
  167. package/src/config/platform-rehydration.ts +75 -21
  168. package/src/config/schema.ts +2 -2
  169. package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
  170. package/src/config/schemas/__tests__/voice.test.ts +148 -0
  171. package/src/config/schemas/calls.ts +1 -28
  172. package/src/config/schemas/live-voice.ts +1 -173
  173. package/src/config/schemas/mcp.ts +1 -1
  174. package/src/config/schemas/memory-retrospective.ts +10 -0
  175. package/src/config/schemas/skills.ts +0 -14
  176. package/src/config/schemas/voice.ts +150 -0
  177. package/src/credential-execution/process-manager.test.ts +18 -19
  178. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
  179. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
  180. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
  181. package/src/daemon/app-change-notify.ts +45 -0
  182. package/src/daemon/config-watcher.ts +13 -194
  183. package/src/daemon/conversation-process.ts +35 -64
  184. package/src/daemon/conversation-skill-tools.ts +13 -31
  185. package/src/daemon/conversation-store.ts +1 -1
  186. package/src/daemon/conversation-surfaces.ts +10 -8
  187. package/src/daemon/conversation-tool-setup.ts +37 -43
  188. package/src/daemon/conversation.ts +0 -3
  189. package/src/daemon/host-bash-proxy.ts +8 -12
  190. package/src/daemon/host-browser-proxy.ts +15 -1
  191. package/src/daemon/host-file-proxy.ts +8 -10
  192. package/src/daemon/host-transfer-proxy.ts +14 -18
  193. package/src/daemon/lifecycle.ts +0 -5
  194. package/src/daemon/mcp-reload-service.ts +5 -5
  195. package/src/daemon/message-types/sync.ts +1 -0
  196. package/src/daemon/providers-setup.ts +1 -1
  197. package/src/daemon/shutdown-handlers.ts +1 -9
  198. package/src/daemon/skill-memory-refresh.ts +64 -0
  199. package/src/daemon/tool-side-effects.ts +7 -18
  200. package/src/desktop/__tests__/fake-desktop.ts +119 -0
  201. package/src/desktop/desktop-chrome-policy.test.ts +71 -0
  202. package/src/desktop/desktop-chrome-policy.ts +42 -0
  203. package/src/desktop/desktop-dependencies.test.ts +102 -0
  204. package/src/desktop/desktop-dependencies.ts +306 -0
  205. package/src/desktop/desktop-feature.test.ts +18 -0
  206. package/src/desktop/desktop-feature.ts +19 -0
  207. package/src/desktop/desktop-panel-config.test.ts +135 -0
  208. package/src/desktop/desktop-panel-config.ts +149 -0
  209. package/src/desktop/desktop-session-manager.test.ts +627 -0
  210. package/src/desktop/desktop-session-manager.ts +814 -0
  211. package/src/desktop/desktop-stream-bridge.test.ts +395 -0
  212. package/src/desktop/desktop-stream-bridge.ts +214 -0
  213. package/src/desktop/desktop-wallpaper.test.ts +191 -0
  214. package/src/desktop/desktop-wallpaper.ts +113 -0
  215. package/src/desktop/desktop-window-theme.ts +170 -0
  216. package/src/hooks/types.ts +33 -0
  217. package/src/i18n/AGENTS.md +22 -0
  218. package/src/i18n/__tests__/i18n.test.ts +108 -0
  219. package/src/i18n/index.ts +16 -0
  220. package/src/i18n/locales.ts +93 -0
  221. package/src/i18n/messages.ts +61 -0
  222. package/src/i18n/title.ts +25 -0
  223. package/src/inbound/platform-callback-registration.ts +4 -5
  224. package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
  225. package/src/live-voice/__tests__/live-voice-progress.test.ts +15 -15
  226. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  227. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
  228. package/src/live-voice/live-voice-session.ts +213 -693
  229. package/src/live-voice/protocol.ts +0 -2
  230. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  231. package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
  232. package/src/mcp/effective-config.ts +3 -3
  233. package/src/mcp/mcp-header-store.ts +29 -45
  234. package/src/mcp/spec-schema.ts +111 -0
  235. package/src/mcp/startup.ts +8 -4
  236. package/src/mcp/workspace-mcp-config.ts +188 -0
  237. package/src/messaging/provider.ts +10 -3
  238. package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
  239. package/src/messaging/providers/callback-routing.ts +18 -0
  240. package/src/messaging/providers/channel-transport.ts +56 -0
  241. package/src/messaging/providers/discord/transport.ts +17 -0
  242. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
  243. package/src/messaging/providers/slack/adapter.ts +7 -44
  244. package/src/messaging/providers/slack/transport.ts +16 -0
  245. package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
  246. package/src/messaging/providers/telegram-bot/client.ts +0 -26
  247. package/src/messaging/providers/telegram-bot/transport.ts +20 -0
  248. package/src/messaging/providers/whatsapp/adapter.ts +1 -48
  249. package/src/messaging/providers/whatsapp/transport.ts +18 -0
  250. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
  251. package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
  252. package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
  253. package/src/notifications/assistant-reply-producer.ts +8 -3
  254. package/src/notifications/conversation-pairing.ts +36 -16
  255. package/src/notifications/delivered-post-record.ts +11 -1
  256. package/src/notifications/home-feed-side-effect.ts +55 -7
  257. package/src/outbound-proxy/certs.ts +3 -5
  258. package/src/permissions/checker.test.ts +11 -6
  259. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  260. package/src/persistence/conversation-crud.ts +71 -63
  261. package/src/persistence/conversation-lineage.ts +12 -4
  262. package/src/persistence/conversation-title-placeholders.ts +23 -0
  263. package/src/persistence/conversation-title-service.ts +9 -26
  264. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  265. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  266. package/src/persistence/message-cursor.ts +78 -0
  267. package/src/persistence/message-reads.ts +30 -0
  268. package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
  269. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
  270. package/src/persistence/schema/memory-core.ts +5 -0
  271. package/src/platform/client.test.ts +2 -0
  272. package/src/platform/client.ts +2 -10
  273. package/src/plugin-api/constants.ts +2 -0
  274. package/src/plugin-api/index.ts +1 -0
  275. package/src/plugin-api/types.ts +2 -0
  276. package/src/plugins/defaults/index.ts +2 -0
  277. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
  278. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
  279. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
  280. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
  281. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
  282. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
  283. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
  284. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
  285. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
  286. package/src/plugins/defaults/memory/graph/capability-seed.ts +4 -6
  287. package/src/plugins/defaults/memory/hooks/init.ts +10 -0
  288. package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
  289. package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
  290. package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
  291. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
  292. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
  293. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
  294. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
  295. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
  296. package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
  297. package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
  298. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
  299. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
  300. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
  301. package/src/plugins/defaults/memory/startup.ts +6 -1
  302. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
  303. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  304. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +89 -0
  305. package/src/plugins/defaults/memory/worker.ts +6 -0
  306. package/src/plugins/mcp-servers.ts +15 -75
  307. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  308. package/src/providers/gemini/client.ts +9 -23
  309. package/src/providers/gemini-thought-signature.test.ts +63 -0
  310. package/src/providers/gemini-thought-signature.ts +44 -0
  311. package/src/providers/model-catalog.ts +34 -0
  312. package/src/providers/model-intents.ts +10 -5
  313. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
  314. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
  315. package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
  316. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
  317. package/src/providers/openai/chat-completions-provider.ts +192 -78
  318. package/src/providers/openai/codex-models.ts +0 -4
  319. package/src/providers/openai/google-thought-signature.ts +320 -0
  320. package/src/runtime/AGENTS.md +2 -0
  321. package/src/runtime/__tests__/channel-send.test.ts +443 -0
  322. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
  323. package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
  324. package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
  325. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
  326. package/src/runtime/auth/same-actor.ts +38 -3
  327. package/src/runtime/channel-send.ts +316 -0
  328. package/src/runtime/http-server.ts +160 -151
  329. package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
  330. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +205 -0
  331. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
  332. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
  333. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
  334. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
  335. package/src/runtime/routes/acp-routes.test.ts +15 -0
  336. package/src/runtime/routes/acp-routes.ts +72 -9
  337. package/src/runtime/routes/app-management-routes.ts +73 -0
  338. package/src/runtime/routes/app-routes.ts +1 -1
  339. package/src/runtime/routes/channel-send-routes.ts +85 -0
  340. package/src/runtime/routes/conversation-list-routes.ts +14 -3
  341. package/src/runtime/routes/conversation-query-routes.ts +40 -0
  342. package/src/runtime/routes/credential-routes.ts +19 -10
  343. package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
  344. package/src/runtime/routes/desktop-setup-routes.ts +35 -0
  345. package/src/runtime/routes/diagnostics-routes.ts +55 -3
  346. package/src/runtime/routes/index.ts +4 -0
  347. package/src/runtime/routes/log-export-routes.ts +2 -0
  348. package/src/runtime/routes/mcp-auth-routes.ts +39 -47
  349. package/src/runtime/routes/migration-routes.ts +8 -21
  350. package/src/runtime/routes/platform-routes.ts +18 -15
  351. package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
  352. package/src/runtime/services/conversation-serializer.ts +16 -3
  353. package/src/runtime/sync/resource-sync-events.ts +2 -1
  354. package/src/schedule/worker.ts +1 -1
  355. package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
  356. package/src/security/ces-http-record-backend.ts +120 -0
  357. package/src/security/ces-rpc-record-backend.ts +7 -0
  358. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
  359. package/src/skills/workspace-skill-md-signature.ts +50 -0
  360. package/src/subagent/consult-context.ts +12 -25
  361. package/src/tools/acp/list-agents.test.ts +3 -1
  362. package/src/tools/acp/spawn.test.ts +146 -11
  363. package/src/tools/acp/spawn.ts +21 -3
  364. package/src/tools/acp/status.test.ts +94 -0
  365. package/src/tools/acp/status.ts +40 -3
  366. package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
  367. package/src/tools/capability-offer.test.ts +22 -2
  368. package/src/tools/capability-offer.ts +16 -1
  369. package/src/tools/computer-use/definitions.ts +4 -2
  370. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
  371. package/src/tools/credentials/metadata-store.ts +100 -0
  372. package/src/tools/host-filesystem/edit.test.ts +2 -1
  373. package/src/tools/host-filesystem/edit.ts +2 -2
  374. package/src/tools/host-filesystem/read.test.ts +2 -1
  375. package/src/tools/host-filesystem/read.ts +2 -2
  376. package/src/tools/host-filesystem/transfer.test.ts +2 -1
  377. package/src/tools/host-filesystem/transfer.ts +2 -2
  378. package/src/tools/host-filesystem/write.test.ts +2 -1
  379. package/src/tools/host-filesystem/write.ts +2 -2
  380. package/src/tools/host-terminal/host-shell.test.ts +16 -0
  381. package/src/tools/host-terminal/host-shell.ts +4 -20
  382. package/src/tools/policy-context.ts +6 -7
  383. package/src/tools/subagent/spawn.ts +5 -8
  384. package/src/tools/terminal/__tests__/safe-env.test.ts +6 -6
  385. package/src/tools/terminal/safe-env.ts +23 -3
  386. package/src/tools/watch/watch-retro-report.ts +0 -8
  387. package/src/util/__tests__/language-subtag.test.ts +23 -1
  388. package/src/util/ca-bundle.ts +13 -0
  389. package/src/util/language-subtag.ts +16 -0
  390. package/src/util/platform.ts +8 -0
  391. package/src/workspace/git-porcelain.ts +66 -0
  392. package/src/workspace/git-service.ts +5 -54
  393. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
  394. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
  395. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
  396. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
  397. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
  398. package/src/workspace/migrations/registry.ts +6 -0
  399. package/src/__tests__/app-source-watcher.test.ts +0 -218
  400. package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
  401. package/src/__tests__/speaker-identification.test.ts +0 -56
  402. package/src/calls/speaker-identification.ts +0 -233
  403. package/src/daemon/app-source-watcher.ts +0 -218
  404. package/src/messaging/providers/whatsapp/client.ts +0 -29
  405. package/src/util/fs-watcher-error.ts +0 -36
  406. /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Bound platform identity (assistant, organization, user).
3
+ *
4
+ * Django maps an assistant API key to these ids via
5
+ * `POST /v1/internal/assistants/validate/`. Process startup rehydrates from
6
+ * that endpoint. Resolution prefers the in-memory ids, then the vault
7
+ * copies of `vellum:platform_*`.
8
+ */
9
+
10
+ import { credentialKey } from "../security/credential-key.js";
11
+ import { getSecureKeyAsync } from "../security/secure-keys.js";
12
+ import { getLogger } from "../util/logger.js";
13
+ import {
14
+ getPlatformAssistantId,
15
+ getPlatformOrganizationId,
16
+ getPlatformUserId,
17
+ setPlatformAssistantId,
18
+ setPlatformOrganizationId,
19
+ setPlatformUserId,
20
+ } from "./env.js";
21
+
22
+ const log = getLogger("platform-identity");
23
+
24
+ export const PLATFORM_IDENTITY_VALIDATE_PATH =
25
+ "/v1/internal/assistants/validate/";
26
+
27
+ const VALIDATE_TIMEOUT_MS = 5_000;
28
+
29
+ export type PlatformIdentityIds = {
30
+ assistantId: string;
31
+ organizationId: string;
32
+ userId: string;
33
+ };
34
+
35
+ function asNonEmptyString(value: unknown): string | undefined {
36
+ if (typeof value !== "string") {
37
+ return undefined;
38
+ }
39
+ const trimmed = value.trim();
40
+ return trimmed.length > 0 ? trimmed : undefined;
41
+ }
42
+
43
+ export function applyPlatformIdentityIds(ids: PlatformIdentityIds): void {
44
+ if (ids.assistantId) {
45
+ setPlatformAssistantId(ids.assistantId);
46
+ }
47
+ if (ids.organizationId) {
48
+ setPlatformOrganizationId(ids.organizationId);
49
+ }
50
+ if (ids.userId) {
51
+ setPlatformUserId(ids.userId);
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Trade an assistant API key for the bound platform ids.
57
+ *
58
+ * Returns null when the platform is unreachable, refuses the key, or the
59
+ * body has no ids. Never throws.
60
+ */
61
+ export async function fetchPlatformIdentityIds(
62
+ baseUrl: string,
63
+ apiKey: string,
64
+ ): Promise<PlatformIdentityIds | null> {
65
+ const url = `${baseUrl.replace(/\/+$/, "")}${PLATFORM_IDENTITY_VALIDATE_PATH}`;
66
+ try {
67
+ const res = await fetch(url, {
68
+ method: "POST",
69
+ headers: {
70
+ Authorization: `Api-Key ${apiKey}`,
71
+ "Content-Type": "application/json",
72
+ },
73
+ body: "{}",
74
+ signal: AbortSignal.timeout(VALIDATE_TIMEOUT_MS),
75
+ });
76
+ if (!res.ok) {
77
+ log.warn(
78
+ { status: res.status },
79
+ "platform identity validate returned non-2xx",
80
+ );
81
+ return null;
82
+ }
83
+ const body: unknown = await res.json();
84
+ if (!body || typeof body !== "object") {
85
+ return null;
86
+ }
87
+ const obj = body as Record<string, unknown>;
88
+ const assistantId = asNonEmptyString(obj.assistant_id);
89
+ const organizationId = asNonEmptyString(obj.organization_id);
90
+ const userId = asNonEmptyString(obj.user_id);
91
+ if (!assistantId && !organizationId && !userId) {
92
+ return null;
93
+ }
94
+ return {
95
+ assistantId: assistantId ?? "",
96
+ organizationId: organizationId ?? "",
97
+ userId: userId ?? "",
98
+ };
99
+ } catch (err) {
100
+ log.warn({ err }, "platform identity validate failed");
101
+ return null;
102
+ }
103
+ }
104
+
105
+ async function readVaultField(field: string): Promise<string> {
106
+ try {
107
+ return (
108
+ (await getSecureKeyAsync(credentialKey("vellum", field)))?.trim() ?? ""
109
+ );
110
+ } catch (err) {
111
+ log.warn(
112
+ { err, field },
113
+ "failed to read platform identity from credential store",
114
+ );
115
+ return "";
116
+ }
117
+ }
118
+
119
+ export async function resolvePlatformAssistantId(): Promise<string> {
120
+ return (
121
+ getPlatformAssistantId().trim() ||
122
+ (await readVaultField("platform_assistant_id"))
123
+ );
124
+ }
125
+
126
+ export async function resolvePlatformAssistantIdOrNull(): Promise<
127
+ string | null
128
+ > {
129
+ const id = await resolvePlatformAssistantId();
130
+ return id || null;
131
+ }
132
+
133
+ export async function resolvePlatformOrganizationId(): Promise<string> {
134
+ return (
135
+ getPlatformOrganizationId().trim() ||
136
+ (await readVaultField("platform_organization_id"))
137
+ );
138
+ }
139
+
140
+ export async function resolvePlatformUserId(): Promise<string> {
141
+ return (
142
+ getPlatformUserId().trim() || (await readVaultField("platform_user_id"))
143
+ );
144
+ }
@@ -1,6 +1,5 @@
1
1
  /**
2
- * Rehydrate the in-memory platform identity/URL overrides from the credential
3
- * store at process startup.
2
+ * Rehydrate the in-memory platform identity/URL overrides at process startup.
4
3
  *
5
4
  * These overrides (`setPlatformBaseUrl` and the platform ID setters in
6
5
  * `config/env.ts`) are normally only populated at runtime by the secret-routes
@@ -10,6 +9,10 @@
10
9
  * environment default (e.g. dev-platform) while the credential store holds the
11
10
  * real production values, and requests are sent to the wrong environment.
12
11
  *
12
+ * Bound platform ids come from `POST /v1/internal/assistants/validate/`,
13
+ * which trades the assistant API key for assistant/organization/user ids.
14
+ * If validate omits an id, that field is read from the credential store.
15
+ *
13
16
  * Each field is best-effort: a credential-store read failure is logged and
14
17
  * skipped so a single missing value never blocks startup.
15
18
  */
@@ -17,11 +20,19 @@ import { credentialKey } from "../security/credential-key.js";
17
20
  import { getSecureKeyAsync } from "../security/secure-keys.js";
18
21
  import { getLogger } from "../util/logger.js";
19
22
  import {
23
+ getPlatformAssistantId,
24
+ getPlatformBaseUrl,
25
+ getPlatformOrganizationId,
26
+ getPlatformUserId,
20
27
  setPlatformAssistantId,
21
28
  setPlatformBaseUrl,
22
29
  setPlatformOrganizationId,
23
30
  setPlatformUserId,
24
31
  } from "./env.js";
32
+ import {
33
+ applyPlatformIdentityIds,
34
+ fetchPlatformIdentityIds,
35
+ } from "./platform-identity.js";
25
36
 
26
37
  const log = getLogger("platform-rehydration");
27
38
 
@@ -45,32 +56,75 @@ async function rehydrateField(
45
56
  }
46
57
  }
47
58
 
59
+ async function readAssistantApiKey(): Promise<string> {
60
+ try {
61
+ const stored = (
62
+ await getSecureKeyAsync(credentialKey("vellum", "assistant_api_key"))
63
+ )?.trim();
64
+ if (stored) {
65
+ return stored;
66
+ }
67
+ } catch (err) {
68
+ log.warn(
69
+ { error: err instanceof Error ? err.message : String(err) },
70
+ "Failed to read assistant API key from credential store (non-fatal)",
71
+ );
72
+ }
73
+ return process.env.ASSISTANT_API_KEY?.trim() ?? "";
74
+ }
75
+
76
+ async function rehydrateIdentityFromValidate(): Promise<boolean> {
77
+ const apiKey = await readAssistantApiKey();
78
+ if (!apiKey) {
79
+ return false;
80
+ }
81
+ const baseUrl = getPlatformBaseUrl();
82
+ if (!baseUrl) {
83
+ return false;
84
+ }
85
+ const ids = await fetchPlatformIdentityIds(baseUrl, apiKey);
86
+ if (!ids) {
87
+ return false;
88
+ }
89
+ applyPlatformIdentityIds(ids);
90
+ log.info("Rehydrated platform identity from platform validate");
91
+ return true;
92
+ }
93
+
48
94
  /**
49
95
  * Rehydrate the platform base URL and the related platform IDs (assistant,
50
- * organization, user) from the credential store into their in-memory
51
- * overrides. Safe to call more than once.
96
+ * organization, user). Safe to call more than once.
52
97
  */
53
98
  export async function rehydratePlatformCredentials(): Promise<void> {
54
- // Base URL first so managed proxy activation resolves the correct
55
- // environment for the ID lookups and every request that follows.
99
+ // Base URL first so managed proxy activation and identity validate resolve
100
+ // the correct environment for every request that follows.
56
101
  await rehydrateField(
57
102
  "platform_base_url",
58
103
  setPlatformBaseUrl,
59
104
  "platform base URL",
60
105
  );
61
- await rehydrateField(
62
- "platform_assistant_id",
63
- setPlatformAssistantId,
64
- "platform assistant ID",
65
- );
66
- await rehydrateField(
67
- "platform_organization_id",
68
- setPlatformOrganizationId,
69
- "platform organization ID",
70
- );
71
- await rehydrateField(
72
- "platform_user_id",
73
- setPlatformUserId,
74
- "platform user ID",
75
- );
106
+
107
+ await rehydrateIdentityFromValidate();
108
+
109
+ if (!getPlatformAssistantId()) {
110
+ await rehydrateField(
111
+ "platform_assistant_id",
112
+ setPlatformAssistantId,
113
+ "platform assistant ID",
114
+ );
115
+ }
116
+ if (!getPlatformOrganizationId()) {
117
+ await rehydrateField(
118
+ "platform_organization_id",
119
+ setPlatformOrganizationId,
120
+ "platform organization ID",
121
+ );
122
+ }
123
+ if (!getPlatformUserId()) {
124
+ await rehydrateField(
125
+ "platform_user_id",
126
+ setPlatformUserId,
127
+ "platform user ID",
128
+ );
129
+ }
76
130
  }
@@ -41,7 +41,6 @@ import {
41
41
  AuditLogConfigSchema,
42
42
  LogFileConfigSchema,
43
43
  } from "./schemas/logging.js";
44
- import { McpConfigSchema } from "./schemas/mcp.js";
45
44
  import { MemoryConfigSchema } from "./schemas/memory.js";
46
45
  import { MigrationsConfigSchema } from "./schemas/migrations.js";
47
46
  import { MonitoringConfigSchema } from "./schemas/monitoring.js";
@@ -62,6 +61,7 @@ import {
62
61
  } from "./schemas/timeouts.js";
63
62
  import { ToolsConfigSchema } from "./schemas/tools.js";
64
63
  import { UserRoutesConfigSchema } from "./schemas/user-routes.js";
64
+ import { VoiceConfigSchema } from "./schemas/voice.js";
65
65
  import { WorkflowsConfigSchema } from "./schemas/workflows.js";
66
66
  import { WorkspaceGitConfigSchema } from "./schemas/workspace-git.js";
67
67
 
@@ -108,7 +108,6 @@ export const AssistantConfigSchema = z.object({
108
108
  ),
109
109
  journal: JournalConfigSchema.default(JournalConfigSchema.parse({})),
110
110
  backup: BackupConfigSchema.default(BackupConfigSchema.parse({})),
111
- mcp: McpConfigSchema.default(McpConfigSchema.parse({})),
112
111
  acp: AcpConfigSchema.default(AcpConfigSchema.parse({})),
113
112
  skills: SkillsConfigSchema.default(SkillsConfigSchema.parse({})),
114
113
  workspaceGit: WorkspaceGitConfigSchema.default(
@@ -119,6 +118,7 @@ export const AssistantConfigSchema = z.object({
119
118
  twilio: TwilioConfigSchema.default(TwilioConfigSchema.parse({})),
120
119
  calls: CallsConfigSchema.default(CallsConfigSchema.parse({})),
121
120
  liveVoice: LiveVoiceConfigSchema.default(LiveVoiceConfigSchema.parse({})),
121
+ voice: VoiceConfigSchema.default(VoiceConfigSchema.parse({})),
122
122
  sight: SightConfigSchema.default(SightConfigSchema.parse({})),
123
123
  whatsapp: WhatsAppConfigSchema.default(WhatsAppConfigSchema.parse({})),
124
124
  telegram: TelegramConfigSchema.default(TelegramConfigSchema.parse({})),
@@ -3,28 +3,9 @@ import { describe, expect, test } from "bun:test";
3
3
  import {
4
4
  LiveVoiceConfigSchema,
5
5
  LiveVoiceFluxConfigSchema,
6
- LiveVoiceFrontModelConfigSchema,
7
6
  LiveVoiceVadConfigSchema,
8
- VALID_LIVE_VOICE_MODES,
9
7
  } from "../live-voice.js";
10
8
 
11
- const PROGRESS_DEFAULTS = {
12
- enabled: true,
13
- opsThreshold: 3,
14
- idleIntervalMs: 5_000,
15
- maxSilenceMs: 35_000,
16
- longOpMs: 15_000,
17
- minGapMs: 6_000,
18
- generationTimeoutMs: 1_500,
19
- };
20
-
21
- const FRONT_MODEL_DEFAULTS = {
22
- endpointDecisionTimeoutMs: 1200,
23
- endpointExtensionMs: 1500,
24
- endpointMaxExtensions: 2,
25
- progress: PROGRESS_DEFAULTS,
26
- };
27
-
28
9
  // `eagerEotThreshold` is deliberately absent: it has no default, and leaving it
29
10
  // unset is what keeps Deepgram from emitting speculative turn events. `model`
30
11
  // is absent for a related reason: unset means the spoken language selects the
@@ -130,117 +111,6 @@ describe("LiveVoiceVadConfigSchema", () => {
130
111
  });
131
112
  });
132
113
 
133
- describe("LiveVoiceFrontModelConfigSchema", () => {
134
- test("empty object parses to defaults", () => {
135
- const parsed = LiveVoiceFrontModelConfigSchema.parse({});
136
- expect(parsed).toEqual(FRONT_MODEL_DEFAULTS);
137
- });
138
-
139
- test("accepts overrides", () => {
140
- const parsed = LiveVoiceFrontModelConfigSchema.parse({
141
- endpointDecisionTimeoutMs: 400,
142
- endpointMaxExtensions: 0,
143
- });
144
- expect(parsed.endpointDecisionTimeoutMs).toBe(400);
145
- expect(parsed.endpointMaxExtensions).toBe(0);
146
- // Unspecified fields still get defaults
147
- expect(parsed.endpointExtensionMs).toBe(1500);
148
- });
149
-
150
- test("strips retired generated-ack settings", () => {
151
- const parsed = LiveVoiceFrontModelConfigSchema.parse({
152
- ackFirstDeltaTimeoutMs: 2500,
153
- ackGenerationTimeoutMs: 600,
154
- });
155
- expect(parsed).not.toHaveProperty("ackFirstDeltaTimeoutMs");
156
- expect(parsed).not.toHaveProperty("ackGenerationTimeoutMs");
157
- });
158
-
159
- test("rejects non-positive endpointDecisionTimeoutMs", () => {
160
- const result = LiveVoiceFrontModelConfigSchema.safeParse({
161
- endpointDecisionTimeoutMs: 0,
162
- });
163
- expect(result.success).toBe(false);
164
- });
165
-
166
- test("rejects negative endpointMaxExtensions", () => {
167
- const result = LiveVoiceFrontModelConfigSchema.safeParse({
168
- endpointMaxExtensions: -1,
169
- });
170
- expect(result.success).toBe(false);
171
- });
172
-
173
- test("absent progress namespace parses to full progress defaults", () => {
174
- const parsed = LiveVoiceFrontModelConfigSchema.parse({});
175
- expect(parsed.progress).toEqual(PROGRESS_DEFAULTS);
176
- });
177
-
178
- test("partial progress overrides merge with defaults", () => {
179
- const parsed = LiveVoiceFrontModelConfigSchema.parse({
180
- progress: { enabled: false, opsThreshold: 5 },
181
- });
182
- expect(parsed.progress.enabled).toBe(false);
183
- expect(parsed.progress.opsThreshold).toBe(5);
184
- // Unspecified progress fields still get defaults
185
- expect(parsed.progress.idleIntervalMs).toBe(5_000);
186
- expect(parsed.progress.maxSilenceMs).toBe(35_000);
187
- expect(parsed.progress.longOpMs).toBe(15_000);
188
- expect(parsed.progress.minGapMs).toBe(6_000);
189
- expect(parsed.progress.generationTimeoutMs).toBe(1_500);
190
- });
191
-
192
- test("a stale maxPerTurn key is stripped, not rejected", () => {
193
- // The cap was removed; configs written while it existed must still parse.
194
- const parsed = LiveVoiceFrontModelConfigSchema.parse({
195
- progress: { maxPerTurn: 3 },
196
- });
197
- expect("maxPerTurn" in parsed.progress).toBe(false);
198
- });
199
-
200
- test("rejects non-positive progress.opsThreshold", () => {
201
- const result = LiveVoiceFrontModelConfigSchema.safeParse({
202
- progress: { opsThreshold: 0 },
203
- });
204
- expect(result.success).toBe(false);
205
- });
206
-
207
- test("rejects a heartbeat ceiling shorter than the idle tick interval", () => {
208
- // The heartbeat is only checked on the idle tick, so a shorter ceiling
209
- // could be overshot by a full interval.
210
- const result = LiveVoiceFrontModelConfigSchema.safeParse({
211
- progress: { idleIntervalMs: 60_000, maxSilenceMs: 10_000 },
212
- });
213
- expect(result.success).toBe(false);
214
- if (!result.success) {
215
- const msgs = result.error.issues.map((issue) => issue.message);
216
- expect(
217
- msgs.some((msg) =>
218
- msg.includes("liveVoice.frontModel.progress.maxSilenceMs"),
219
- ),
220
- ).toBe(true);
221
- }
222
- // The floor itself is allowed: the tick is exactly on the ceiling.
223
- expect(
224
- LiveVoiceFrontModelConfigSchema.safeParse({
225
- progress: { idleIntervalMs: 60_000, maxSilenceMs: 60_000 },
226
- }).success,
227
- ).toBe(true);
228
- });
229
-
230
- test("rejects a non-boolean progress.enabled", () => {
231
- const result = LiveVoiceFrontModelConfigSchema.safeParse({
232
- progress: { enabled: "yes" },
233
- });
234
- expect(result.success).toBe(false);
235
- if (!result.success) {
236
- const msgs = result.error.issues.map((i) => i.message);
237
- expect(
238
- msgs.some((m) => m.includes("liveVoice.frontModel.progress.enabled")),
239
- ).toBe(true);
240
- }
241
- });
242
- });
243
-
244
114
  describe("LiveVoiceFluxConfigSchema", () => {
245
115
  test("empty object parses to defaults, with turn-end on", () => {
246
116
  expect(LiveVoiceFluxConfigSchema.parse({})).toEqual(FLUX_DEFAULTS);
@@ -326,7 +196,6 @@ describe("LiveVoiceConfigSchema", () => {
326
196
  test("empty object parses to defaults", () => {
327
197
  const parsed = LiveVoiceConfigSchema.parse({});
328
198
  expect(parsed).toEqual({
329
- mode: "open-mic",
330
199
  vad: {
331
200
  speechEnergyThreshold: 800,
332
201
  noiseFloorMargin: 3,
@@ -337,11 +206,9 @@ describe("LiveVoiceConfigSchema", () => {
337
206
  echoEmaHalfLifeMs: 400,
338
207
  echoDrainSlackMs: 300,
339
208
  },
340
- frontModel: FRONT_MODEL_DEFAULTS,
341
209
  // Off by default: Flux turn detection is opt-in, so the front-door hold
342
210
  // verdict keeps committing turns until it is enabled.
343
211
  flux: FLUX_DEFAULTS,
344
- maxSessionDurationSeconds: 1800,
345
212
  // Off by default: voice turns carry only their transcript, no audio
346
213
  // artifacts on the conversation messages (JARVIS-1283).
347
214
  archiveAudio: false,
@@ -365,43 +232,26 @@ describe("LiveVoiceConfigSchema", () => {
365
232
 
366
233
  test("accepts overrides", () => {
367
234
  const parsed = LiveVoiceConfigSchema.parse({
368
- mode: "ptt",
369
235
  vad: { silenceThresholdMs: 900 },
370
- frontModel: { endpointDecisionTimeoutMs: 300 },
371
236
  flux: { turnEnd: { enabled: true } },
372
- maxSessionDurationSeconds: 600,
373
237
  });
374
- expect(parsed.mode).toBe("ptt");
375
238
  expect(parsed.vad.silenceThresholdMs).toBe(900);
376
239
  // Unspecified vad fields still get defaults
377
240
  expect(parsed.vad.speechEnergyThreshold).toBe(800);
378
241
  expect(parsed.vad.maxTurnDurationMs).toBe(30_000);
379
- // Partial frontModel overrides merge with defaults
380
- expect(parsed.frontModel.endpointDecisionTimeoutMs).toBe(300);
381
- expect(parsed.frontModel.endpointExtensionMs).toBe(1500);
382
242
  // Partial flux overrides merge with defaults
383
243
  expect(parsed.flux.turnEnd.enabled).toBe(true);
384
244
  expect(parsed.flux.eotThreshold).toBe(0.7);
385
- expect(parsed.maxSessionDurationSeconds).toBe(600);
386
- });
387
-
388
- test("rejects invalid mode", () => {
389
- const result = LiveVoiceConfigSchema.safeParse({ mode: "always-on" });
390
- expect(result.success).toBe(false);
391
- if (!result.success) {
392
- const msgs = result.error.issues.map((i) => i.message);
393
- expect(msgs.some((m) => m.includes("liveVoice.mode"))).toBe(true);
394
- }
395
245
  });
396
246
 
397
- test("rejects non-positive maxSessionDurationSeconds", () => {
398
- const result = LiveVoiceConfigSchema.safeParse({
399
- maxSessionDurationSeconds: -1,
247
+ test("persisted configs carrying retired keys keep parsing", () => {
248
+ // Zod strips unrecognized keys, so a persisted config that still carries
249
+ // `mode` or `maxSessionDurationSeconds` parses.
250
+ const parsed = LiveVoiceConfigSchema.parse({
251
+ mode: "ptt",
252
+ maxSessionDurationSeconds: 600,
400
253
  });
401
- expect(result.success).toBe(false);
402
- });
403
-
404
- test("VALID_LIVE_VOICE_MODES lists ptt and open-mic", () => {
405
- expect(VALID_LIVE_VOICE_MODES).toEqual(["ptt", "open-mic"]);
254
+ expect(parsed).not.toHaveProperty("mode");
255
+ expect(parsed).not.toHaveProperty("maxSessionDurationSeconds");
406
256
  });
407
257
  });
@@ -0,0 +1,148 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { VoiceConfigSchema, VoiceFrontModelConfigSchema } from "../voice.js";
4
+
5
+ const PROGRESS_DEFAULTS = {
6
+ enabled: true,
7
+ opsThreshold: 3,
8
+ idleIntervalMs: 5_000,
9
+ maxSilenceMs: 35_000,
10
+ longOpMs: 15_000,
11
+ minGapMs: 6_000,
12
+ generationTimeoutMs: 1_500,
13
+ };
14
+
15
+ const FRONT_MODEL_DEFAULTS = {
16
+ endpointDecisionTimeoutMs: 1200,
17
+ endpointExtensionMs: 1500,
18
+ endpointMaxExtensions: 2,
19
+ progress: PROGRESS_DEFAULTS,
20
+ };
21
+
22
+ describe("VoiceFrontModelConfigSchema", () => {
23
+ test("empty object parses to defaults", () => {
24
+ const parsed = VoiceFrontModelConfigSchema.parse({});
25
+ expect(parsed).toEqual(FRONT_MODEL_DEFAULTS);
26
+ });
27
+
28
+ test("accepts overrides", () => {
29
+ const parsed = VoiceFrontModelConfigSchema.parse({
30
+ endpointDecisionTimeoutMs: 400,
31
+ endpointMaxExtensions: 0,
32
+ });
33
+ expect(parsed.endpointDecisionTimeoutMs).toBe(400);
34
+ expect(parsed.endpointMaxExtensions).toBe(0);
35
+ // Unspecified fields still get defaults
36
+ expect(parsed.endpointExtensionMs).toBe(1500);
37
+ });
38
+
39
+ test("strips retired generated-ack settings", () => {
40
+ const parsed = VoiceFrontModelConfigSchema.parse({
41
+ ackFirstDeltaTimeoutMs: 2500,
42
+ ackGenerationTimeoutMs: 600,
43
+ });
44
+ expect(parsed).not.toHaveProperty("ackFirstDeltaTimeoutMs");
45
+ expect(parsed).not.toHaveProperty("ackGenerationTimeoutMs");
46
+ });
47
+
48
+ test("rejects non-positive endpointDecisionTimeoutMs", () => {
49
+ const result = VoiceFrontModelConfigSchema.safeParse({
50
+ endpointDecisionTimeoutMs: 0,
51
+ });
52
+ expect(result.success).toBe(false);
53
+ });
54
+
55
+ test("rejects negative endpointMaxExtensions", () => {
56
+ const result = VoiceFrontModelConfigSchema.safeParse({
57
+ endpointMaxExtensions: -1,
58
+ });
59
+ expect(result.success).toBe(false);
60
+ });
61
+
62
+ test("absent progress namespace parses to full progress defaults", () => {
63
+ const parsed = VoiceFrontModelConfigSchema.parse({});
64
+ expect(parsed.progress).toEqual(PROGRESS_DEFAULTS);
65
+ });
66
+
67
+ test("partial progress overrides merge with defaults", () => {
68
+ const parsed = VoiceFrontModelConfigSchema.parse({
69
+ progress: { enabled: false, opsThreshold: 5 },
70
+ });
71
+ expect(parsed.progress.enabled).toBe(false);
72
+ expect(parsed.progress.opsThreshold).toBe(5);
73
+ // Unspecified progress fields still get defaults
74
+ expect(parsed.progress.idleIntervalMs).toBe(5_000);
75
+ expect(parsed.progress.maxSilenceMs).toBe(35_000);
76
+ expect(parsed.progress.longOpMs).toBe(15_000);
77
+ expect(parsed.progress.minGapMs).toBe(6_000);
78
+ expect(parsed.progress.generationTimeoutMs).toBe(1_500);
79
+ });
80
+
81
+ test("a stale maxPerTurn key is stripped, not rejected", () => {
82
+ // The cap was removed; configs written while it existed must still parse.
83
+ const parsed = VoiceFrontModelConfigSchema.parse({
84
+ progress: { maxPerTurn: 3 },
85
+ });
86
+ expect("maxPerTurn" in parsed.progress).toBe(false);
87
+ });
88
+
89
+ test("rejects non-positive progress.opsThreshold", () => {
90
+ const result = VoiceFrontModelConfigSchema.safeParse({
91
+ progress: { opsThreshold: 0 },
92
+ });
93
+ expect(result.success).toBe(false);
94
+ });
95
+
96
+ test("rejects a heartbeat ceiling shorter than the idle tick interval", () => {
97
+ // The heartbeat is only checked on the idle tick, so a shorter ceiling
98
+ // could be overshot by a full interval.
99
+ const result = VoiceFrontModelConfigSchema.safeParse({
100
+ progress: { idleIntervalMs: 60_000, maxSilenceMs: 10_000 },
101
+ });
102
+ expect(result.success).toBe(false);
103
+ if (!result.success) {
104
+ const msgs = result.error.issues.map((issue) => issue.message);
105
+ expect(
106
+ msgs.some((msg) =>
107
+ msg.includes("voice.frontModel.progress.maxSilenceMs"),
108
+ ),
109
+ ).toBe(true);
110
+ }
111
+ // The floor itself is allowed: the tick is exactly on the ceiling.
112
+ expect(
113
+ VoiceFrontModelConfigSchema.safeParse({
114
+ progress: { idleIntervalMs: 60_000, maxSilenceMs: 60_000 },
115
+ }).success,
116
+ ).toBe(true);
117
+ });
118
+
119
+ test("rejects a non-boolean progress.enabled", () => {
120
+ const result = VoiceFrontModelConfigSchema.safeParse({
121
+ progress: { enabled: "yes" },
122
+ });
123
+ expect(result.success).toBe(false);
124
+ if (!result.success) {
125
+ const msgs = result.error.issues.map((i) => i.message);
126
+ expect(
127
+ msgs.some((m) => m.includes("voice.frontModel.progress.enabled")),
128
+ ).toBe(true);
129
+ }
130
+ });
131
+ });
132
+
133
+ describe("VoiceConfigSchema", () => {
134
+ test("empty object parses to front-model defaults", () => {
135
+ expect(VoiceConfigSchema.parse({})).toEqual({
136
+ frontModel: FRONT_MODEL_DEFAULTS,
137
+ });
138
+ });
139
+
140
+ test("partial frontModel overrides merge with defaults", () => {
141
+ const parsed = VoiceConfigSchema.parse({
142
+ frontModel: { endpointDecisionTimeoutMs: 300 },
143
+ });
144
+ expect(parsed.frontModel.endpointDecisionTimeoutMs).toBe(300);
145
+ expect(parsed.frontModel.endpointExtensionMs).toBe(1500);
146
+ expect(parsed.frontModel.progress).toEqual(PROGRESS_DEFAULTS);
147
+ });
148
+ });