@vellumai/assistant 0.12.1-staging.1 → 0.12.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 (404) hide show
  1. package/AGENTS.md +0 -4
  2. package/ARCHITECTURE.md +12 -11
  3. package/docs/architecture/turn-actor.md +8 -15
  4. package/openapi.yaml +1 -291
  5. package/package.json +1 -1
  6. package/src/__tests__/always-loaded-tools-guard.test.ts +10 -18
  7. package/src/__tests__/app-compiler.test.ts +0 -6
  8. package/src/__tests__/app-control-tool-schemas.test.ts +2 -5
  9. package/src/__tests__/app-dir-path-guard.test.ts +3 -1
  10. package/src/__tests__/app-source-watcher.test.ts +218 -0
  11. package/src/{calls/__tests__ → __tests__}/call-controller.test.ts +82 -503
  12. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +2 -5
  13. package/src/__tests__/computer-use-tools.test.ts +5 -17
  14. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  15. package/src/__tests__/config-schema.test.ts +29 -28
  16. package/src/__tests__/config-watcher-skill-reseed.test.ts +359 -0
  17. package/src/__tests__/config-watcher.test.ts +8 -23
  18. package/src/__tests__/connection-model-compat.test.ts +2 -2
  19. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  20. package/src/__tests__/conversation-pairing.test.ts +0 -26
  21. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +10 -4
  22. package/src/__tests__/conversation-queue.test.ts +207 -48
  23. package/src/__tests__/conversation-retry-discard.test.ts +1 -54
  24. package/src/__tests__/conversation-skill-tools.test.ts +0 -27
  25. package/src/__tests__/conversation-surfaces-app-control.test.ts +3 -8
  26. package/src/__tests__/conversation-surfaces-task-progress.test.ts +0 -67
  27. package/src/__tests__/conversation-title-service.test.ts +10 -14
  28. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +5 -3
  29. package/src/__tests__/conversation-tool-setup-attribution.test.ts +0 -51
  30. package/src/__tests__/credential-record-retire.test.ts +1 -171
  31. package/src/__tests__/credential-routes.test.ts +3 -212
  32. package/src/__tests__/credential-security-invariants.test.ts +1 -2
  33. package/src/__tests__/cu-unified-flow.test.ts +2 -8
  34. package/src/__tests__/drain-kick-guard.test.ts +2 -0
  35. package/src/__tests__/drain-requeue-on-contention.test.ts +6 -0
  36. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  37. package/src/__tests__/host-bash-proxy.test.ts +45 -26
  38. package/src/__tests__/host-browser-proxy.test.ts +17 -6
  39. package/src/__tests__/host-file-proxy-targeted.test.ts +0 -23
  40. package/src/__tests__/host-file-proxy.test.ts +23 -11
  41. package/src/__tests__/host-transfer-proxy-targeted.test.ts +9 -30
  42. package/src/__tests__/llm-catalog-parity.test.ts +0 -25
  43. package/src/__tests__/mcp-auth-routes.test.ts +9 -6
  44. package/src/__tests__/mcp-config-secret-boundary.test.ts +136 -78
  45. package/src/__tests__/mcp-health-check.test.ts +11 -8
  46. package/src/__tests__/mcp-list-plugin-servers.test.ts +10 -8
  47. package/src/{calls/__tests__ → __tests__}/media-stream-audio-transcode.test.ts +1 -1
  48. package/src/{calls/__tests__ → __tests__}/media-stream-output.test.ts +8 -37
  49. package/src/{calls/__tests__ → __tests__}/media-stream-parser.test.ts +1 -1
  50. package/src/{calls/__tests__ → __tests__}/media-stream-server-integration.test.ts +57 -150
  51. package/src/{calls/__tests__ → __tests__}/media-stream-stt-session.test.ts +5 -5
  52. package/src/{calls/__tests__ → __tests__}/media-turn-detector.test.ts +1 -1
  53. package/src/__tests__/messages-after-tiebreaker.test.ts +0 -91
  54. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  55. package/src/__tests__/messaging-send-tool.test.ts +548 -355
  56. package/src/__tests__/model-intents.test.ts +0 -3
  57. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  58. package/src/__tests__/platform-client-verify-credential.test.ts +0 -2
  59. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -13
  60. package/src/__tests__/script-proxy-certs.test.ts +0 -35
  61. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  62. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  63. package/src/__tests__/speaker-identification.test.ts +56 -0
  64. package/src/__tests__/subagent-tool-gate-mode.test.ts +3 -19
  65. package/src/__tests__/subagent-tools.test.ts +2 -3
  66. package/src/__tests__/telephony-tts-guard.test.ts +3 -38
  67. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +0 -4
  68. package/src/{calls/__tests__/voice-session-bridge-turn-flow.test.ts → __tests__/voice-session-bridge.test.ts} +12 -20
  69. package/src/__tests__/watch-retro-tool-availability.test.ts +17 -8
  70. package/src/__tests__/workspace-git-service.test.ts +0 -26
  71. package/src/acp/__tests__/agent-process.test.ts +4 -258
  72. package/src/acp/__tests__/auth-required.test.ts +0 -96
  73. package/src/acp/__tests__/client-handler.test.ts +2 -90
  74. package/src/acp/__tests__/session-manager-persistence.test.ts +1 -5
  75. package/src/acp/__tests__/session-manager-resume.test.ts +6 -270
  76. package/src/acp/__tests__/session-manager-usage.test.ts +2 -4
  77. package/src/acp/__tests__/session-manager.test.ts +4 -895
  78. package/src/acp/agent-process.test.ts +1 -4
  79. package/src/acp/agent-process.ts +9 -94
  80. package/src/acp/auth-required.ts +1 -50
  81. package/src/acp/auto-install.test.ts +7 -9
  82. package/src/acp/client-handler.ts +3 -16
  83. package/src/acp/resolve-agent.test.ts +6 -220
  84. package/src/acp/resolve-agent.ts +23 -88
  85. package/src/acp/session-manager.test.ts +3 -196
  86. package/src/acp/session-manager.ts +18 -441
  87. package/src/acp/types.ts +5 -50
  88. package/src/api/events/app-files-changed.ts +3 -4
  89. package/src/api/index.ts +0 -6
  90. package/src/apps/app-store.ts +1 -1
  91. package/src/bundler/app-compiler.ts +0 -15
  92. package/src/calls/__tests__/voice-control-protocol.test.ts +25 -58
  93. package/src/calls/__tests__/voice-session-bridge.test.ts +1 -37
  94. package/src/calls/__tests__/voice-triage-escalate.test.ts +0 -105
  95. package/src/calls/call-constants.ts +4 -24
  96. package/src/calls/call-controller.ts +199 -499
  97. package/src/calls/call-transport.ts +0 -16
  98. package/src/calls/media-stream-output.ts +0 -32
  99. package/src/calls/media-stream-server.ts +30 -82
  100. package/src/calls/media-stream-stt-session.ts +0 -15
  101. package/src/calls/resolve-call-tts-provider.ts +2 -33
  102. package/src/calls/speaker-identification.ts +233 -0
  103. package/src/calls/tts-call-strategy.ts +5 -9
  104. package/src/calls/types.ts +0 -2
  105. package/src/calls/voice-control-protocol.ts +21 -53
  106. package/src/calls/voice-session-bridge.ts +46 -59
  107. package/src/calls/voice-triage-escalate.ts +56 -175
  108. package/src/cli/commands/apps.help.ts +13 -67
  109. package/src/cli/commands/apps.ts +0 -183
  110. package/src/cli/commands/channels/index.help.ts +2 -70
  111. package/src/cli/commands/channels/index.ts +3 -16
  112. package/src/cli/commands/credentials.help.ts +2 -6
  113. package/src/cli/commands/credentials.ts +2 -2
  114. package/src/cli/commands/mcp.help.ts +8 -8
  115. package/src/cli/commands/mcp.ts +1 -1
  116. package/src/cli/commands/roadmap.help.ts +1 -5
  117. package/src/cli/commands/roadmap.ts +1 -1
  118. package/src/cli/lib/bundled-marketplace.json +90 -103
  119. package/src/config/__tests__/default-profile-catalog.test.ts +0 -22
  120. package/src/config/__tests__/memory-retrospective-schema.test.ts +1 -15
  121. package/src/config/__tests__/platform-rehydration.test.ts +0 -93
  122. package/src/config/acp-defaults.test.ts +3 -13
  123. package/src/config/acp-defaults.ts +6 -23
  124. package/src/config/acp-schema.ts +2 -30
  125. package/src/config/bundled-skills/acp/SKILL.md +14 -22
  126. package/src/config/bundled-skills/acp/TOOLS.json +1 -5
  127. package/src/config/bundled-skills/app-builder/SKILL.md +9 -12
  128. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  129. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  130. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +139 -67
  131. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  132. package/src/config/bundled-skills/screen-annotation/SKILL.md +1 -8
  133. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +1 -4
  134. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  135. package/src/config/default-profile-catalog.ts +1 -1
  136. package/src/config/env.ts +2 -2
  137. package/src/config/feature-flag-registry.json +12 -8
  138. package/src/config/memory-v3-gate.test.ts +0 -31
  139. package/src/config/memory-v3-gate.ts +16 -20
  140. package/src/config/platform-rehydration.ts +21 -75
  141. package/src/config/schema.ts +2 -2
  142. package/src/config/schemas/__tests__/live-voice.test.ts +158 -8
  143. package/src/config/schemas/calls.ts +28 -1
  144. package/src/config/schemas/live-voice.ts +173 -1
  145. package/src/config/schemas/mcp.ts +1 -1
  146. package/src/config/schemas/memory-retrospective.ts +0 -10
  147. package/src/config/schemas/skills.ts +14 -0
  148. package/src/credential-execution/process-manager.test.ts +19 -18
  149. package/src/daemon/__tests__/conversation-tool-setup.test.ts +121 -66
  150. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +1 -4
  151. package/src/daemon/app-source-watcher.ts +218 -0
  152. package/src/daemon/config-watcher.ts +194 -13
  153. package/src/daemon/conversation-process.ts +64 -35
  154. package/src/daemon/conversation-skill-tools.ts +31 -13
  155. package/src/daemon/conversation-store.ts +1 -1
  156. package/src/daemon/conversation-surfaces.ts +8 -10
  157. package/src/daemon/conversation-tool-setup.ts +43 -37
  158. package/src/daemon/conversation.ts +3 -0
  159. package/src/daemon/host-bash-proxy.ts +12 -8
  160. package/src/daemon/host-browser-proxy.ts +1 -15
  161. package/src/daemon/host-file-proxy.ts +10 -8
  162. package/src/daemon/host-transfer-proxy.ts +18 -14
  163. package/src/daemon/lifecycle.ts +5 -0
  164. package/src/daemon/mcp-reload-service.ts +5 -5
  165. package/src/daemon/message-types/sync.ts +0 -1
  166. package/src/daemon/providers-setup.ts +1 -1
  167. package/src/daemon/shutdown-handlers.ts +9 -1
  168. package/src/daemon/skill-memory-refresh.ts +0 -64
  169. package/src/daemon/tool-side-effects.ts +18 -7
  170. package/src/hooks/types.ts +0 -33
  171. package/src/inbound/platform-callback-registration.ts +5 -4
  172. package/src/live-voice/__tests__/live-voice-metrics.test.ts +3 -0
  173. package/src/live-voice/__tests__/live-voice-progress.test.ts +15 -15
  174. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  175. package/src/{calls → live-voice}/__tests__/progress-narration.test.ts +4 -4
  176. package/src/{calls → live-voice}/__tests__/progress-phrases.test.ts +1 -1
  177. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +69 -13
  178. package/src/live-voice/live-voice-session.ts +693 -213
  179. package/src/{calls → live-voice}/progress-narration.ts +2 -2
  180. package/src/live-voice/protocol.ts +2 -0
  181. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  182. package/src/mcp/effective-config.ts +3 -3
  183. package/src/mcp/mcp-header-store.ts +45 -29
  184. package/src/mcp/startup.ts +4 -8
  185. package/src/messaging/provider.ts +3 -10
  186. package/src/messaging/providers/callback-routing.ts +0 -18
  187. package/src/messaging/providers/channel-transport.ts +0 -56
  188. package/src/messaging/providers/discord/transport.ts +0 -17
  189. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +17 -6
  190. package/src/messaging/providers/slack/adapter.ts +44 -7
  191. package/src/messaging/providers/slack/transport.ts +0 -16
  192. package/src/messaging/providers/telegram-bot/adapter.ts +48 -1
  193. package/src/messaging/providers/telegram-bot/client.ts +26 -0
  194. package/src/messaging/providers/telegram-bot/transport.ts +0 -20
  195. package/src/messaging/providers/whatsapp/adapter.ts +48 -1
  196. package/src/messaging/providers/whatsapp/client.ts +29 -0
  197. package/src/messaging/providers/whatsapp/transport.ts +0 -18
  198. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +0 -8
  199. package/src/notifications/__tests__/assistant-reply-producer.test.ts +0 -55
  200. package/src/notifications/__tests__/home-feed-side-effect.test.ts +3 -153
  201. package/src/notifications/assistant-reply-producer.ts +3 -8
  202. package/src/notifications/conversation-pairing.ts +16 -36
  203. package/src/notifications/delivered-post-record.ts +1 -11
  204. package/src/notifications/home-feed-side-effect.ts +7 -55
  205. package/src/outbound-proxy/certs.ts +5 -3
  206. package/src/permissions/checker.test.ts +6 -11
  207. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  208. package/src/persistence/conversation-crud.ts +63 -71
  209. package/src/persistence/conversation-lineage.ts +4 -12
  210. package/src/persistence/conversation-title-service.ts +26 -9
  211. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  212. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  213. package/src/persistence/message-reads.ts +0 -30
  214. package/src/persistence/migrations/157-invite-contact-id.ts +6 -18
  215. package/src/persistence/schema/memory-core.ts +0 -5
  216. package/src/platform/client.test.ts +0 -2
  217. package/src/platform/client.ts +10 -2
  218. package/src/plugin-api/constants.ts +0 -2
  219. package/src/plugin-api/index.ts +0 -1
  220. package/src/plugin-api/types.ts +0 -2
  221. package/src/plugins/defaults/index.ts +0 -2
  222. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +0 -46
  223. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +5 -9
  224. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +10 -20
  225. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +0 -219
  226. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +0 -31
  227. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +0 -63
  228. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +0 -4
  229. package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -4
  230. package/src/plugins/defaults/memory/hooks/init.ts +0 -10
  231. package/src/plugins/defaults/memory/hooks/shutdown.ts +0 -2
  232. package/src/plugins/defaults/memory/jobs-worker.ts +0 -13
  233. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +60 -34
  234. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +5 -3
  235. package/src/plugins/defaults/memory/memory-retrospective-job.ts +3 -12
  236. package/src/plugins/defaults/memory/memory-retrospective-state.ts +71 -312
  237. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +1 -2
  238. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +3 -5
  239. package/src/plugins/defaults/memory/startup.ts +1 -6
  240. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +9 -7
  241. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  242. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +0 -89
  243. package/src/plugins/defaults/memory/worker.ts +0 -6
  244. package/src/plugins/mcp-servers.ts +75 -15
  245. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  246. package/src/providers/gemini/client.ts +23 -9
  247. package/src/providers/model-catalog.ts +0 -34
  248. package/src/providers/model-intents.ts +5 -10
  249. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +18 -146
  250. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +1 -83
  251. package/src/providers/openai/chat-completions-provider.ts +78 -192
  252. package/src/providers/openai/codex-models.ts +4 -0
  253. package/src/runtime/AGENTS.md +0 -2
  254. package/src/runtime/auth/same-actor.ts +3 -38
  255. package/src/runtime/http-server.ts +151 -160
  256. package/src/runtime/routes/__tests__/acp-routes.test.ts +4 -159
  257. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +1 -108
  258. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +12 -16
  259. package/src/runtime/routes/acp-routes.test.ts +0 -15
  260. package/src/runtime/routes/acp-routes.ts +9 -72
  261. package/src/runtime/routes/app-management-routes.ts +0 -73
  262. package/src/runtime/routes/app-routes.ts +1 -1
  263. package/src/runtime/routes/conversation-list-routes.ts +3 -14
  264. package/src/runtime/routes/conversation-query-routes.ts +0 -40
  265. package/src/runtime/routes/credential-routes.ts +10 -19
  266. package/src/runtime/routes/diagnostics-routes.ts +3 -55
  267. package/src/runtime/routes/index.ts +0 -4
  268. package/src/runtime/routes/log-export-routes.ts +0 -2
  269. package/src/runtime/routes/mcp-auth-routes.ts +47 -39
  270. package/src/runtime/routes/migration-routes.ts +21 -8
  271. package/src/runtime/routes/platform-routes.ts +15 -18
  272. package/src/runtime/services/__tests__/conversation-serializer.test.ts +0 -28
  273. package/src/runtime/services/conversation-serializer.ts +3 -16
  274. package/src/runtime/sync/resource-sync-events.ts +1 -2
  275. package/src/schedule/worker.ts +1 -1
  276. package/src/security/ces-rpc-record-backend.ts +0 -7
  277. package/src/subagent/consult-context.ts +25 -12
  278. package/src/tools/acp/list-agents.test.ts +1 -3
  279. package/src/tools/acp/spawn.test.ts +11 -146
  280. package/src/tools/acp/spawn.ts +3 -21
  281. package/src/tools/acp/status.ts +3 -40
  282. package/src/tools/app-control/skill-proxy-bridge.ts +1 -2
  283. package/src/tools/capability-offer.test.ts +2 -22
  284. package/src/tools/capability-offer.ts +1 -16
  285. package/src/tools/computer-use/definitions.ts +2 -4
  286. package/src/tools/computer-use/skill-proxy-bridge.ts +1 -2
  287. package/src/tools/credentials/metadata-store.ts +0 -100
  288. package/src/tools/host-filesystem/edit.test.ts +1 -2
  289. package/src/tools/host-filesystem/edit.ts +2 -2
  290. package/src/tools/host-filesystem/read.test.ts +1 -2
  291. package/src/tools/host-filesystem/read.ts +2 -2
  292. package/src/tools/host-filesystem/transfer.test.ts +1 -2
  293. package/src/tools/host-filesystem/transfer.ts +2 -2
  294. package/src/tools/host-filesystem/write.test.ts +1 -2
  295. package/src/tools/host-filesystem/write.ts +2 -2
  296. package/src/tools/host-terminal/host-shell.test.ts +0 -16
  297. package/src/tools/host-terminal/host-shell.ts +20 -4
  298. package/src/tools/policy-context.ts +7 -6
  299. package/src/tools/subagent/spawn.ts +8 -5
  300. package/src/tools/watch/watch-retro-report.ts +8 -0
  301. package/src/util/__tests__/language-subtag.test.ts +1 -23
  302. package/src/util/fs-watcher-error.ts +36 -0
  303. package/src/util/language-subtag.ts +0 -16
  304. package/src/util/platform.ts +0 -8
  305. package/src/workspace/git-service.ts +54 -5
  306. package/src/workspace/migrations/registry.ts +0 -6
  307. package/scripts/smoke-desktop-window-theme.ts +0 -186
  308. package/src/__tests__/git-porcelain.test.ts +0 -47
  309. package/src/__tests__/helpers/set-workspace-mcp.ts +0 -11
  310. package/src/__tests__/message-deleted-hook-dispatch.test.ts +0 -119
  311. package/src/__tests__/resolve-app.test.ts +0 -135
  312. package/src/__tests__/source-fingerprint.test.ts +0 -184
  313. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +0 -336
  314. package/src/acp/__tests__/helpers/acp-model-option.ts +0 -51
  315. package/src/acp/model-config.test.ts +0 -166
  316. package/src/acp/model-config.ts +0 -138
  317. package/src/api/events/acp-session-model-update.test.ts +0 -88
  318. package/src/api/events/acp-session-model-update.ts +0 -46
  319. package/src/apps/resolve-app.ts +0 -81
  320. package/src/apps/source-fingerprint.ts +0 -266
  321. package/src/calls/__tests__/barge-in-guard.test.ts +0 -81
  322. package/src/calls/__tests__/voice-leg-coordinator.test.ts +0 -229
  323. package/src/calls/__tests__/voice-progress-cadence.test.ts +0 -274
  324. package/src/calls/barge-in-guard.ts +0 -118
  325. package/src/calls/voice-leg-coordinator.ts +0 -189
  326. package/src/calls/voice-progress-cadence.ts +0 -423
  327. package/src/cli/commands/__tests__/apps.test.ts +0 -245
  328. package/src/cli/commands/__tests__/roadmap.help.test.ts +0 -19
  329. package/src/cli/commands/channels/__tests__/send.test.ts +0 -218
  330. package/src/cli/commands/channels/send.ts +0 -128
  331. package/src/config/__tests__/platform-identity.test.ts +0 -117
  332. package/src/config/acp-agent-write.ts +0 -43
  333. package/src/config/acp-schema.test.ts +0 -81
  334. package/src/config/platform-identity.ts +0 -144
  335. package/src/config/schemas/__tests__/voice.test.ts +0 -148
  336. package/src/config/schemas/voice.ts +0 -150
  337. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +0 -133
  338. package/src/daemon/app-change-notify.ts +0 -45
  339. package/src/desktop/__tests__/fake-desktop.ts +0 -119
  340. package/src/desktop/desktop-chrome-policy.test.ts +0 -71
  341. package/src/desktop/desktop-chrome-policy.ts +0 -42
  342. package/src/desktop/desktop-dependencies.test.ts +0 -102
  343. package/src/desktop/desktop-dependencies.ts +0 -306
  344. package/src/desktop/desktop-feature.test.ts +0 -18
  345. package/src/desktop/desktop-feature.ts +0 -19
  346. package/src/desktop/desktop-panel-config.test.ts +0 -135
  347. package/src/desktop/desktop-panel-config.ts +0 -149
  348. package/src/desktop/desktop-session-manager.test.ts +0 -627
  349. package/src/desktop/desktop-session-manager.ts +0 -814
  350. package/src/desktop/desktop-stream-bridge.test.ts +0 -395
  351. package/src/desktop/desktop-stream-bridge.ts +0 -214
  352. package/src/desktop/desktop-wallpaper.test.ts +0 -191
  353. package/src/desktop/desktop-wallpaper.ts +0 -113
  354. package/src/desktop/desktop-window-theme.ts +0 -170
  355. package/src/i18n/AGENTS.md +0 -22
  356. package/src/i18n/__tests__/i18n.test.ts +0 -108
  357. package/src/i18n/index.ts +0 -16
  358. package/src/i18n/locales.ts +0 -93
  359. package/src/i18n/messages.ts +0 -61
  360. package/src/i18n/title.ts +0 -25
  361. package/src/mcp/__tests__/workspace-mcp-config.test.ts +0 -135
  362. package/src/mcp/spec-schema.ts +0 -111
  363. package/src/mcp/workspace-mcp-config.ts +0 -188
  364. package/src/messaging/providers/__tests__/proactive-address.test.ts +0 -95
  365. package/src/persistence/conversation-title-placeholders.ts +0 -23
  366. package/src/persistence/message-cursor.ts +0 -78
  367. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +0 -149
  368. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +0 -118
  369. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +0 -83
  370. package/src/plugins/defaults/memory/hooks/message-deleted.ts +0 -21
  371. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +0 -126
  372. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +0 -79
  373. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +0 -28
  374. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +0 -9
  375. package/src/providers/gemini-thought-signature.test.ts +0 -63
  376. package/src/providers/gemini-thought-signature.ts +0 -44
  377. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +0 -613
  378. package/src/providers/openai/__tests__/google-thought-signature.test.ts +0 -235
  379. package/src/providers/openai/google-thought-signature.ts +0 -320
  380. package/src/runtime/__tests__/channel-send.test.ts +0 -443
  381. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +0 -89
  382. package/src/runtime/__tests__/live-voice-upgrade.test.ts +0 -58
  383. package/src/runtime/__tests__/runtime-ws-test-utils.ts +0 -72
  384. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +0 -59
  385. package/src/runtime/channel-send.ts +0 -316
  386. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +0 -205
  387. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +0 -127
  388. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +0 -107
  389. package/src/runtime/routes/channel-send-routes.ts +0 -85
  390. package/src/runtime/routes/desktop-setup-routes.test.ts +0 -74
  391. package/src/runtime/routes/desktop-setup-routes.ts +0 -35
  392. package/src/security/__tests__/ces-http-record-backend.test.ts +0 -137
  393. package/src/security/ces-http-record-backend.ts +0 -120
  394. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +0 -60
  395. package/src/skills/workspace-skill-md-signature.ts +0 -50
  396. package/src/tools/acp/status.test.ts +0 -94
  397. package/src/util/ca-bundle.ts +0 -13
  398. package/src/workspace/git-porcelain.ts +0 -66
  399. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +0 -258
  400. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +0 -78
  401. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +0 -214
  402. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +0 -102
  403. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +0 -199
  404. /package/src/{calls → live-voice}/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, 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.
