@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,186 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ import { writeDesktopWindowTheme } from "../src/desktop/desktop-window-theme.js";
7
+
8
+ if (process.platform !== "linux") {
9
+ throw new Error(
10
+ "Run in a disposable Linux environment with Xvfb, Openbox, xterm, xdotool, xwininfo and xmllint installed",
11
+ );
12
+ }
13
+
14
+ const directory = await mkdtemp(join(tmpdir(), "window-theme-smoke-"));
15
+ const env = { ...process.env, DISPLAY: ":97" };
16
+ const children: ReturnType<typeof Bun.spawn>[] = [];
17
+ const start = (cmd: string[]) => {
18
+ const child = Bun.spawn(cmd, {
19
+ env,
20
+ stdout: "ignore",
21
+ stderr: "inherit",
22
+ windowsHide: true,
23
+ });
24
+ children.push(child);
25
+ return child;
26
+ };
27
+ const run = (...cmd: string[]) => {
28
+ const result = Bun.spawnSync(cmd, {
29
+ env,
30
+
31
+ windowsHide: true,
32
+ });
33
+ assert.equal(result.exitCode, 0, result.stderr.toString());
34
+ return result.stdout.toString();
35
+ };
36
+ const waitFor = async (predicate: () => boolean) => {
37
+ for (let attempt = 0; attempt < 100; attempt++) {
38
+ try {
39
+ if (predicate()) {
40
+ return;
41
+ }
42
+ } catch {}
43
+ await Bun.sleep(50);
44
+ }
45
+ throw new Error("Window theme smoke condition timed out");
46
+ };
47
+ const pointer = async (...args: string[]) => {
48
+ run("xdotool", ...args);
49
+ await Bun.sleep(200);
50
+ };
51
+ const geometry = () => {
52
+ const info = run("xwininfo", "-name", "Theme smoke");
53
+ return [
54
+ "Absolute upper-left X",
55
+ "Absolute upper-left Y",
56
+ "Width",
57
+ "Height",
58
+ ].map((label) =>
59
+ Number(new RegExp(`${label}:\\s+(-?\\d+)`).exec(info)?.[1]),
60
+ ) as [number, number, number, number];
61
+ };
62
+ const state = () => run("xprop", "-name", "Theme smoke", "_NET_WM_STATE");
63
+ const button = async (offset: number) => {
64
+ const [x, y, width] = geometry();
65
+ await pointer(
66
+ "mousemove",
67
+ String(x + width - offset),
68
+ String(y - 16),
69
+ "click",
70
+ "1",
71
+ );
72
+ };
73
+
74
+ try {
75
+ const home = join(directory, "home");
76
+ const sourceDir = join(home, ".config", "openbox");
77
+ await mkdir(sourceDir, { recursive: true });
78
+ const original = await readFile("/etc/xdg/openbox/rc.xml", "utf8");
79
+ const mouse = original.match(/<mouse>[^]*?<\/mouse>/)![0];
80
+ const keyboard = original.match(/<keyboard>[^]*?<\/keyboard>/)![0];
81
+ const source = original
82
+ .replace(
83
+ mouse,
84
+ `<xi:include href="bindings.xml" xpointer="xpointer(/*/*)"/>`,
85
+ )
86
+ .replace(keyboard, "");
87
+ const sourcePath = join(sourceDir, "rc.xml");
88
+ await writeFile(sourcePath, source);
89
+ await writeFile(
90
+ join(sourceDir, "bindings.xml"),
91
+ `<bindings xmlns="http://openbox.org/3.4/rc">${mouse}${keyboard}</bindings>`,
92
+ );
93
+ const configDir = join(directory, "managed & theme");
94
+ const config = writeDesktopWindowTheme(configDir, home, "#53b7aa");
95
+ const expanded = run("xmllint", "--xinclude", config);
96
+ assert(
97
+ expanded.includes("NextWindow") && expanded.includes("Move"),
98
+ "Relative includes must retain the actual keyboard and mouse bindings",
99
+ );
100
+ assert.equal(await readFile(sourcePath, "utf8"), source);
101
+ writeDesktopWindowTheme(configDir, home, "#bc7799");
102
+ writeDesktopWindowTheme(configDir, home, "#53b7aa");
103
+ assert.equal(await readFile(sourcePath, "utf8"), source);
104
+ const blocked = join(directory, "blocked");
105
+ await writeFile(blocked, "not a directory");
106
+ assert.throws(() => writeDesktopWindowTheme(blocked, home));
107
+ assert.equal(await readFile(sourcePath, "utf8"), source);
108
+ const customBase = source.replace(
109
+ "<openbox_config",
110
+ '<openbox_config xml:base="./custom/"',
111
+ );
112
+ await writeFile(sourcePath, customBase);
113
+ assert.throws(() => writeDesktopWindowTheme(configDir, home));
114
+ assert.equal(await readFile(sourcePath, "utf8"), customBase);
115
+ await writeFile(sourcePath, source);
116
+
117
+ start(["Xvfb", ":97", "-screen", "0", "1100x700x24"]);
118
+ await waitFor(
119
+ () => run("xdotool", "getdisplaygeometry").trim() === "1100 700",
120
+ );
121
+ start(["openbox", "--config-file", config]);
122
+ await waitFor(() =>
123
+ run("xprop", "-root", "_OB_THEME").includes("window-theme"),
124
+ );
125
+ start([
126
+ "xterm",
127
+ "-title",
128
+ "Theme smoke",
129
+ "-geometry",
130
+ "48x12+450+250",
131
+ "-e",
132
+ "sleep",
133
+ "60",
134
+ ]);
135
+ await waitFor(
136
+ () => run("xdotool", "search", "--name", "^Theme smoke$").trim().length > 0,
137
+ );
138
+ await Bun.sleep(300);
139
+ const id = run("xdotool", "search", "--name", "^Theme smoke$").trim();
140
+ await button(39);
141
+ assert(
142
+ state().includes("MAXIMIZED_HORZ") && state().includes("MAXIMIZED_VERT"),
143
+ );
144
+ await button(39);
145
+ assert(!state().includes("MAXIMIZED"));
146
+ await button(62);
147
+ assert(state().includes("HIDDEN"));
148
+ await pointer("windowactivate", id);
149
+ assert(!state().includes("HIDDEN"));
150
+ let [x, y, width, height] = geometry();
151
+ await pointer("mousemove", String(x + 100), String(y - 16), "mousedown", "1");
152
+ await pointer("mousemove", String(x + 90), String(y - 26));
153
+ await pointer("mousemove", String(x + 45), String(y - 65));
154
+ await pointer("mouseup", "1");
155
+ const [movedX, movedY] = geometry();
156
+ assert(Math.abs(movedX - x) >= 30 && Math.abs(movedY - y) >= 30);
157
+ [x, y, width, height] = geometry();
158
+ await pointer(
159
+ "mousemove",
160
+ String(x + width - 2),
161
+ String(y + height + 2),
162
+ "mousedown",
163
+ "1",
164
+ );
165
+ await pointer("mousemove", String(x + width + 10), String(y + height + 10));
166
+ await pointer("mousemove", String(x + width + 60), String(y + height + 40));
167
+ await pointer("mouseup", "1");
168
+ const [, , resizedWidth, resizedHeight] = geometry();
169
+ assert(resizedWidth > width && resizedHeight > height);
170
+ await pointer("key", "alt+F4");
171
+ await waitFor(
172
+ () =>
173
+ !run("xprop", "-root", "_NET_CLIENT_LIST").includes(
174
+ `0x${Number(id).toString(16)}`,
175
+ ),
176
+ );
177
+ console.log(
178
+ "PASS: relative includes, preserved source, repeated theme updates, maximize/restore, minimize/activation, dragging, resizing, and keyboard close",
179
+ );
180
+ } finally {
181
+ for (const child of children.reverse()) {
182
+ child.kill();
183
+ }
184
+ await Promise.all(children.map((child) => child.exited));
185
+ await rm(directory, { recursive: true, force: true });
186
+ }
@@ -26,7 +26,7 @@ afterAll(() => {
26
26
  });
