@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
@@ -28,6 +28,9 @@ describe("model intents", () => {
28
28
  expect(resolveModelIntent("openai", "latency-optimized")).toBe(
29
29
  "gpt-5.6-luna",
30
30
  );
31
+ expect(resolveModelIntent("gemini", "cost-optimized")).toBe(
32
+ "gemini-3.1-flash-lite",
33
+ );
31
34
  expect(resolveModelIntent("gemini", "latency-optimized")).toBe(
32
35
  "gemini-3.1-flash-lite",
33
36
  );
@@ -429,7 +429,7 @@ describe("Outlook messaging provider", () => {
429
429
  describe("sendMessage", () => {
430
430
  test("sends a new message with correct recipient, subject, and body", async () => {
431
431
  const conn = createMockConnection();
432
- const result = await outlookMessagingProvider.sendMessage(
432
+ const result = await outlookMessagingProvider.sendMessage!(
433
433
  conn,
434
434
  "recipient@example.com",
435
435
  "Hello!",
@@ -452,7 +452,7 @@ describe("Outlook messaging provider", () => {
452
452
 
453
453
  test("uses empty string for subject when not provided", async () => {
454
454
  const conn = createMockConnection();
455
- await outlookMessagingProvider.sendMessage(
455
+ await outlookMessagingProvider.sendMessage!(
456
456
  conn,
457
457
  "recipient@example.com",
458
458
  "Hello!",
@@ -471,7 +471,7 @@ describe("Outlook messaging provider", () => {
471
471
 
472
472
  test("calls replyToMessage when inReplyTo is provided", async () => {
473
473
  const conn = createMockConnection();
474
- const result = await outlookMessagingProvider.sendMessage(
474
+ const result = await outlookMessagingProvider.sendMessage!(
475
475
  conn,
476
476
  "conv-id",
477
477
  "Reply text",
@@ -493,7 +493,7 @@ describe("Outlook messaging provider", () => {
493
493
 
494
494
  test("attaches files to a new message as Graph file attachments", async () => {
495
495
  const conn = createMockConnection();
496
- await outlookMessagingProvider.sendMessage(
496
+ await outlookMessagingProvider.sendMessage!(
497
497
  conn,
498
498
  "recipient@example.com",
499
499
  "See attached",
@@ -530,7 +530,7 @@ describe("Outlook messaging provider", () => {
530
530
 
531
531
  test("forwards attachments to replyToMessage when replying", async () => {
532
532
  const conn = createMockConnection();
533
- await outlookMessagingProvider.sendMessage(
533
+ await outlookMessagingProvider.sendMessage!(
534
534
  conn,
535
535
  "conv-id",
536
536
  "Reply body",
@@ -563,7 +563,7 @@ describe("Outlook messaging provider", () => {
563
563
 
564
564
  test("throws when connection is undefined", async () => {
565
565
  await expect(
566
- outlookMessagingProvider.sendMessage(
566
+ outlookMessagingProvider.sendMessage!(
567
567
  undefined,
568
568
  "recipient@example.com",
569
569
  "Hello!",
@@ -19,7 +19,9 @@ mock.module("../providers/platform-proxy/context.js", () => ({
19
19
  }),
20
20
  }));
21
21
 
22
+ const actualEnv = await import("../config/env.js");
22
23
  mock.module("../config/env.js", () => ({
24
+ ...actualEnv,
23
25
  getPlatformAssistantId: () => platformAssistantId,
24
26
  }));
25
27
 
@@ -129,6 +129,9 @@ const BASELINE: Record<string, readonly string[]> = {
129
129
  "../../../../daemon/embedding-reconcile.js",
130
130
  "../../../../daemon/trust-context.js",
131
131
  "../../../../daemon/turn-latency-sub-spans.js",
132
+ // mcp-setup capability cards list configured server names from the
133
+ // workspace mcp.json document. No plugin-api equivalent.
134
+ "../../../../mcp/workspace-mcp-config.js",
132
135
  "../../../../notifications/background-failure-signal.js",
133
136
  "../../../../persistence/checkpoints.js",
134
137
  "../../../../persistence/conversation-types.js",
@@ -210,6 +213,16 @@ const BASELINE: Record<string, readonly string[]> = {
210
213
  "../../../persistence/job-handlers/message-lexical.js",
211
214
  "../../../persistence/job-utils.js",
212
215
  "../../../persistence/jobs-store.js",
216
+ // The retrospective cursor is a `(createdAt, id)` bound that must keep
217
+ // working after its message row is deleted. The resolver lives beside
218
+ // the persistence after-reads it also drives, so the plugin's
219
+ // accounting imports it rather than re-deriving the bound. No
220
+ // plugin-api equivalent.
221
+ "../../../persistence/message-cursor.js",
222
+ // Named any-state `createdAt` lookup the cursor bookkeeping uses for
223
+ // forks and the timestamp backfill; the messages-read boundary guard
224
+ // requires the read to live in persistence. No plugin-api equivalent.
225
+ "../../../persistence/message-reads.js",
213
226
  "../../../persistence/raw-query.js",
214
227
  "../../../persistence/schema/index.js",
215
228
  "../../../prompts/persona-resolver.js",
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Tests for resolving an app query by name, slug, or id.
3
+ */
4
+
5
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { join } from "node:path";
8
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
9
+
10
+ import { createApp } from "../apps/app-store.js";
11
+ import { resolveAppQuery } from "../apps/resolve-app.js";
12
+ import { getWorkspacePluginsDir } from "../util/platform.js";
13
+
14
+ let workspaceDir: string;
15
+ let previousWorkspaceDir: string | undefined;
16
+
17
+ function freshWorkspace(): string {
18
+ return join(
19
+ tmpdir(),
20
+ `vellum-resolve-app-${Date.now()}-${Math.random().toString(36).slice(2)}`,
21
+ );
22
+ }
23
+
24
+ function installPlugin(name: string): string {
25
+ const pluginDir = join(getWorkspacePluginsDir(), name);
26
+ mkdirSync(pluginDir, { recursive: true });
27
+ writeFileSync(
28
+ join(pluginDir, "package.json"),
29
+ JSON.stringify({ name, version: "1.0.0" }),
30
+ );
31
+ return pluginDir;
32
+ }
33
+
34
+ function bundleApp(pluginDir: string, app: string): void {
35
+ const appDir = join(pluginDir, "apps", app);
36
+ mkdirSync(appDir, { recursive: true });
37
+ writeFileSync(join(appDir, "index.html"), "<h1>Plugin app</h1>");
38
+ }
39
+
40
+ beforeEach(() => {
41
+ previousWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR;
42
+ workspaceDir = freshWorkspace();
43
+ process.env.VELLUM_WORKSPACE_DIR = workspaceDir;
44
+ });
45
+
46
+ afterEach(() => {
47
+ rmSync(workspaceDir, { recursive: true, force: true });
48
+ if (previousWorkspaceDir === undefined) {
49
+ delete process.env.VELLUM_WORKSPACE_DIR;
50
+ } else {
51
+ process.env.VELLUM_WORKSPACE_DIR = previousWorkspaceDir;
52
+ }
53
+ });
54
+
55
+ describe("resolveAppQuery", () => {
56
+ test("resolves a workspace app by display name, slug, and id", () => {
57
+ const created = createApp({
58
+ name: "Budget",
59
+ schemaJson: "{}",
60
+ htmlDefinition: "<h1>Budget</h1>",
61
+ });
62
+
63
+ const byName = resolveAppQuery("Budget");
64
+ expect(byName.ok).toBe(true);
65
+ if (byName.ok) {
66
+ expect(byName.app.id).toBe(created.id);
67
+ }
68
+
69
+ const byNameCase = resolveAppQuery("budget");
70
+ expect(byNameCase.ok).toBe(true);
71
+ if (byNameCase.ok) {
72
+ expect(byNameCase.app.id).toBe(created.id);
73
+ }
74
+
75
+ const slug = created.dirName ?? "";
76
+ expect(slug.length).toBeGreaterThan(0);
77
+ const bySlug = resolveAppQuery(slug);
78
+ expect(bySlug.ok).toBe(true);
79
+ if (bySlug.ok) {
80
+ expect(bySlug.app.id).toBe(created.id);
81
+ }
82
+
83
+ const byId = resolveAppQuery(created.id);
84
+ expect(byId.ok).toBe(true);
85
+ if (byId.ok) {
86
+ expect(byId.app.id).toBe(created.id);
87
+ }
88
+ });
89
+
90
+ test("returns not_found for an unknown name", () => {
91
+ const result = resolveAppQuery("missing");
92
+ expect(result).toEqual({
93
+ ok: false,
94
+ reason: "not_found",
95
+ query: "missing",
96
+ });
97
+ });
98
+
99
+ test("returns ambiguous when two apps share a display name", () => {
100
+ const first = createApp({
101
+ name: "Notes",
102
+ schemaJson: "{}",
103
+ htmlDefinition: "<h1>One</h1>",
104
+ });
105
+ const second = createApp({
106
+ name: "Notes",
107
+ schemaJson: "{}",
108
+ htmlDefinition: "<h1>Two</h1>",
109
+ });
110
+
111
+ const result = resolveAppQuery("Notes");
112
+ expect(result.ok).toBe(false);
113
+ if (!result.ok && result.reason === "ambiguous") {
114
+ const ids = result.matches.map((m) => m.id).sort();
115
+ expect(ids).toEqual([first.id, second.id].sort());
116
+ } else {
117
+ throw new Error("expected ambiguous resolution");
118
+ }
119
+ });
120
+
121
+ test("resolves a plugin app by directory name", () => {
122
+ const pluginDir = installPlugin("charts");
123
+ bundleApp(pluginDir, "viewer");
124
+
125
+ const result = resolveAppQuery("viewer");
126
+ expect(result.ok).toBe(true);
127
+ if (result.ok) {
128
+ expect(result.app.origin).toEqual({
129
+ kind: "plugin",
130
+ pluginName: "charts",
131
+ });
132
+ expect(result.app.id).toBe("plugins~charts~viewer");
133
+ }
134
+ });
135
+ });
@@ -8,6 +8,7 @@ import {
8
8
  getCAPath,
9
9
  issueLeafCert,
10
10
  } from "../outbound-proxy/index.js";
11
+ import { writeCombinedCABundle } from "../util/ca-bundle.js";
11
12
 
12
13
  let dataDir: string;
13
14
 
@@ -19,6 +20,40 @@ afterAll(async () => {
19
20
  await rm(dataDir, { recursive: true, force: true });
20
21
  });
21
22
 
23
+ test("combined trust verifies certificates from both CA sources", async () => {
24
+ const roots = [join(dataDir, "system"), join(dataDir, "extra")];
25
+ const leaves: string[] = [];
26
+ for (const root of roots) {
27
+ await ensureLocalCA(root);
28
+ const caDir = join(root, "proxy-ca");
29
+ await issueLeafCert(caDir, "example.com");
30
+ leaves.push(join(caDir, "issued", "example.com.pem"));
31
+ }
32
+ const bundle = join(dataDir, "combined.pem");
33
+ await writeCombinedCABundle(
34
+ getCAPath(roots[0]!),
35
+ getCAPath(roots[1]!),
36
+ bundle,
37
+ );
38
+ const verify = Bun.spawn(
39
+ ["openssl", "verify", "-CAfile", bundle, ...leaves],
40
+ {
41
+ stdout: "pipe",
42
+ stderr: "pipe",
43
+ windowsHide: true,
44
+ },
45
+ );
46
+ const [code, output, error] = await Promise.all([
47
+ verify.exited,
48
+ new Response(verify.stdout).text(),
49
+ new Response(verify.stderr).text(),
50
+ ]);
51
+ expect({ code, error }).toEqual({ code: 0, error: "" });
52
+ for (const leaf of leaves) {
53
+ expect(output).toContain(`${leaf}: OK`);
54
+ }
55
+ });
56
+
22
57
  describe("ensureLocalCA", () => {
23
58
  test("creates CA cert and key with correct permissions", async () => {
24
59
  await ensureLocalCA(dataDir);
@@ -60,7 +60,7 @@ function makeConfig(overrides: Partial<AssistantConfig> = {}): AssistantConfig {
60
60
  return {
61
61
  skills: {
62
62
  entries: {},
63
- load: { extraDirs: [], watch: true, watchDebounceMs: 250 },
63
+ load: { extraDirs: [] },
64
64
  install: { nodeManager: "npm" },
65
65
  allowBundled: null,
66
66
  remoteProviders: {
@@ -28,7 +28,7 @@ function makeConfig(overrides: Partial<AssistantConfig> = {}): AssistantConfig {
28
28
  return {
29
29
  skills: {
30
30
  entries: {},
31
- load: { extraDirs: [], watch: true, watchDebounceMs: 250 },
31
+ load: { extraDirs: [] },
32
32
  install: { nodeManager: "npm" },
33
33
  allowBundled: null,
34
34
  remoteProviders: {
@@ -233,7 +233,7 @@ describe("resolveSkillStates with feature flags", () => {
233
233
  const config = makeConfig({
234
234
  skills: {
235
235
  entries: { [DECLARED_SKILL_ID]: { enabled: true } },
236
- load: { extraDirs: [], watch: true, watchDebounceMs: 250 },
236
+ load: { extraDirs: [] },
237
237
  install: { nodeManager: "npm" },
238
238
  allowBundled: null,
239
239
  remoteProviders: {
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Tests for app source fingerprinting: walk exclusions, comparison, inspect
3
+ * statuses, and the compile-time sidecar format.
4
+ */
5
+
6
+ import {
7
+ mkdirSync,
8
+ mkdtempSync,
9
+ readFileSync,
10
+ rmSync,
11
+ symlinkSync,
12
+ writeFileSync,
13
+ } from "node:fs";
14
+ import { tmpdir } from "node:os";
15
+ import { join } from "node:path";
16
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
17
+
18
+ import {
19
+ compareAppSourceFingerprint,
20
+ computeAppSourceFingerprint,
21
+ inspectAppSource,
22
+ parseAppSourceFingerprint,
23
+ SOURCE_FINGERPRINT_FILENAME,
24
+ writeAppSourceFingerprint,
25
+ } from "../apps/source-fingerprint.js";
26
+
27
+ let root: string;
28
+
29
+ beforeEach(() => {
30
+ root = mkdtempSync(join(tmpdir(), "app-source-fingerprint-"));
31
+ });
32
+
33
+ afterEach(() => {
34
+ rmSync(root, { recursive: true, force: true });
35
+ });
36
+
37
+ function write(rel: string, contents: string): void {
38
+ const abs = join(root, rel);
39
+ mkdirSync(join(abs, ".."), { recursive: true });
40
+ writeFileSync(abs, contents);
41
+ }
42
+
43
+ describe("computeAppSourceFingerprint", () => {
44
+ test("digests source files with POSIX-relative keys", () => {
45
+ write("src/main.tsx", "export const x = 1;");
46
+ write("src/index.html", "<html></html>");
47
+
48
+ const fp = computeAppSourceFingerprint(root);
49
+
50
+ expect(Object.keys(fp.files).sort()).toEqual([
51
+ "src/index.html",
52
+ "src/main.tsx",
53
+ ]);
54
+ expect(fp.algorithm).toBe("sha256");
55
+ expect(fp.files["src/main.tsx"]).toMatch(/^[0-9a-f]{64}$/);
56
+ });
57
+
58
+ test("skips dist, records, node_modules, and .git", () => {
59
+ write("src/main.tsx", "src");
60
+ write("dist/main.js", "compiled");
61
+ write("records/rec-1.json", "{}");
62
+ write("node_modules/pkg/index.js", "dep");
63
+ write("src/node_modules/nested/index.js", "nested");
64
+ write(".git/HEAD", "ref");
65
+ write("src/.git/config", "nope");
66
+
67
+ const fp = computeAppSourceFingerprint(root);
68
+
69
+ expect(Object.keys(fp.files)).toEqual(["src/main.tsx"]);
70
+ });
71
+
72
+ test("skips symlinks", () => {
73
+ write("src/real.tsx", "hello");
74
+ mkdirSync(join(root, "src"), { recursive: true });
75
+ symlinkSync(join(root, "src/real.tsx"), join(root, "src/link.tsx"));
76
+
77
+ const fp = computeAppSourceFingerprint(root);
78
+
79
+ expect(Object.keys(fp.files)).toEqual(["src/real.tsx"]);
80
+ });
81
+ });
82
+
83
+ describe("compareAppSourceFingerprint", () => {
84
+ test("reports modified, added, and removed paths", () => {
85
+ write("src/main.tsx", "one");
86
+ write("src/keep.tsx", "keep");
87
+ const baseline = computeAppSourceFingerprint(root);
88
+
89
+ write("src/main.tsx", "two");
90
+ write("src/new.tsx", "new");
91
+ rmSync(join(root, "src/keep.tsx"));
92
+
93
+ const diff = compareAppSourceFingerprint(root, baseline);
94
+ expect(diff.clean).toBe(false);
95
+ expect(diff.modified).toEqual(["src/main.tsx"]);
96
+ expect(diff.added).toEqual(["src/new.tsx"]);
97
+ expect(diff.removed).toEqual(["src/keep.tsx"]);
98
+ });
99
+
100
+ test("is clean when source matches the baseline", () => {
101
+ write("src/main.tsx", "same");
102
+ const baseline = computeAppSourceFingerprint(root);
103
+ const diff = compareAppSourceFingerprint(root, baseline);
104
+ expect(diff.clean).toBe(true);
105
+ expect(diff.modified).toEqual([]);
106
+ expect(diff.added).toEqual([]);
107
+ expect(diff.removed).toEqual([]);
108
+ });
109
+ });
110
+
111
+ describe("inspectAppSource", () => {
112
+ test("reports never_compiled when dist index is missing even if a fingerprint exists", () => {
113
+ write("src/main.tsx", "src");
114
+ mkdirSync(join(root, "dist"), { recursive: true });
115
+ writeAppSourceFingerprint(root, join(root, "dist"));
116
+
117
+ const result = inspectAppSource(root);
118
+ expect(result.status).toBe("never_compiled");
119
+ });
120
+
121
+ test("reports unknown_baseline when dist exists without a fingerprint", () => {
122
+ write("src/main.tsx", "src");
123
+ write("dist/index.html", "<html></html>");
124
+ const result = inspectAppSource(root);
125
+ expect(result.status).toBe("unknown_baseline");
126
+ });
127
+
128
+ test("reports clean after writing a fingerprint that matches source", () => {
129
+ write("src/main.tsx", "src");
130
+ mkdirSync(join(root, "dist"), { recursive: true });
131
+ writeFileSync(join(root, "dist", "index.html"), "<html></html>");
132
+ writeAppSourceFingerprint(root, join(root, "dist"));
133
+
134
+ const result = inspectAppSource(root);
135
+ expect(result.status).toBe("clean");
136
+ expect(result.compiledAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
137
+ });
138
+
139
+ test("reports stale after a source edit", () => {
140
+ write("src/main.tsx", "before");
141
+ mkdirSync(join(root, "dist"), { recursive: true });
142
+ writeFileSync(join(root, "dist", "index.html"), "<html></html>");
143
+ writeAppSourceFingerprint(root, join(root, "dist"));
144
+ write("src/main.tsx", "after");
145
+
146
+ const result = inspectAppSource(root);
147
+ expect(result.status).toBe("stale");
148
+ expect(result.modified).toEqual(["src/main.tsx"]);
149
+ });
150
+ });
151
+
152
+ describe("parseAppSourceFingerprint", () => {
153
+ test("accepts a compile sidecar with compiledAt", () => {
154
+ const parsed = parseAppSourceFingerprint({
155
+ algorithm: "sha256",
156
+ compiledAt: "2026-09-09T00:00:00.000Z",
157
+ files: { "src/main.tsx": "abc" },
158
+ });
159
+ expect(parsed).toEqual({
160
+ algorithm: "sha256",
161
+ compiledAt: "2026-09-09T00:00:00.000Z",
162
+ files: { "src/main.tsx": "abc" },
163
+ });
164
+ });
165
+
166
+ test("returns null for an invalid sidecar", () => {
167
+ expect(parseAppSourceFingerprint({ algorithm: "md5", files: {} })).toBe(
168
+ null,
169
+ );
170
+ expect(parseAppSourceFingerprint(null)).toBeNull();
171
+ });
172
+
173
+ test("writeAppSourceFingerprint records a sidecar the parser accepts", () => {
174
+ write("src/main.tsx", "src");
175
+ mkdirSync(join(root, "dist"), { recursive: true });
176
+ writeAppSourceFingerprint(root, join(root, "dist"));
177
+ const raw = JSON.parse(
178
+ readFileSync(join(root, "dist", SOURCE_FINGERPRINT_FILENAME), "utf-8"),
179
+ );
180
+ expect(parseAppSourceFingerprint(raw)?.files["src/main.tsx"]).toMatch(
181
+ /^[0-9a-f]{64}$/,
182
+ );
183
+ });
184
+ });
@@ -249,14 +249,23 @@ describe("createResolveToolsCallback — toolContextPin", () => {
249
249
  });
250
250
  }
251
251
 
252
- test("control: without a pin, a clientless fork drops every client-gated tool from the wire", () => {
252
+ test("control: without a pin, a clientless fork drops client-gated tools but keeps host and UI tools", () => {
253
253
  projectedSkillToolNames = [];
254
254
  const resolve = createResolveToolsCallback(
255
255
  CLIENT_GATED_DEFS,
256
256
  clientlessExecutionCtx(),
257
257
  )!;
258
258
 
259
- expect(resolve(EMPTY_HISTORY).map((t) => t.name)).toEqual(["remember"]);
259
+ // Host tool definitions stay on the wire for background turns, and
260
+ // ui_show stays because background UI surfaces persist and return. The
261
+ // executor rejects host tools on explicitly non-interactive turns before
262
+ // dispatch; ask_question and request_system_permission remain
263
+ // client-gated at resolution.
264
+ expect(resolve(EMPTY_HISTORY).map((t) => t.name)).toEqual([
265
+ "remember",
266
+ "host_bash",
267
+ "ui_show",
268
+ ]);
260
269
  });
261
270
 
262
271
  test("a desktop-source pin restores the host/UI/client tool defs on the wire", () => {
@@ -291,7 +300,14 @@ describe("createResolveToolsCallback — toolContextPin", () => {
291
300
  }),
292
301
  )!;
293
302
 
294
- expect(resolve(EMPTY_HISTORY).map((t) => t.name)).toEqual(["remember"]);
303
+ // Host tool definitions stay on the wire for background turns, and
304
+ // ui_show survives the clientless pin: it persists and returns. The
305
+ // executor rejects host tools on explicitly non-interactive turns.
306
+ expect(resolve(EMPTY_HISTORY).map((t) => t.name)).toEqual([
307
+ "remember",
308
+ "host_bash",
309
+ "ui_show",
310
+ ]);
295
311
  });
296
312
 
297
313
  test("invariant: a pinned-in tool is on the wire but can never execute", async () => {
@@ -3447,8 +3447,9 @@ describe("Subagent advisor-role consult", () => {
3447
3447
  });
3448
3448
 
3449
3449
  test("advisor consults still run when the parent conversation is gone", async () => {
3450
- // The parent is looked up only for its warm skill catalog, so an evicted
3451
- // conversation costs a section of the context pack, not the consult.
3450
+ // Advisor consults do not look up the parent conversation for a skill
3451
+ // catalog snapshot. An evicted parent still gets a consult; the context
3452
+ // pack loads the catalog from disk independently.
3452
3453
  mockFindConversation = () => undefined;
3453
3454
  const { captured, restore } = stubSpawn();
3454
3455
  try {
@@ -455,10 +455,11 @@ describe("resolveCallTtsProvider with requiresPcmAudio", () => {
455
455
  expect(result.provider?.id).toBe("elevenlabs");
456
456
  });
457
457
 
458
- test("without requiresPcmAudio the configured provider is not swapped", async () => {
458
+ test("without requiresPcmAudio the configured provider is not swapped for playability", async () => {
459
459
  testConfig.services.tts.provider = "deepgram";
460
- // No credentials at all — the CR transport path does not consult the
461
- // media-stream playability capability.
460
+ storedKeys["deepgram"] = "dg-key";
461
+ // The CR transport path does not consult the media-stream playability
462
+ // capability.
462
463
  registerStubProvider("deepgram", async () => {
463
464
  return { audio: Buffer.from("x"), contentType: "audio/mpeg" };
464
465
  });
@@ -468,6 +469,37 @@ describe("resolveCallTtsProvider with requiresPcmAudio", () => {
468
469
  expect(result.useSynthesizedPath).toBe(true);
469
470
  expect(result.audioFormat).toBe("mp3");
470
471
  });
472
+
473
+ test("managed speech stands in for a configured provider whose credential is missing", async () => {
474
+ testConfig.services.tts.provider = "deepgram";
475
+ // No deepgram key, managed speech available: the phone speaks through
476
+ // managed speech, the same substitution live voice makes.
477
+ registerStubProvider("deepgram", async () => {
478
+ throw new Error("should not be used");
479
+ });
480
+ registerStubProvider("vellum", async () => {
481
+ return { audio: Buffer.from("x"), contentType: "audio/mpeg" };
482
+ });
483
+
484
+ const result = await resolveCallTtsProvider();
485
+ expect(result.provider?.id).toBe("vellum");
486
+ expect(result.useSynthesizedPath).toBe(true);
487
+ });
488
+
489
+ test("without managed speech a credential-less configured provider is kept", async () => {
490
+ mockManagedSpeechAvailable = false;
491
+ try {
492
+ testConfig.services.tts.provider = "deepgram";
493
+ registerStubProvider("deepgram", async () => {
494
+ return { audio: Buffer.from("x"), contentType: "audio/mpeg" };
495
+ });
496
+
497
+ const result = await resolveCallTtsProvider();
498
+ expect(result.provider?.id).toBe("deepgram");
499
+ } finally {
500
+ mockManagedSpeechAvailable = true;
501
+ }
502
+ });
471
503
  });
472
504
 
473
505
  // ---------------------------------------------------------------------------
@@ -584,6 +616,7 @@ describe("speakSystemPrompt mid-stream failure after audio started", () => {
584
616
  test("non-PCM transport: skips the native text fallback — no full-prompt re-speak", async () => {
585
617
  testConfig.services.tts.provider = "fish-audio";
586
618
  testConfig.services.tts.providers["fish-audio"].referenceId = "ref-123";
619
+ storedKeys["fish-audio"] = "fa-key";
587
620
 
588
621
  const fishStream = jest.fn(failAfterFirstChunk());
589
622
  registerStreamingStubProvider("fish-audio", fishStream);
@@ -657,6 +690,7 @@ describe("speakSystemPrompt aborted synthesis", () => {
657
690
  test("non-PCM transport with native-fallback provider: abort skips the text fallback and end-of-turn", async () => {
658
691
  testConfig.services.tts.provider = "fish-audio";
659
692
  testConfig.services.tts.providers["fish-audio"].referenceId = "ref-123";
693
+ storedKeys["fish-audio"] = "fa-key";
660
694
 
661
695
  const controller = new AbortController();
662
696
  controller.abort();
@@ -742,6 +776,7 @@ describe("speakSystemPrompt aborted synthesis", () => {
742
776
  test("provider AbortError without any signal is a failure — non-PCM native fallback still runs", async () => {
743
777
  testConfig.services.tts.provider = "fish-audio";
744
778
  testConfig.services.tts.providers["fish-audio"].referenceId = "ref-123";
779
+ storedKeys["fish-audio"] = "fa-key";
745
780
 
746
781
  const fishSynthesize = jest.fn(async () => {
747
782
  throw new DOMException("socket torn down", "AbortError");
@@ -136,6 +136,10 @@ describe("vellum-self-knowledge skill", () => {
136
136
  const result = await executeSkillLoad({ skill: "vellum-self-knowledge" });
137
137
  expect(result.content).toContain("## Critical Rule");
138
138
  expect(result.content).toContain("Never answer from memory");
139
+ expect(result.content).toContain("distinct, legacy Vellum product");
140
+ expect(result.content).toContain(
141
+ "Never use it to answer questions about this Vellum",
142
+ );
139
143
  });
140
144
 
141
145
  // ── Resolution order ─────────────────────────────────────────────────