@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
@@ -1,19 +1,19 @@
1
1
  /**
2
2
  * File watchers and config reload logic extracted from DaemonServer.
3
- * Watches workspace files (config, prompts) and skills directories
4
- * for changes.
3
+ * Watches workspace files (config, prompts, signals, avatar, sounds).
4
+ * Skill capability cards reseed from SKILL.md mtimes in the memory worker
5
+ * and the assistant process, not from this watcher.
5
6
  */
6
7
  import {
7
8
  existsSync,
8
9
  type FSWatcher,
9
10
  mkdirSync,
10
- readdirSync,
11
11
  readFileSync,
12
12
  unwatchFile,
13
13
  watch,
14
14
  watchFile,
15
15
  } from "node:fs";
16
- import { basename, join } from "node:path";
16
+ import { join } from "node:path";
17
17
 
18
18
  import { AVATAR_IMAGE_FILENAME } from "@vellumai/avatar-manifest";
19
19
 
@@ -44,50 +44,14 @@ import {
44
44
  getSoundsDir,
45
45
  getWorkspaceDir,
46
46
  getWorkspacePromptPath,
47
- getWorkspaceSkillsDir,
47
+ WORKSPACE_MCP_FILENAME,
48
48
  } from "../util/platform.js";
49
49
  import { evictConversationsForReload } from "./conversation-store.js";
50
50
  import { parseIdentityFields } from "./handlers/identity.js";
51
51
  import { reloadMcpServers } from "./mcp-reload-service.js";
52
- import { refreshSkillCapabilityMemories } from "./skill-memory-refresh.js";
53
52
 
54
53
  const log = getLogger("config-watcher");
55
54
 