27
27
 
28
28
  describe("always-loaded tool count", () => {
29
- test("should be exactly 11 with recall occupying the existing slot", async () => {
29
+ test("should be exactly 19 with recall occupying the existing slot", async () => {
30
30
  await initializeTools();
31
31
  const allDefs = getAllToolDefinitions();
32
32
 
@@ -44,23 +44,31 @@ describe("always-loaded tool count", () => {
44
44
  );
45
45
  const activeNames = activeTools.map((t) => t.name).sort();
46
46
 
47
- // Host tools (host_bash, host_file_*) are excluded when no client is
48
- // connected without a human in the loop, the guardian auto-approve
49
- // path would allow unchecked host command execution.
50
- //
51
- // `watch_retro_report` is here for the same reason the ui_surface tools are
52
- // NOT: a watch retrospective runs clientless, so it can only report through
53
- // a tool that survives this baseline. Its description and schema are kept
54
- // deliberately terse because that is the cost of the slot.
47
+ // `watch_retro_report` survives this baseline for the same reason the
48
+ // ui_* tools do: a watch retrospective runs clientless, so it can only
49
+ // report through a tool that survives this baseline. The ui_* tools are
50
+ // here because background UI surfaces persist and return instead of
51
+ // awaiting action, so they no longer need a connected client.
52
+ // Host tool definitions stay on the wire for background turns: schemas
53
+ // are cache-stable, and the shared execution gate rejects host tools on
54
+ // explicitly non-interactive turns before any proxy dispatch.
55
55
  const expectedNames = [
56
56
  "bash",
57
57
  "file_edit",
58
58
  "file_read",
59
59
  "file_write",
60
+ "host_bash",
61
+ "host_file_edit",
62
+ "host_file_read",
63
+ "host_file_transfer",
64
+ "host_file_write",
60
65
  "recall",
61
66
  "remember",
62
67
  "skill_execute",
63
68
  "skill_load",
69
+ "ui_dismiss",
70
+ "ui_show",
71
+ "ui_update",
64
72
  "watch_retro_report",
65
73
  "web_fetch",
66
74
  "web_search",
@@ -68,6 +76,6 @@ describe("always-loaded tool count", () => {
68
76
 
69
77
  expect(activeNames).toEqual(expectedNames);
70
78
  expect(activeNames.filter((name) => name === "recall")).toHaveLength(1);
71
- expect(activeTools.length).toBe(11);
79
+ expect(activeTools.length).toBe(19);
72
80
  });
73
81
  });
@@ -112,6 +112,12 @@ describe("compileApp", () => {
112
112
  const html = await readFile(join(appDir, "dist", "index.html"), "utf-8");
113
113
  expect(html).toContain('src="main.js"');
114
114
  expect(html).toContain('type="module"');
115
+
116
+ const fingerprint = JSON.parse(
117
+ await readFile(join(appDir, "dist", ".source-fingerprint.json"), "utf-8"),
118
+ ) as { algorithm: string; files: Record<string, string> };
119
+ expect(fingerprint.algorithm).toBe("sha256");
120
+ expect(fingerprint.files["src/main.tsx"]).toMatch(/^[0-9a-f]{64}$/);
115
121
  });
116
122
 
117
123
  test("compiles preact JSX correctly", async () => {
@@ -591,8 +591,11 @@ describe("forwardAppControlProxyTool", () => {
591
591
  ctx,
592
592
  );
593
593
  expect(result.isError).toBe(true);
594
- expect(result.content).toContain("no proxy resolver available");
595
- expect(result.content).toContain("app_control_click");
594
+ expect(result.content).toContain(
595
+ "The Vellum desktop app is needed to control apps on your computer",
596
+ );
597
+ expect(result.content).toContain("https://www.vellum.ai/downloads");
598
+ expect(result.content).not.toContain("app_control_click");
596
599
  });
597
600
 
598
601
  test("delegates to proxy resolver when available", async () => {
@@ -9,15 +9,13 @@ import { describe, expect, test } from "bun:test";
9
9
  * This prevents regressions where new code bypasses the dirName-based path
10
10
  * resolution and constructs UUID-based paths directly.
11
11
  *
12
- * Allowlist: only app-store.ts itself, app-source-watcher.ts (uses getAppsDir
13
- * for the fs.watch root, not for per-app paths), and workspace migrations
12
+ * Allowlist: only app-store.ts itself and workspace migrations
14
13
  * (self-contained, don't import from app-store).
15
14
  */
16
15
 
17
16
  /** Files that are permitted to import getAppsDir. */
18
17
  const ALLOWLIST = new Set([
19
18
  "assistant/src/apps/app-store.ts", // defines getAppsDir
20
- "assistant/src/daemon/app-source-watcher.ts", // uses getAppsDir for recursive fs.watch root, not per-app paths
21
19
  "assistant/src/tools/filesystem/write.ts", // uses getAppsDir as an exemption root for the artifact-HTML guard, not per-app paths
22
20
  ]);
23
21
 
@@ -13,8 +13,11 @@ describe("forwardComputerUseProxyTool", () => {
13
13
  );
14
14
 
15
15
  expect(result.isError).toBe(true);
16
- expect(result.content).toContain("no proxy resolver available");
17
- expect(result.content).toContain("computer_use_click");
16
+ expect(result.content).toContain(
17
+ "The Vellum desktop app is needed to view or control your screen",
18
+ );
19
+ expect(result.content).toContain("https://www.vellum.ai/downloads");
20
+ expect(result.content).not.toContain("computer_use_click");
18
21
  });
19
22
 
20
23
  test("forwards to proxyToolResolver when present", async () => {
@@ -104,7 +104,10 @@ describe("computer_use_click (unified)", () => {
104
104
  test("execute returns isError when no proxy resolver is configured", async () => {
105
105
  const result = await computerUseClickTool.execute({}, ctx);
106
106
  expect(result.isError).toBe(true);
107
- expect(result.content).toMatch(/No proxy resolver/);
107
+ expect(result.content).toContain(
108
+ "The Vellum desktop app is needed to view or control your screen",
109
+ );
110
+ expect(result.content).toContain("https://www.vellum.ai/downloads");
108
111
  });
109
112
  });
110
113
 
@@ -119,7 +122,10 @@ describe("computer_use_type_text", () => {
119
122
  test("execute returns isError when no proxy resolver is configured", async () => {
120
123
  const result = await computerUseTypeTextTool.execute({}, ctx);
121
124
  expect(result.isError).toBe(true);
122
- expect(result.content).toMatch(/No proxy resolver/);
125
+ expect(result.content).toContain(
126
+ "The Vellum desktop app is needed to view or control your screen",
127
+ );
128
+ expect(result.content).toContain("https://www.vellum.ai/downloads");
123
129
  });
124
130
  });
125
131
 
@@ -134,7 +140,10 @@ describe("computer_use_key", () => {
134
140
  test("execute returns isError when no proxy resolver is configured", async () => {
135
141
  const result = await computerUseKeyTool.execute({}, ctx);
136
142
  expect(result.isError).toBe(true);
137
- expect(result.content).toMatch(/No proxy resolver/);
143
+ expect(result.content).toContain(
144
+ "The Vellum desktop app is needed to view or control your screen",
145
+ );
146
+ expect(result.content).toContain("https://www.vellum.ai/downloads");
138
147
  });
139
148
  });
140
149
 
@@ -241,8 +250,11 @@ describe("forwardComputerUseProxyTool", () => {
241
250
  );
242
251
 
243
252
  expect(result.isError).toBe(true);
244
- expect(result.content).toContain("no proxy resolver available");
245
- expect(result.content).toContain("computer_use_click");
253
+ expect(result.content).toContain(
254
+ "The Vellum desktop app is needed to view or control your screen",
255
+ );
256
+ expect(result.content).toContain("https://www.vellum.ai/downloads");
257
+ expect(result.content).not.toContain("computer_use_click");
246
258
  });
247
259
 
248
260
  test("delegates to proxy resolver when available", async () => {
@@ -130,7 +130,7 @@ describe("getSchemaAtPath", () => {
130
130
  expect(properties).toBeDefined();
131
131
  expect(properties.enabled).toBeDefined();
132
132
  expect(properties.voice).toBeDefined();
133
- expect(properties.safety).toBeDefined();
133
+ expect(properties.disclosure).toBeDefined();
134
134
  });
135
135
 
136
136
  test("navigates through .transform() wrappers (ingress → object schema)", () => {
@@ -230,7 +230,7 @@ describe("z.toJSONSchema integration", () => {
230
230
  expect(properties).toBeDefined();
231
231
  expect(properties!.enabled).toBeDefined();
232
232
  expect(properties!.voice).toBeDefined();
233
- expect(properties!.safety).toBeDefined();
233
+ expect(properties!.disclosure).toBeDefined();
234
234
  });
235
235
 
236
236
  test("sub-schema at a leaf like llm.profileSession.defaultTtlSeconds produces integer schema", () => {
@@ -952,11 +952,7 @@ describe("AssistantConfigSchema", () => {
952
952
  enabled: true,
953
953
  text: 'At the very beginning of the call, introduce yourself as an assistant calling on behalf of the person you represent. Do not say "AI assistant".',
954
954
  },
955
- safety: {
956
- denyCategories: [],
957
- },
958
955
  voice: {
959
- interruptSensitivity: "low",
960
956
  telephonyStreaming: true,
961
957
  utteranceEndMs: 1000,
962
958
  },
@@ -978,7 +974,6 @@ describe("AssistantConfigSchema", () => {
978
974
  maxDurationSeconds: 1800,
979
975
  userConsultTimeoutSeconds: 60,
980
976
  disclosure: { enabled: false, text: "Custom disclosure" },
981
- safety: { denyCategories: ["spam"] },
982
977
  },
983
978
  });
984
979
  expect(result.calls.enabled).toBe(false);
@@ -986,7 +981,6 @@ describe("AssistantConfigSchema", () => {
986
981
  expect(result.calls.userConsultTimeoutSeconds).toBe(60);
987
982
  expect(result.calls.disclosure.enabled).toBe(false);
988
983
  expect(result.calls.disclosure.text).toBe("Custom disclosure");
989
- expect(result.calls.safety.denyCategories).toEqual(["spam"]);
990
984
  });
991
985
 
992
986
  // ── Live voice config ───────────────────────────────────────────────
@@ -994,7 +988,6 @@ describe("AssistantConfigSchema", () => {
994
988
  test("applies liveVoice defaults", () => {
995
989
  const result = AssistantConfigSchema.parse({});
996
990
  expect(result.liveVoice).toEqual({
997
- mode: "open-mic",
998
991
  vad: {
999
992
  speechEnergyThreshold: 800,
1000
993
  noiseFloorMargin: 3,
@@ -1005,6 +998,18 @@ describe("AssistantConfigSchema", () => {
1005
998
  echoEmaHalfLifeMs: 400,
1006
999
  echoDrainSlackMs: 300,
1007
1000
  },
1001
+ flux: {
1002
+ turnEnd: { enabled: true },
1003
+ eotThreshold: 0.7,
1004
+ eotTimeoutMs: 5000,
1005
+ },
1006
+ archiveAudio: false,
1007
+ });
1008
+ });
1009
+
1010
+ test("applies voice defaults", () => {
1011
+ const result = AssistantConfigSchema.parse({});
1012
+ expect(result.voice).toEqual({
1008
1013
  frontModel: {
1009
1014
  endpointDecisionTimeoutMs: 1200,
1010
1015
  endpointExtensionMs: 1500,
@@ -1019,35 +1024,24 @@ describe("AssistantConfigSchema", () => {
1019
1024
  generationTimeoutMs: 1500,
1020
1025
  },
1021
1026
  },
1022
- flux: {
1023
- turnEnd: { enabled: true },
1024
- eotThreshold: 0.7,
1025
- eotTimeoutMs: 5000,
1026
- },
1027
- maxSessionDurationSeconds: 1800,
1028
- archiveAudio: false,
1029
1027
  });
1030
1028
  });
1031
1029
 
1032
1030
  test("accepts valid liveVoice config overrides", () => {
1033
1031
  const result = AssistantConfigSchema.parse({
1034
1032
  liveVoice: {
1035
- mode: "ptt",
1036
1033
  vad: {
1037
1034
  speechEnergyThreshold: 1500,
1038
1035
  silenceThresholdMs: 1000,
1039
1036
  bargeInMinSpeechMs: 120,
1040
1037
  },
1041
- maxSessionDurationSeconds: 900,
1042
1038
  },
1043
1039
  });
1044
- expect(result.liveVoice.mode).toBe("ptt");
1045
1040
  expect(result.liveVoice.vad.speechEnergyThreshold).toBe(1500);
1046
1041
  expect(result.liveVoice.vad.silenceThresholdMs).toBe(1000);
1047
1042
  expect(result.liveVoice.vad.bargeInMinSpeechMs).toBe(120);
1048
1043
  // Unspecified vad fields still get defaults
1049
1044
  expect(result.liveVoice.vad.maxTurnDurationMs).toBe(30000);
1050
- expect(result.liveVoice.maxSessionDurationSeconds).toBe(900);
1051
1045
  // A partial liveVoice override leaves Flux turn-end disabled by default.
1052
1046
  expect(result.liveVoice.flux.turnEnd.enabled).toBe(true);
1053
1047
  });
@@ -1136,18 +1130,10 @@ describe("AssistantConfigSchema", () => {
1136
1130
  expect(result.success).toBe(false);
1137
1131
  });
1138
1132
 
1139
- test("rejects non-array calls.safety.denyCategories", () => {
1140
- const result = AssistantConfigSchema.safeParse({
1141
- calls: { safety: { denyCategories: "spam" } },
1142
- });
1143
- expect(result.success).toBe(false);
1144
- });
1145
-
1146
1133
  // ── Calls voice config ──────────────────────────────────────────────
1147
1134
 
1148
1135
  test("config without calls.voice parses correctly and produces defaults", () => {
1149
1136
  const result = AssistantConfigSchema.parse({});
1150
- expect(result.calls.voice.interruptSensitivity).toBe("low");
1151
1137
  expect(result.calls.voice.telephonyStreaming).toBe(true);
1152
1138
  expect(result.calls.voice.utteranceEndMs).toBe(1000);
1153
1139
  });
@@ -1165,9 +1151,23 @@ describe("AssistantConfigSchema", () => {
1165
1151
  expect(result.calls.voice.utteranceEndMs).toBe(2500);
1166
1152
  });
1167
1153
 
1154
+ test("persisted calls configs carrying retired keys keep parsing", () => {
1155
+ // Zod strips unrecognized keys, so a persisted config that still carries
1156
+ // `calls.safety.denyCategories` or `calls.voice.interruptSensitivity`
1157
+ // parses.
1158
+ const result = AssistantConfigSchema.parse({
1159
+ calls: {
1160
+ safety: { denyCategories: ["spam"] },
1161
+ voice: { interruptSensitivity: "high" },
1162
+ },
1163
+ });
1164
+ expect(result.calls).not.toHaveProperty("safety");
1165
+ expect(result.calls.voice).not.toHaveProperty("interruptSensitivity");
1166
+ });
1167
+
1168
1168
  test("language is no longer part of the voice config schema", () => {
1169
- // The retired knob was read by nothing; Zod strips the unrecognized key
1170
- // so persisted configs that still carry it keep parsing.
1169
+ // Zod strips the unrecognized key, so persisted configs that still carry
1170
+ // it parse.
1171
1171
  const result = AssistantConfigSchema.parse({
1172
1172
  calls: { voice: { language: "es-ES" } },
1173
1173
  });
@@ -2633,7 +2633,6 @@ describe("loadConfig with schema validation", () => {
2633
2633
  expect(config.calls.maxDurationSeconds).toBe(3600);
2634
2634
  expect(config.calls.userConsultTimeoutSeconds).toBe(120);
2635
2635
  expect(config.calls.disclosure.enabled).toBe(true);
2636
- expect(config.calls.safety.denyCategories).toEqual([]);
2637
2636
  expect(
2638
2637
  (config.calls.voice as Record<string, unknown>).language,
2639
2638
  ).toBeUndefined();
@@ -113,8 +113,11 @@ mock.module("../providers/registry.js", () => ({
113
113
  resolveProviderFromConnection: async () => null,
114
114
  }));
115
115
 
116
+ let mcpReloadCallCount = 0;
116
117
  mock.module("../daemon/mcp-reload-service.js", () => ({
117
- reloadMcpServers: async () => {},
118
+ reloadMcpServers: async () => {
119
+ mcpReloadCallCount += 1;
120
+ },
118
121
  }));
119
122
 
120
123
  mock.module("../signals/conversation-undo.js", () => ({
@@ -154,10 +157,6 @@ mock.module("../runtime/sync/resource-sync-events.js", () => ({
154
157
  },
155
158
  }));
156
159
 
157
- mock.module("../daemon/skill-memory-refresh.js", () => ({
158
- refreshSkillCapabilityMemories: () => {},
159
- }));
160
-
161
160
  mock.module("../platform/sync-identity.js", () => ({
162
161
  syncIdentityNameToPlatform: () => {},
163
162
  }));
@@ -183,6 +182,7 @@ function findFileWatch(filePath: string): CapturedFileWatch | undefined {
183
182
 
184
183
  const WORKSPACE_FILES = new Set([
185
184
  "config.json",
185
+ "mcp.json",
186
186
  "SOUL.md",
187
187
  "IDENTITY.md",
188
188
  "ui/theme.json",
@@ -239,6 +239,7 @@ beforeEach(() => {
239
239
  evictCallCount = 0;
240
240
  identityCallCount = 0;
241
241
  themeCallCount = 0;
242
+ mcpReloadCallCount = 0;
242
243
  watcher = new ConfigWatcher(undefined, TEST_DEBOUNCE_MS);
243
244
  });
244
245
 
@@ -270,9 +271,9 @@ describe("ConfigWatcher workspace file handlers", () => {
270
271
 
271
272
  test("unregistered workspace files are not subscribed (only the registered handler set is)", () => {
272
273
  watcher.start();
273
- // Per-file watching only registers config.json, SOUL.md, IDENTITY.md,
274
- // and ui/theme.json. The whole workspace dir must not be watched
275
- // either that was the ENXIO-on-Unix-sockets bug.
274
+ // Per-file watching only registers config.json, mcp.json, SOUL.md,
275
+ // IDENTITY.md, and ui/theme.json. The whole workspace dir must not
276
+ // be watched either: that was the ENXIO-on-Unix-sockets bug.
276
277
  expect(findFileWatch(join(WORKSPACE_DIR, "OTHER.md"))).toBeUndefined();
277
278
  expect(findWatcher(WORKSPACE_DIR)).toBeUndefined();
278
279
  });
@@ -285,6 +286,14 @@ describe("ConfigWatcher workspace file handlers", () => {
285
286
  expect(evictCallCount).toBe(0);
286
287
  });
287
288
 
289
+ test("mcp.json change reloads MCP servers without evicting conversations", async () => {
290
+ watcher.start();
291
+ simulateFileChange(WORKSPACE_DIR, "mcp.json");
292
+ await new Promise((r) => setTimeout(r, WAIT_MS));
293
+ expect(mcpReloadCallCount).toBe(1);
294
+ expect(evictCallCount).toBe(0);
295
+ });
296
+
288
297
  test("config.json change calls refreshConfigFromSources", async () => {
289
298
  let refreshCalled = false;
290
299
  watcher.refreshConfigFromSources = async () => {
@@ -332,6 +341,12 @@ describe("ConfigWatcher watcher lifecycle", () => {
332
341
  expect(findFileWatch(join(WORKSPACE_DIR, "IDENTITY.md"))).toBeDefined();
333
342
  });
334
343
 
344
+ test("start does not watch the skills directory", () => {
345
+ mkdirSync(join(WORKSPACE_DIR, "skills"), { recursive: true });
346
+ watcher.start();
347
+ expect(findWatcher(join(WORKSPACE_DIR, "skills"))).toBeUndefined();
348
+ });
349
+
335
350
  test("stop cancels pending debounce work, no eviction fires after", async () => {
336
351
  watcher.start();
337
352
  simulateFileChange(WORKSPACE_DIR, "SOUL.md");
@@ -19,7 +19,7 @@ describe("isConnectionCompatibleWithModel", () => {
19
19
  expect(
20
20
  isConnectionCompatibleWithModel(
21
21
  { auth: { type: "oauth_subscription" } as never },
22
- "gpt-5.4",
22
+ "gpt-5.5",
23
23
  ),
24
24
  ).toBe(true);
25
25
  });
@@ -79,7 +79,7 @@ describe("describeSubscriptionModelIncompatibility", () => {
79
79
 
80
80
  test("returns null when model is Codex-compatible", () => {
81
81
  expect(
82
- describeSubscriptionModelIncompatibility([subscriptionConn], "gpt-5.4"),
82
+ describeSubscriptionModelIncompatibility([subscriptionConn], "gpt-5.5"),
83
83
  ).toBeNull();
84
84
  });
85
85
  });
@@ -59,7 +59,7 @@ describe("conversation-key-store disk view", () => {
59
59
 
60
60
  const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
61
61
  expect(meta.id).toBe(first.conversationId);
62
- expect(meta.title).toBe("Generating title...");
62
+ expect(meta.title).toBe("conversation.title.generating");
63
63
  expect(meta.type).toBe("standard");
64
64
  expect(meta.channel).toBeNull();
65
65
  expect(readdirSync(conversationsDir)).toEqual([expectedDirName]);
@@ -901,6 +901,32 @@ describe("pairDeliveryWithConversation", () => {
901
901
  // body has to land there for that button to open something that contains
902
902
  // the notification the user tapped.
903
903
 
904
+ test("assistant reply keeps the source conversation without appending its preview", async () => {
905
+ mockExistingConversations["conv-user-chat"] = {
906
+ id: "conv-user-chat",
907
+ source: "user",
908
+ title: "An ordinary chat",
909
+ };
910
+ const signal = makeSignal({
911
+ requiresConversation: undefined,
912
+ sourceContextId: "conv-user-chat",
913
+ sourceEventName: "chat.assistant_reply",
914
+ });
915
+
916
+ const result = await pairDeliveryWithConversation(
917
+ signal,
918
+ "vellum" as NotificationChannel,
919
+ makeCopy({ body: "A short push preview…" }),
920
+ );
921
+
922
+ expect(result.conversationId).toBe("conv-user-chat");
923
+ expect(result.messageId).toBeNull();
924
+ expect(result.createdNewConversation).toBe(false);
925
+ expect(createConversationMock).not.toHaveBeenCalled();
926
+ expect(addMessageMock).not.toHaveBeenCalled();
927
+ expect(messagesInvalidated).toEqual([]);
928
+ });
929
+
904
930
  test("passive vellum signal appends the body to the producing conversation", async () => {
905
931
  mockExistingConversations["conv-producer"] = {
906
932
  id: "conv-producer",