3
+ * Watches workspace files (config, prompts) and skills directories
4
+ * for changes.
6
5
  */
7
6
  import {
8
7
  existsSync,
9
8
  type FSWatcher,
10
9
  mkdirSync,
10
+ readdirSync,
11
11
  readFileSync,
12
12
  unwatchFile,
13
13
  watch,
14
14
  watchFile,
15
15
  } from "node:fs";
16
- import { join } from "node:path";
16
+ import { basename, join } from "node:path";
17
17
 
18
18
  import { AVATAR_IMAGE_FILENAME } from "@vellumai/avatar-manifest";
19
19
 
@@ -44,14 +44,50 @@ import {
44
44
  getSoundsDir,
45
45
  getWorkspaceDir,
46
46
  getWorkspacePromptPath,
47
- WORKSPACE_MCP_FILENAME,
47
+ getWorkspaceSkillsDir,
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";
52
53
 
53
54
  const log = getLogger("config-watcher");
54
55
 
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
+
55
91
  /**
56
92
  * Attach a resilient error handler to an FSWatcher so that async errors
57
93
  * (e.g. ENXIO when a Unix socket file like `gateway.sock` appears in a
@@ -191,11 +227,19 @@ export class ConfigWatcher {
191
227
  return;
192
228
  }
193
229
  try {
230
+ const prevMcpFingerprint = JSON.stringify(this.lastConfig?.mcp ?? {});
194
231
  const changed = await this.refreshConfigFromSources();
195
232
  if (changed) {
196
233
  evictConversationsForReload();
197
234
  refreshAuthenticatedApiRateLimit();
198
235
  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
+ }
199
243
  }
200
244
  } catch (err) {
201
245
  log.error(
@@ -204,14 +248,6 @@ export class ConfigWatcher {
204
248
  );
205
249
  }
206
250
  },
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
- },
215
251
  "SOUL.md": () => {
216
252
  evictConversationsForReload();
217
253
  },
@@ -234,6 +270,7 @@ export class ConfigWatcher {
234
270
  this.startAvatarWatcher();
235
271
  this.startSignalsWatcher();
236
272
  this.startUsersWatcher();
273
+ this.startSkillsWatchers();
237
274
  }
238
275
 
239
276
  stop(): void {
@@ -448,6 +485,150 @@ export class ConfigWatcher {
448
485
  );
449
486
  }
450
487
  }
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
+ }
451
632
  }
452
633
 
453
634
  /**
@@ -646,6 +646,52 @@ 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
+
649
695
  async function drainSingleMessage(
650
696
  conversation: Conversation,
651
697
  next: QueuedMessage,
@@ -748,11 +794,12 @@ async function drainSingleMessage(
748
794
  // a different actor's context if a concurrent request mutates the live fields.
749
795
  // Trust comes from the queued message, not the live slot: the slot holds
750
796
  // whichever actor sent most recently, which is this sender only when nobody
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;
797
+ // else sent while this message waited.
798
+ const turnTrustContext = await commitTurnActor(
799
+ conversation,
800
+ next.trustContext,
801
+ { reloadHistory: !steered },
802
+ );
756
803
  conversation.currentTurnChannelCapabilities =
757
804
  conversation.channelCapabilities;
758
805
 
@@ -1360,9 +1407,12 @@ async function drainBatch(
1360
1407
  // The head's trust governs the batch, which is sound only because
1361
1408
  // `buildPassthroughBatch` refuses to coalesce messages from different
1362
1409
  // actors; without that boundary this would run a tail under the head's
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;
1410
+ // trust.
1411
+ const turnTrustContext = await commitTurnActor(
1412
+ conversation,
1413
+ head.trustContext,
1414
+ { reloadHistory: true },
1415
+ );
1366
1416
  conversation.currentTurnChannelCapabilities =
1367
1417
  conversation.channelCapabilities;
1368
1418
 
@@ -1840,37 +1890,16 @@ export async function processMessage(
1840
1890
  metadata: callerMetadata,
1841
1891
  trustContext: committingTrustContext,
1842
1892
  } = options;
1843
- const priorRestingTrust = restingTrust(conversation);
1844
- if (committingTrustContext) {
1845
- conversation.setTrustContext(committingTrustContext);
1846
- }
1847
1893
  // Held in a local as well as on the conversation: the field is writable
1848
1894
  // out-of-band while this turn is in flight (`agent-wake` stamps it and
1849
1895
  // restores the prior value in a `finally`), so reading it back at the agent
1850
1896
  // loop call below would reintroduce the late read this capture exists to
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
- }
1897
+ // avoid. The local is what the loop runs under.
1898
+ const turnTrustContext = await commitTurnActor(
1899
+ conversation,
1900
+ committingTrustContext,
1901
+ { reloadHistory: true },
1902
+ );
1874
1903
  conversation.currentTurnAuthContext = conversation.authContext;
1875
1904
  conversation.currentTurnSourceActorPrincipalId =
1876
1905
  sourceActorPrincipalId ?? conversation.authContext?.actorPrincipalId;
@@ -75,13 +75,11 @@ export interface SkillToolProjection {
75
75
  }
76
76
 
77
77
  /**
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.
78
+ * Conversation-scoped cache for skill projection. Avoids re-scanning the entire
79
+ * conversation history and re-reading the filesystem on every agent turn.
82
80
  *
83
- * Each conversation should own its own cache instance to prevent
84
- * cross-conversation state bleed.
81
+ * Each conversation should own its own cache instance to prevent cross-conversation
82
+ * state bleed.
85
83
  */
86
84
  export interface SkillProjectionCache {
87
85
  /** Cached deriveActiveSkills result. */
@@ -97,6 +95,9 @@ export interface SkillProjectionCache {
97
95
  /** The accumulated active skill entries. */
98
96
  entries: ActiveSkillEntry[];
99
97
  };
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[];
100
101
  }
101
102
 
102
103
  export interface ProjectSkillToolsOptions {
@@ -112,9 +113,9 @@ export interface ProjectSkillToolsOptions {
112
113
  */
113
114
  previouslyActiveSkillIds?: Map<string, string>;
114
115
  /**
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.
116
+ * Conversation-scoped projection cache. When provided, projectSkillTools will
117
+ * avoid redundant deriveActiveSkills scans and loadSkillCatalog filesystem
118
+ * reads across agent turns.
118
119
  */
119
120
  cache?: SkillProjectionCache;
120
121
  /**
@@ -309,6 +310,23 @@ function getCachedActiveSkills(
309
310
  return entries;
310
311
  }
311
312
 
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
+
312
330
  // ---------------------------------------------------------------------------
313
331
  // Main export
314
332
  // ---------------------------------------------------------------------------
@@ -345,11 +363,11 @@ export function projectSkillTools(
345
363
  const contextIds = contextEntries.map((e) => e.id);
346
364
  const allCandidateIds = new Set<string>([...contextIds, ...preactivated]);
347
365
 
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).
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).
351
369
  const catalog = filterSkillsByEnabledPlugins(
352
- loadSkillCatalog(),
370
+ getCachedCatalog(options?.cache),
353
371
  options?.effectiveEnabledPluginSet ?? null,
354
372
  );
355
373
  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 reload so the next
551
+ * Evict in-memory conversations after a config/prompt/skills 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,7 +55,6 @@ 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";
59
58
  import { POINT_AT_PROXY_TOOL } from "../tools/computer-use/skill-proxy-bridge.js";
60
59
  import type { ToolExecutionResult } from "../tools/types.js";
61
60
  import { getLogger } from "../util/logger.js";
@@ -2937,7 +2936,7 @@ export function buildAppOpenPreview(
2937
2936
  function describeComputerUseUnavailable(ctx: Conversation): string {
2938
2937
  const capable = assistantEventHub.listClientsByCapability("host_cu");
2939
2938
  if (capable.length === 0) {
2940
- return formatDesktopAppRequired("screen");
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.";
2941
2940
  }
2942
2941
  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.`;
2943
2942
  }
@@ -3252,7 +3251,8 @@ export async function surfaceProxyResolver(
3252
3251
 
3253
3252
  if (!ctx.hostAppControlProxy || !ctx.hostAppControlProxy.isAvailable()) {
3254
3253
  return {
3255
- content: formatDesktopAppRequired("apps"),
3254
+ content:
3255
+ "App control is not available — enable the `app-control` feature flag and connect a macOS client.",
3256
3256
  isError: true,
3257
3257
  };
3258
3258
  }
@@ -3344,7 +3344,6 @@ export async function surfaceProxyResolver(
3344
3344
  const caps = ctx.channelCapabilities;
3345
3345
  if (
3346
3346
  caps &&
3347
- !ctx.hasNoClient &&
3348
3347
  !caps.supportsDynamicUi &&
3349
3348
  !isSlackTaskProgressUiException(ctx, toolName, input)
3350
3349
  ) {
@@ -3553,12 +3552,11 @@ export async function surfaceProxyResolver(
3553
3552
  : surfaceType === "table"
3554
3553
  ? hasActions
3555
3554
  : INTERACTIVE_SURFACE_TYPES.includes(surfaceType);
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);
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);
3562
3560
 
3563
3561
  // Only one non-persistent interactive surface at a time. If another
3564
3562
  // surface is already awaiting user input, reject this one so the LLM
@@ -28,6 +28,7 @@ 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";
31
32
  import { registerConversationSender } from "../tools/browser/browser-screencast.js";
32
33
  import { supportsClientOsForSkillTool } from "../tools/client-os.js";
33
34
  import type { ToolExecutor } from "../tools/executor.js";
@@ -259,24 +260,6 @@ export function createToolExecutor(
259
260
  // see {@link SubagentToolGateMode}): rejects non-allowlisted calls BEFORE
260
261
  // any executor dispatch, so a non-allowlisted tool's executor never runs.
261
262
  // 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
-
280
263
  const rejectNonAllowlistedTool = (
281
264
  toolName: string,
282
265
  ): ToolExecutionResult | null => {
@@ -404,10 +387,6 @@ export function createToolExecutor(
404
387
  if (rejection) {
405
388
  return rejection;
406
389
  }
407
- const unattendedRejection = rejectUnattendedHostTool(executionName);
408
- if (unattendedRejection) {
409
- return unattendedRejection;
410
- }
411
390
  }
412
391
 
413
392
  if (isDoordashCommand(executionName, executionInput)) {
@@ -571,10 +550,6 @@ export function createToolExecutor(
571
550
  if (innerRejection) {
572
551
  return innerRejection;
573
552
  }
574
- const unattendedRejection = rejectUnattendedHostTool(toolName);
575
- if (unattendedRejection) {
576
- return unattendedRejection;
577
- }
578
553
 
579
554
  // Per-chat plugin scope: reject the resolved inner tool when it belongs
580
555
  // to a plugin outside the conversation's effective set.
@@ -653,6 +628,7 @@ export const DEFAULT_PREACTIVATED_SKILL_IDS = ["notifications", "subagent"];
653
628
  // ── Conditional tool sets ────────────────────────────────────────────
654
629
 
655
630
  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"]);
656
632
  /**
657
633
  * Single source of truth for which tools are host tools and the capability
658
634
  * each one requires from the connected client interface. Adding a tool here
@@ -880,26 +856,56 @@ export function isToolActiveForContext(
880
856
  return supportsChannelReaction(turnChannel);
881
857
  }
882
858
  if (UI_SURFACE_TOOL_NAMES.has(name)) {
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;
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;
887
866
  }
888
867
  if (HOST_TOOL_NAMES.has(name)) {
889
868
  const capability = HOST_TOOL_TO_CAPABILITY.get(name);
890
869
  const transport = transportInterface;
891
870
 
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.
871
+ // Per-capability check is authoritative for structural support: if the
872
+ // transport cannot service this capability, the tool is filtered out.
895
873
  if (transport && capability && !supportsHostProxy(transport, capability)) {
896
- return (
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 &&
897
886
  CROSS_CLIENT_EXPOSED_CAPABILITIES.has(capability) &&
898
- transport !== "chrome-extension"
899
- );
887
+ transport !== "chrome-extension" &&
888
+ !hasNoClient &&
889
+ assistantEventHub.listClientsByCapability(capability).length > 0
890
+ ) {
891
+ return true;
892
+ }
893
+ return false;
900
894
  }
901
895
 
902
- return true;
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;
903
+ }
904
+
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;
903
909
  }
904
910
  if (CLIENT_CAPABILITY_TOOL_NAMES.has(name)) {
905
911
  if (name === "ask_question" && channelCapabilities?.clientOS === "macos") {
@@ -2366,6 +2366,9 @@ 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;
2369
2372
  }
2370
2373
 
2371
2374
  isStale(): boolean {