56
- const SKILL_WATCH_SKIPPED_DIRS = new Set([
57
- "node_modules",
58
- ".git",
59
- "__pycache__",
60
- ".install-staging",
61
- ".cache",
62
- ".next",
63
- ".turbo",
64
- ".venv",
65
- "coverage",
66
- ]);
67
-
68
- function isSkippedSkillWatchPath(relativePath: string): boolean {
69
- if (relativePath === "(unknown)") {
70
- return false;
71
- }
72
-
73
- const segments = relativePath.split(/[\\/]+/).filter(Boolean);
74
- return segments.some((segment) => SKILL_WATCH_SKIPPED_DIRS.has(segment));
75
- }
76
-
77
- /**
78
- * Files whose contents feed the skill catalog: `SKILL.md` carries a skill's
79
- * metadata and body, `TOOLS.json` its tool manifest. Both live at the top
80
- * level of a skill directory. Every other file (scripts, reference docs, build
81
- * output) is read fresh on demand or irrelevant to the catalog, so changes to
82
- * them do not require a catalog reload.
83
- */
84
- const SKILL_CATALOG_FILENAMES = new Set(["SKILL.md", "TOOLS.json"]);
85
-
86
- /** True when a changed path's basename is a skill-catalog file. */
87
- function isSkillCatalogFile(filename: string): boolean {
88
- return SKILL_CATALOG_FILENAMES.has(basename(filename));
89
- }
90
-
91
55
  /**
92
56
  * Attach a resilient error handler to an FSWatcher so that async errors
93
57
  * (e.g. ENXIO when a Unix socket file like `gateway.sock` appears in a
@@ -227,19 +191,11 @@ export class ConfigWatcher {
227
191
  return;
228
192
  }
229
193
  try {
230
- const prevMcpFingerprint = JSON.stringify(this.lastConfig?.mcp ?? {});
231
194
  const changed = await this.refreshConfigFromSources();
232
195
  if (changed) {
233
196
  evictConversationsForReload();
234
197
  refreshAuthenticatedApiRateLimit();
235
198
  publishConfigChanged();
236
- const newConfig = this.lastConfig ?? getConfig();
237
- const newMcpFingerprint = JSON.stringify(newConfig.mcp ?? {});
238
- if (newMcpFingerprint !== prevMcpFingerprint) {
239
- reloadMcpServers().catch((err: unknown) => {
240
- log.error({ err }, "MCP reload after config change failed");
241
- });
242
- }
243
199
  }
244
200
  } catch (err) {
245
201
  log.error(
@@ -248,6 +204,14 @@ export class ConfigWatcher {
248
204
  );
249
205
  }
250
206
  },
207
+ [WORKSPACE_MCP_FILENAME]: () => {
208
+ if (this.suppressReload) {
209
+ return;
210
+ }
211
+ reloadMcpServers().catch((err: unknown) => {
212
+ log.error({ err }, "MCP reload after mcp.json change failed");
213
+ });
214
+ },
251
215
  "SOUL.md": () => {
252
216
  evictConversationsForReload();
253
217
  },
@@ -270,7 +234,6 @@ export class ConfigWatcher {
270
234
  this.startAvatarWatcher();
271
235
  this.startSignalsWatcher();
272
236
  this.startUsersWatcher();
273
- this.startSkillsWatchers();
274
237
  }
275
238
 
276
239
  stop(): void {
@@ -485,150 +448,6 @@ export class ConfigWatcher {
485
448
  );
486
449
  }
487
450
  }
488
-
489
- private startSkillsWatchers(): void {
490
- const skillsDir = getWorkspaceSkillsDir();
491
- if (!existsSync(skillsDir)) {
492
- return;
493
- }
494
-
495
- const scheduleSkillsReload = (file: string): void => {
496
- if (isSkippedSkillWatchPath(file)) {
497
- return;
498
- }
499
-
500
- this.debounceTimers.schedule("skills:catalog", () => {
501
- log.info({ file }, "Skill catalog changed, reloading");
502
- evictConversationsForReload();
503
- refreshSkillCapabilityMemories(getConfig());
504
- });
505
- };
506
-
507
- // Only SKILL.md and TOOLS.json feed the skill catalog, and both live at the
508
- // top level of a skill directory. Changes to any other file (scripts,
509
- // reference docs, build output) never alter the catalog, so the watchers
510
- // react only to those two files — plus skill directories appearing or
511
- // disappearing, which is an install or removal.
512
- try {
513
- const recursiveWatcher = watch(
514
- skillsDir,
515
- { recursive: true },
516
- (_eventType, filename) => {
517
- if (!filename || isSkillCatalogFile(String(filename))) {
518
- scheduleSkillsReload(filename ? String(filename) : "(unknown)");
519
- }
520
- },
521
- );
522
- attachWatcherErrorHandler(recursiveWatcher, skillsDir);
523
- this.watchers.push(recursiveWatcher);
524
- log.info({ dir: skillsDir }, "Watching skills catalog recursively");
525
- return;
526
- } catch (err) {
527
- log.info(
528
- { err, dir: skillsDir },
529
- "Recursive skills watch unavailable; using per-skill-directory watchers",
530
- );
531
- }
532
-
533
- // Fallback when recursive watches are unavailable: watch the skills root
534
- // (to detect a skill being installed or removed) plus each immediate skill
535
- // directory (to detect SKILL.md / TOOLS.json edits). Nested subdirectories
536
- // are deliberately not watched, bounding the descriptor count to one per
537
- // skill rather than one per subdirectory.
538
- const skillDirWatchers = new Map<string, FSWatcher>();
539
-
540
- const watchDir = (
541
- dirPath: string,
542
- onChange: (filename: string | null) => void,
543
- ): FSWatcher | null => {
544
- try {
545
- const watcher = watch(dirPath, (_eventType, filename) => {
546
- onChange(filename ? String(filename) : null);
547
- });
548
- attachWatcherErrorHandler(watcher, dirPath);
549
- this.watchers.push(watcher);
550
- return watcher;
551
- } catch (err) {
552
- log.warn({ err, dirPath }, "Failed to watch skill directory");
553
- return null;
554
- }
555
- };
556
-
557
- const removeWatcher = (watcher: FSWatcher): void => {
558
- const idx = this.watchers.indexOf(watcher);
559
- if (idx !== -1) {
560
- this.watchers.splice(idx, 1);
561
- }
562
- };
563
-
564
- const listSkillDirs = (): string[] => {
565
- try {
566
- return readdirSync(skillsDir, { withFileTypes: true })
567
- .filter(
568
- (entry) =>
569
- entry.isDirectory() && !SKILL_WATCH_SKIPPED_DIRS.has(entry.name),
570
- )
571
- .map((entry) => join(skillsDir, entry.name));
572
- } catch (err) {
573
- log.warn({ err, skillsDir }, "Failed to enumerate skill directories");
574
- return [];
575
- }
576
- };
577
-
578
- // Reconcile the per-skill-directory watchers against what is on disk.
579
- // Returns true when the set of watched directories changed — i.e. a skill
580
- // was installed or removed — so the caller can reload the catalog.
581
- const refreshSkillDirWatchers = (): boolean => {
582
- const nextDirs = new Set(listSkillDirs());
583
- let changed = false;
584
-
585
- for (const [dirPath, watcher] of skillDirWatchers.entries()) {
586
- if (nextDirs.has(dirPath)) {
587
- continue;
588
- }
589
- watcher.close();
590
- skillDirWatchers.delete(dirPath);
591
- removeWatcher(watcher);
592
- changed = true;
593
- }
594
-
595
- for (const dirPath of nextDirs) {
596
- if (skillDirWatchers.has(dirPath)) {
597
- continue;
598
- }
599
- const watcher = watchDir(dirPath, (filename) => {
600
- if (!filename || isSkillCatalogFile(filename)) {
601
- scheduleSkillsReload(filename ?? "(unknown)");
602
- }
603
- });
604
- if (watcher) {
605
- skillDirWatchers.set(dirPath, watcher);
606
- changed = true;
607
- }
608
- }
609
-
610
- return changed;
611
- };
612
-
613
- const rootWatcher = watchDir(skillsDir, () => {
614
- // A change directly under the skills root means a skill directory was
615
- // added, removed, or renamed. Re-sync the per-skill watchers and reload
616
- // the catalog when the set actually changed.
617
- if (refreshSkillDirWatchers()) {
618
- scheduleSkillsReload("(skill installed or removed)");
619
- }
620
- });
621
-
622
- if (!rootWatcher) {
623
- return;
624
- }
625
-
626
- refreshSkillDirWatchers();
627
- log.info(
628
- { dir: skillsDir },
629
- "Watching skills catalog with per-skill-directory fallback",
630
- );
631
- }
632
451
  }
633
452
 
634
453
  /**
@@ -646,52 +646,6 @@ export async function kickQueueDrain(
646
646
  }
647
647
  }
648
648
 
649
- /**
650
- * Commit `actor` as the actor the turn about to start runs for: the resting
651
- * slot names them, so the history is scoped to them and every reader of the
652
- * slot (the `<turn_context>` actor section, memory retrieval, the Slack
653
- * transcript filters) describes them rather than whoever sent last, and the
654
- * per-turn snapshot then reads back that same actor, which is also returned
655
- * for callers that must hold it in a local. A caller with no actor of its own
656
- * (internal dispatch, or a queued message whose enqueue found the slot empty)
657
- * passes `undefined`, and the resting actor stands.
658
- *
659
- * `reloadHistory` is off only for a steered drain, which keeps its resident
660
- * history: a steer comes from the actor whose turn it cut off, and that
661
- * history may hold the in-memory repair of the abandoned `tool_use`, which a
662
- * reload would discard.
663
- *
664
- * The committed actor is captured and snapshotted before the reload awaits:
665
- * the slot is writable out-of-band across that await (a wake's stamp, a
666
- * pointer elevation), and a read after it would hand the turn that writer's
667
- * actor. A reload that fails starts no turn, so the slot is put back to what
668
- * it held before, guarded so a writer that legitimately moved it in between
669
- * is left alone.
670
- */
671
- async function commitTurnActor(
672
- conversation: Conversation,
673
- actor: TrustContext | undefined,
674
- options: { reloadHistory: boolean },
675
- ): Promise<TrustContext | undefined> {
676
- const prior = restingTrust(conversation);
677
- if (actor) {
678
- conversation.setTrustContext(actor);
679
- }
680
- const turnTrustContext = restingTrust(conversation);
681
- conversation.currentTurnTrustContext = turnTrustContext;
682
- if (options.reloadHistory) {
683
- try {
684
- await conversation.ensureActorScopedHistory();
685
- } catch (err) {
686
- if (actor && restingTrust(conversation) === actor) {
687
- conversation.setTrustContext(prior ?? null);
688
- }
689
- throw err;
690
- }
691
- }
692
- return turnTrustContext;
693
- }
694
-
695
649
  async function drainSingleMessage(
696
650
  conversation: Conversation,
697
651
  next: QueuedMessage,
@@ -794,12 +748,11 @@ async function drainSingleMessage(
794
748
  // a different actor's context if a concurrent request mutates the live fields.
795
749
  // Trust comes from the queued message, not the live slot: the slot holds
796
750
  // whichever actor sent most recently, which is this sender only when nobody
797
- // else sent while this message waited.
798
- const turnTrustContext = await commitTurnActor(
799
- conversation,
800
- next.trustContext,
801
- { reloadHistory: !steered },
802
- );
751
+ // else sent while this message waited. Held in a local as well, because the
752
+ // field is writable out-of-band across the awaits between here and the loop
753
+ // call below.
754
+ const turnTrustContext = next.trustContext ?? restingTrust(conversation);
755
+ conversation.currentTurnTrustContext = turnTrustContext;
803
756
  conversation.currentTurnChannelCapabilities =
804
757
  conversation.channelCapabilities;
805
758
 
@@ -1407,12 +1360,9 @@ async function drainBatch(
1407
1360
  // The head's trust governs the batch, which is sound only because
1408
1361
  // `buildPassthroughBatch` refuses to coalesce messages from different
1409
1362
  // actors; without that boundary this would run a tail under the head's
1410
- // trust.
1411
- const turnTrustContext = await commitTurnActor(
1412
- conversation,
1413
- head.trustContext,
1414
- { reloadHistory: true },
1415
- );
1363
+ // trust. Held in a local for the same reason as the single-message drain.
1364
+ const turnTrustContext = head.trustContext ?? restingTrust(conversation);
1365
+ conversation.currentTurnTrustContext = turnTrustContext;
1416
1366
  conversation.currentTurnChannelCapabilities =
1417
1367
  conversation.channelCapabilities;
1418
1368
 
@@ -1890,16 +1840,37 @@ export async function processMessage(
1890
1840
  metadata: callerMetadata,
1891
1841
  trustContext: committingTrustContext,
1892
1842
  } = options;
1843
+ const priorRestingTrust = restingTrust(conversation);
1844
+ if (committingTrustContext) {
1845
+ conversation.setTrustContext(committingTrustContext);
1846
+ }
1893
1847
  // Held in a local as well as on the conversation: the field is writable
1894
1848
  // out-of-band while this turn is in flight (`agent-wake` stamps it and
1895
1849
  // restores the prior value in a `finally`), so reading it back at the agent
1896
1850
  // loop call below would reintroduce the late read this capture exists to
1897
- // avoid. The local is what the loop runs under.
1898
- const turnTrustContext = await commitTurnActor(
1899
- conversation,
1900
- committingTrustContext,
1901
- { reloadHistory: true },
1902
- );
1851
+ // avoid. The local is what the loop runs under. Captured before the history
1852
+ // reload for the same reason: that await is one of the windows a writer can
1853
+ // land in.
1854
+ const turnTrustContext = restingTrust(conversation);
1855
+ conversation.currentTurnTrustContext = turnTrustContext;
1856
+ try {
1857
+ await conversation.ensureActorScopedHistory();
1858
+ } catch (err) {
1859
+ // This is the commitment point for the turn, so the stamp above is
1860
+ // correct, but a reload that fails starts no turn: the conversation must
1861
+ // not be left attributed to a sender that never ran. Guarded on identity
1862
+ // so a writer that legitimately moved the slot across the await keeps it.
1863
+ // Only the resting slot needs putting back; `runAgentLoopImpl` re-seeds
1864
+ // the per-turn field at the head of every turn, so no later dispatch can
1865
+ // inherit it.
1866
+ if (
1867
+ committingTrustContext &&
1868
+ restingTrust(conversation) === committingTrustContext
1869
+ ) {
1870
+ conversation.setTrustContext(priorRestingTrust ?? null);
1871
+ }
1872
+ throw err;
1873
+ }
1903
1874
  conversation.currentTurnAuthContext = conversation.authContext;
1904
1875
  conversation.currentTurnSourceActorPrincipalId =
1905
1876
  sourceActorPrincipalId ?? conversation.authContext?.actorPrincipalId;
@@ -75,11 +75,13 @@ export interface SkillToolProjection {
75
75
  }
76
76
 
77
77
  /**
78
- * Conversation-scoped cache for skill projection. Avoids re-scanning the entire
79
- * conversation history and re-reading the filesystem on every agent turn.
78
+ * Conversation-scoped cache for skill projection. Avoids re-scanning the
79
+ * conversation history on every agent turn. Catalog membership is not stored
80
+ * here: each projection reads a fresh `loadSkillCatalog()` so newly
81
+ * installed or removed skills are visible on the next turn.
80
82
  *
81
- * Each conversation should own its own cache instance to prevent cross-conversation
82
- * state bleed.
83
+ * Each conversation should own its own cache instance to prevent
84
+ * cross-conversation state bleed.
83
85
  */
84
86
  export interface SkillProjectionCache {
85
87
  /** Cached deriveActiveSkills result. */
@@ -95,9 +97,6 @@ export interface SkillProjectionCache {
95
97
  /** The accumulated active skill entries. */
96
98
  entries: ActiveSkillEntry[];
97
99
  };
98
- /** Cached skill catalog. Invalidated when the conversation is marked stale
99
- * (e.g. skill directories changed on disk while a run is in progress). */
100
- catalog?: SkillSummary[];
101
100
  }
102
101
 
103
102
  export interface ProjectSkillToolsOptions {
@@ -113,9 +112,9 @@ export interface ProjectSkillToolsOptions {
113
112
  */
114
113
  previouslyActiveSkillIds?: Map<string, string>;
115
114
  /**
116
- * Conversation-scoped projection cache. When provided, projectSkillTools will
117
- * avoid redundant deriveActiveSkills scans and loadSkillCatalog filesystem
118
- * reads across agent turns.
115
+ * Conversation-scoped projection cache. When provided, projectSkillTools
116
+ * avoids redundant deriveActiveSkills scans across agent turns. Catalog
117
+ * membership is always a fresh `loadSkillCatalog()` read.
119
118
  */
120
119
  cache?: SkillProjectionCache;
121
120
  /**
@@ -310,23 +309,6 @@ function getCachedActiveSkills(
310
309
  return entries;
311
310
  }
312
311
 
313
- /**
314
- * Return the skill catalog, caching it across agent turns.
315
- *
316
- * The cache is invalidated when the conversation is marked stale (e.g. skill
317
- * directories changed on disk while the conversation is still processing).
318
- */
319
- function getCachedCatalog(cache?: SkillProjectionCache): SkillSummary[] {
320
- if (!cache) {
321
- return loadSkillCatalog();
322
- }
323
-
324
- if (!cache.catalog) {
325
- cache.catalog = loadSkillCatalog();
326
- }
327
- return cache.catalog;
328
- }
329
-
330
312
  // ---------------------------------------------------------------------------
331
313
  // Main export
332
314
  // ---------------------------------------------------------------------------
@@ -363,11 +345,11 @@ export function projectSkillTools(
363
345
  const contextIds = contextEntries.map((e) => e.id);
364
346
  const allCandidateIds = new Set<string>([...contextIds, ...preactivated]);
365
347
 
366
- // Load the catalog (cached for conversation lifetime), then scope it to the
367
- // conversation's per-chat plugin selection so plugin-contributed skills from
368
- // unselected plugins are not resolvable for this run (null = no restriction).
348
+ // Load the catalog, then scope it to the conversation's per-chat plugin
349
+ // selection so plugin-contributed skills from unselected plugins are not
350
+ // resolvable for this run (null = no restriction).
369
351
  const catalog = filterSkillsByEnabledPlugins(
370
- getCachedCatalog(options?.cache),
352
+ loadSkillCatalog(),
371
353
  options?.effectiveEnabledPluginSet ?? null,
372
354
  );
373
355
  const catalogById = new Map<string, SkillSummary>();
@@ -548,7 +548,7 @@ export function clearAllActiveConversations(): number {
548
548
  }
549
549
 
550
550
  /**
551
- * Evict in-memory conversations after a config/prompt/skills reload so the next
551
+ * Evict in-memory conversations after a config/prompt reload so the next
552
552
  * turn rebuilds them against the new config. Conversations with in-flight work
553
553
  * (a live turn, a queued successor, or an active subagent) are marked stale
554
554
  * and rebuilt by `getOrCreateConversation` once that work finishes. Idle
@@ -55,6 +55,7 @@ import {
55
55
  isActivationMomentParam,
56
56
  } from "../telemetry/activation-funnel.js";
57
57
  import { resolveAppId } from "../tools/apps/resolve-app-id.js";
58
+ import { formatDesktopAppRequired } from "../tools/capability-offer.js";
58
59
  import { POINT_AT_PROXY_TOOL } from "../tools/computer-use/skill-proxy-bridge.js";
59
60
  import type { ToolExecutionResult } from "../tools/types.js";
60
61
  import { getLogger } from "../util/logger.js";
@@ -2936,7 +2937,7 @@ export function buildAppOpenPreview(
2936
2937
  function describeComputerUseUnavailable(ctx: Conversation): string {
2937
2938
  const capable = assistantEventHub.listClientsByCapability("host_cu");
2938
2939
  if (capable.length === 0) {
2939
- return "Computer use is not available — no desktop client connected. Open the Vellum desktop app on the machine you want to control, then retry.";
2940
+ return formatDesktopAppRequired("screen");
2940
2941
  }
2941
2942
  return `Computer use is not available for this conversation — ${capable.length} desktop client(s) advertise host_cu, but none of them can be driven from this conversation's interface (${ctx.transportInterface ?? "unknown"}) as its current user.`;
2942
2943
  }
@@ -3251,8 +3252,7 @@ export async function surfaceProxyResolver(
3251
3252
 
3252
3253
  if (!ctx.hostAppControlProxy || !ctx.hostAppControlProxy.isAvailable()) {
3253
3254
  return {
3254
- content:
3255
- "App control is not available — enable the `app-control` feature flag and connect a macOS client.",
3255
+ content: formatDesktopAppRequired("apps"),
3256
3256
  isError: true,
3257
3257
  };
3258
3258
  }
@@ -3344,6 +3344,7 @@ export async function surfaceProxyResolver(
3344
3344
  const caps = ctx.channelCapabilities;
3345
3345
  if (
3346
3346
  caps &&
3347
+ !ctx.hasNoClient &&
3347
3348
  !caps.supportsDynamicUi &&
3348
3349
  !isSlackTaskProgressUiException(ctx, toolName, input)
3349
3350
  ) {
@@ -3552,11 +3553,12 @@ export async function surfaceProxyResolver(
3552
3553
  : surfaceType === "table"
3553
3554
  ? hasActions
3554
3555
  : INTERACTIVE_SURFACE_TYPES.includes(surfaceType);
3555
- // An explicit `await_action: true` is honored for every other type; an
3556
- // actionless surface has nothing to await, so it is forced false.
3557
- const awaitAction = isActionless
3558
- ? false
3559
- : ((input.await_action as boolean) ?? isInteractive);
3556
+ // Background turns persist surfaces for a later conversation open and
3557
+ // return immediately. An actionless surface also has nothing to await.
3558
+ const awaitAction =
3559
+ !ctx.hasNoClient &&
3560
+ !isActionless &&
3561
+ ((input.await_action as boolean) ?? isInteractive);
3560
3562
 
3561
3563
  // Only one non-persistent interactive surface at a time. If another
3562
3564
  // surface is already awaiting user input, reject this one so the LLM
@@ -28,7 +28,6 @@ import { getAllDefaultPluginNames } from "../plugins/defaults/main.js";
28
28
  import { isActivationSession } from "../plugins/defaults/memory/activation-session-store.js";
29
29
  import { isPluginDisabled } from "../plugins/disabled-state.js";
30
30
  import type { Message, ToolDefinition } from "../providers/types.js";
31
- import { assistantEventHub } from "../runtime/assistant-event-hub.js";
32
31
  import { registerConversationSender } from "../tools/browser/browser-screencast.js";
33
32
  import { supportsClientOsForSkillTool } from "../tools/client-os.js";
34
33
  import type { ToolExecutor } from "../tools/executor.js";
@@ -260,6 +259,24 @@ export function createToolExecutor(
260
259
  // see {@link SubagentToolGateMode}): rejects non-allowlisted calls BEFORE
261
260
  // any executor dispatch, so a non-allowlisted tool's executor never runs.
262
261
  // The error tool_result lets the model continue or finish.
262
+ const rejectUnattendedHostTool = (
263
+ toolName: string,
264
+ ): ToolExecutionResult | null => {
265
+ const { transportInterface } = resolveTurnClientOs(ctx);
266
+ const isUnattended = ctx.currentTurnIsNonInteractive ?? ctx.hasNoClient;
267
+ if (
268
+ HOST_TOOL_NAMES.has(toolName) &&
269
+ isUnattended &&
270
+ transportInterface !== "chrome-extension"
271
+ ) {
272
+ return {
273
+ content: `The "${toolName}" tool requires an interactive user turn and cannot run in the background.`,
274
+ isError: true,
275
+ };
276
+ }
277
+ return null;
278
+ };
279
+
263
280
  const rejectNonAllowlistedTool = (
264
281
  toolName: string,
265
282
  ): ToolExecutionResult | null => {
@@ -387,6 +404,10 @@ export function createToolExecutor(
387
404
  if (rejection) {
388
405
  return rejection;
389
406
  }
407
+ const unattendedRejection = rejectUnattendedHostTool(executionName);
408
+ if (unattendedRejection) {
409
+ return unattendedRejection;
410
+ }
390
411
  }
391
412
 
392
413
  if (isDoordashCommand(executionName, executionInput)) {
@@ -550,6 +571,10 @@ export function createToolExecutor(
550
571
  if (innerRejection) {
551
572
  return innerRejection;
552
573
  }
574
+ const unattendedRejection = rejectUnattendedHostTool(toolName);
575
+ if (unattendedRejection) {
576
+ return unattendedRejection;
577
+ }
553
578
 
554
579
  // Per-chat plugin scope: reject the resolved inner tool when it belongs
555
580
  // to a plugin outside the conversation's effective set.
@@ -628,7 +653,6 @@ export const DEFAULT_PREACTIVATED_SKILL_IDS = ["notifications", "subagent"];
628
653
  // ── Conditional tool sets ────────────────────────────────────────────
629
654
 
630
655
  const UI_SURFACE_TOOL_NAMES = new Set(["ui_show", "ui_update", "ui_dismiss"]);
631
- const SLACK_TASK_PROGRESS_UI_TOOL_NAMES = new Set(["ui_show", "ui_update"]);
632
656
  /**
633
657
  * Single source of truth for which tools are host tools and the capability
634
658
  * each one requires from the connected client interface. Adding a tool here
@@ -856,56 +880,26 @@ export function isToolActiveForContext(
856
880
  return supportsChannelReaction(turnChannel);
857
881
  }
858
882
  if (UI_SURFACE_TOOL_NAMES.has(name)) {
859
- if (
860
- channelCapabilities?.channel === "slack" &&
861
- SLACK_TASK_PROGRESS_UI_TOOL_NAMES.has(name)
862
- ) {
863
- return !hasNoClient;
864
- }
865
- return channelCapabilities?.supportsDynamicUi ?? !hasNoClient;
883
+ // Surface calls write conversation content. Background turns persist that
884
+ // content for the next client that opens the conversation, so presence
885
+ // cannot change whether the model receives these definitions.
886
+ return true;
866
887
  }
867
888
  if (HOST_TOOL_NAMES.has(name)) {
868
889
  const capability = HOST_TOOL_TO_CAPABILITY.get(name);
869
890
  const transport = transportInterface;
870
891
 
871
- // Per-capability check is authoritative for structural support: if the
872
- // transport cannot service this capability, the tool is filtered out.
892
+ // A transport that does not implement a capability can invoke it through
893
+ // an eligible same-user client. Client selection happens when the call
894
+ // runs, so the wire schema stays stable across live and background turns.
873
895
  if (transport && capability && !supportsHostProxy(transport, capability)) {
874
- // Cross-client exception: allow host tools whose capabilities have
875
- // cross-client routing infrastructure (Phases 1–3 plus host_browser
876
- // via PR #27489) to be exposed for non-host-proxy transports (e.g.
877
- // "web", "ios") when at least one capable client is connected via
878
- // the event hub. Members of CROSS_CLIENT_EXPOSED_CAPABILITIES
879
- // (host_bash, host_file, host_browser) qualify.
880
- // chrome-extension transport is excluded as a security boundary
881
- // (extension only gets host_browser via its own executor path);
882
- // hasNoClient turns are excluded (no interactive approval UI
883
- // available).
884
- if (
885
- capability &&
896
+ return (
886
897
  CROSS_CLIENT_EXPOSED_CAPABILITIES.has(capability) &&
887
- transport !== "chrome-extension" &&
888
- !hasNoClient &&
889
- assistantEventHub.listClientsByCapability(capability).length > 0
890
- ) {
891
- return true;
892
- }
893
- return false;
894
- }
895
-
896
- // chrome-extension is its own executor — the extension's popup gates
897
- // commands via its own UI, and the transport does not use an SSE-level
898
- // interactive approval channel. hasNoClient is intentionally `true` for
899
- // chrome-extension turns (chrome-extension is not in INTERACTIVE_INTERFACES)
900
- // and must not gate host_browser. Trust the per-capability check.
901
- if (transport === "chrome-extension") {
902
- return true;
898
+ transport !== "chrome-extension"
899
+ );
903
900
  }
904
901
 
905
- // For transports that surface approvals over SSE (macos, backwards-compat
906
- // fallback), deny when no client is present so the guardian auto-approve
907
- // path cannot execute host commands unattended.
908
- return !hasNoClient;
902
+ return true;
909
903
  }
910
904
  if (CLIENT_CAPABILITY_TOOL_NAMES.has(name)) {
911
905
  if (name === "ask_question" && channelCapabilities?.clientOS === "macos") {
@@ -2366,9 +2366,6 @@ export class Conversation {
2366
2366
 
2367
2367
  markStale(): void {
2368
2368
  this.stale = true;
2369
- // Invalidate the cached skill catalog so the next projection picks up
2370
- // filesystem changes (e.g. a skill created during this run).
2371
- this.skillProjectionCache.catalog = undefined;
2372
2369
  }
2373
2370
 
2374
2371
  isStale(): boolean {