@vellumai/assistant 0.12.1 → 0.12.2-staging.2

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 (577) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +20 -21
  3. package/docs/architecture/turn-actor.md +15 -8
  4. package/docs/guardian-request-flow.md +16 -0
  5. package/node_modules/@vellumai/slack-text/src/index.ts +13 -8
  6. package/node_modules/@vellumai/slack-text/src/label-resolution-entities.test.ts +95 -0
  7. package/openapi.yaml +360 -51
  8. package/package.json +3 -1
  9. package/scripts/smoke-desktop-window-theme.ts +186 -0
  10. package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
  11. package/src/__tests__/anthropic-provider.test.ts +0 -63
  12. package/src/__tests__/app-compiler.test.ts +6 -0
  13. package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
  14. package/src/__tests__/app-dir-path-guard.test.ts +1 -3
  15. package/src/__tests__/assistant-event-hub-machine-name.test.ts +6 -7
  16. package/src/__tests__/assistant-event-hub.test.ts +44 -0
  17. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  18. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
  19. package/src/__tests__/computer-use-tools.test.ts +19 -7
  20. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  21. package/src/__tests__/config-schema.test.ts +28 -29
  22. package/src/__tests__/config-watcher.test.ts +23 -8
  23. package/src/__tests__/connection-model-compat.test.ts +2 -2
  24. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  25. package/src/__tests__/conversation-pairing.test.ts +26 -0
  26. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
  27. package/src/__tests__/conversation-queue.test.ts +48 -207
  28. package/src/__tests__/conversation-retry-discard.test.ts +54 -1
  29. package/src/__tests__/conversation-runtime-assembly.test.ts +28 -0
  30. package/src/__tests__/conversation-skill-tools.test.ts +27 -0
  31. package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
  32. package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +1 -0
  33. package/src/__tests__/conversation-surfaces-point-at-capability.test.ts +1 -0
  34. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  35. package/src/__tests__/conversation-title-service.test.ts +14 -10
  36. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
  37. package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
  38. package/src/__tests__/credential-record-retire.test.ts +171 -1
  39. package/src/__tests__/credential-routes.test.ts +250 -3
  40. package/src/__tests__/credential-security-invariants.test.ts +5 -3
  41. package/src/__tests__/cu-unified-flow.test.ts +14 -4
  42. package/src/__tests__/document-sync-tags.test.ts +25 -0
  43. package/src/__tests__/drain-kick-guard.test.ts +0 -2
  44. package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
  45. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  46. package/src/__tests__/git-porcelain.test.ts +47 -0
  47. package/src/__tests__/heartbeat-service.test.ts +56 -40
  48. package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
  49. package/src/__tests__/host-bash-proxy.test.ts +26 -45
  50. package/src/__tests__/host-browser-proxy.test.ts +6 -17
  51. package/src/__tests__/host-cu-proxy.test.ts +596 -0
  52. package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
  53. package/src/__tests__/host-file-proxy.test.ts +11 -23
  54. package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
  55. package/src/__tests__/list-messages-camera-frame.test.ts +121 -0
  56. package/src/__tests__/llm-catalog-parity.test.ts +25 -0
  57. package/src/__tests__/mcp-auth-routes.test.ts +6 -9
  58. package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
  59. package/src/__tests__/mcp-health-check.test.ts +8 -11
  60. package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
  61. package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
  62. package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
  63. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  64. package/src/__tests__/messaging-send-tool.test.ts +355 -548
  65. package/src/__tests__/model-intents.test.ts +3 -0
  66. package/src/__tests__/notification-vellum-adapter.test.ts +85 -2
  67. package/src/__tests__/oauth-commands-routes.test.ts +168 -0
  68. package/src/__tests__/oauth-provider-profiles.test.ts +18 -0
  69. package/src/__tests__/oauth-provider-serializer.test.ts +22 -0
  70. package/src/__tests__/oauth-providers-routes.test.ts +1 -0
  71. package/src/__tests__/openai-provider.test.ts +1 -0
  72. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  73. package/src/__tests__/platform-callback-registration.test.ts +8 -16
  74. package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
  75. package/src/__tests__/plugin-import-boundary-guard.test.ts +14 -0
  76. package/src/__tests__/qdrant-manager.test.ts +132 -0
  77. package/src/__tests__/resolve-app.test.ts +135 -0
  78. package/src/__tests__/scaffold-managed-skill-tool.test.ts +229 -6
  79. package/src/__tests__/script-proxy-certs.test.ts +35 -0
  80. package/src/__tests__/secret-routes-acp-guard.test.ts +59 -1
  81. package/src/__tests__/service-account-token.test.ts +247 -0
  82. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  83. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  84. package/src/__tests__/skills.test.ts +9 -4
  85. package/src/__tests__/source-fingerprint.test.ts +184 -0
  86. package/src/__tests__/subagent-tool-gate-mode.test.ts +70 -3
  87. package/src/__tests__/subagent-tools.test.ts +3 -2
  88. package/src/__tests__/telephony-tts-guard.test.ts +38 -3
  89. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  90. package/src/__tests__/ui-channel-variants.test.ts +1 -56
  91. package/src/__tests__/unicode.test.ts +13 -0
  92. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
  93. package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
  94. package/src/__tests__/workspace-git-service.test.ts +26 -0
  95. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
  96. package/src/acp/__tests__/acp-claude-oauth.test.ts +257 -6
  97. package/src/acp/__tests__/acp-credentials.test.ts +13 -0
  98. package/src/acp/__tests__/agent-process.test.ts +258 -4
  99. package/src/acp/__tests__/auth-required.test.ts +96 -0
  100. package/src/acp/__tests__/claude-token-refresh.test.ts +257 -0
  101. package/src/acp/__tests__/client-handler.test.ts +90 -2
  102. package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
  103. package/src/acp/__tests__/prepare-agent-env.test.ts +60 -1
  104. package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
  105. package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
  106. package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
  107. package/src/acp/__tests__/session-manager.test.ts +895 -4
  108. package/src/acp/acp-claude-oauth.ts +328 -14
  109. package/src/acp/acp-credentials.ts +19 -0
  110. package/src/acp/agent-process.test.ts +4 -1
  111. package/src/acp/agent-process.ts +96 -10
  112. package/src/acp/auth-required.ts +50 -1
  113. package/src/acp/auto-install.test.ts +9 -7
  114. package/src/acp/claude-token-refresh.ts +150 -0
  115. package/src/acp/client-handler.ts +16 -3
  116. package/src/acp/model-config.test.ts +166 -0
  117. package/src/acp/model-config.ts +138 -0
  118. package/src/acp/prepare-agent-env.ts +21 -6
  119. package/src/acp/resolve-agent.test.ts +220 -6
  120. package/src/acp/resolve-agent.ts +88 -23
  121. package/src/acp/session-manager.test.ts +196 -3
  122. package/src/acp/session-manager.ts +441 -18
  123. package/src/acp/types.ts +50 -5
  124. package/src/agent/loop.ts +1 -0
  125. package/src/api/events/acp-session-model-update.test.ts +88 -0
  126. package/src/api/events/acp-session-model-update.ts +46 -0
  127. package/src/api/events/app-files-changed.ts +4 -3
  128. package/src/api/events/notification-intent.ts +2 -0
  129. package/src/api/events/user-message-echo.ts +7 -1
  130. package/src/api/index.ts +6 -0
  131. package/src/api/responses/conversation-message.ts +5 -0
  132. package/src/apps/app-store.ts +1 -1
  133. package/src/apps/resolve-app.ts +81 -0
  134. package/src/apps/source-fingerprint.ts +266 -0
  135. package/src/avatar/identity-avatar.ts +7 -2
  136. package/src/bundler/app-compiler.ts +15 -0
  137. package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
  138. package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
  139. package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
  140. package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
  141. package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
  142. package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
  143. package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
  144. package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
  145. package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
  146. package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
  147. package/src/calls/__tests__/voice-control-protocol.test.ts +120 -25
  148. package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
  149. package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
  150. package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
  151. package/src/calls/__tests__/voice-session-bridge.test.ts +56 -1
  152. package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
  153. package/src/calls/barge-in-guard.ts +118 -0
  154. package/src/calls/call-constants.ts +24 -4
  155. package/src/calls/call-controller.ts +499 -199
  156. package/src/calls/call-transport.ts +16 -0
  157. package/src/calls/media-stream-output.ts +32 -0
  158. package/src/calls/media-stream-server.ts +82 -30
  159. package/src/calls/media-stream-stt-session.ts +15 -0
  160. package/src/{live-voice → calls}/progress-narration.ts +2 -2
  161. package/src/calls/resolve-call-tts-provider.ts +33 -2
  162. package/src/calls/tts-call-strategy.ts +9 -5
  163. package/src/calls/types.ts +2 -0
  164. package/src/calls/voice-control-protocol.ts +155 -21
  165. package/src/calls/voice-leg-coordinator.ts +189 -0
  166. package/src/calls/voice-progress-cadence.ts +423 -0
  167. package/src/calls/voice-session-bridge.ts +92 -72
  168. package/src/calls/voice-triage-escalate.ts +178 -59
  169. package/src/cli/commands/__tests__/apps.test.ts +245 -0
  170. package/src/cli/commands/__tests__/bash.test.ts +53 -0
  171. package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
  172. package/src/cli/commands/apps.help.ts +67 -13
  173. package/src/cli/commands/apps.ts +183 -0
  174. package/src/cli/commands/bash.help.ts +8 -13
  175. package/src/cli/commands/bash.ts +7 -25
  176. package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
  177. package/src/cli/commands/channels/index.help.ts +70 -2
  178. package/src/cli/commands/channels/index.ts +16 -3
  179. package/src/cli/commands/channels/send.ts +128 -0
  180. package/src/cli/commands/credentials.help.ts +6 -2
  181. package/src/cli/commands/credentials.ts +2 -2
  182. package/src/cli/commands/mcp.help.ts +8 -8
  183. package/src/cli/commands/mcp.ts +1 -1
  184. package/src/cli/commands/oauth/status.ts +5 -0
  185. package/src/cli/commands/roadmap.help.ts +5 -1
  186. package/src/cli/commands/roadmap.ts +1 -1
  187. package/src/cli/lib/bundled-marketplace.json +103 -90
  188. package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
  189. package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
  190. package/src/config/__tests__/platform-identity.test.ts +255 -0
  191. package/src/config/__tests__/platform-rehydration.test.ts +99 -9
  192. package/src/config/acp-agent-write.ts +43 -0
  193. package/src/config/acp-defaults.test.ts +13 -3
  194. package/src/config/acp-defaults.ts +23 -6
  195. package/src/config/acp-schema.test.ts +81 -0
  196. package/src/config/acp-schema.ts +30 -2
  197. package/src/config/bundled-skills/acp/SKILL.md +22 -14
  198. package/src/config/bundled-skills/acp/TOOLS.json +5 -1
  199. package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
  200. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  201. package/src/config/bundled-skills/computer-use/SKILL.md +29 -2
  202. package/src/config/bundled-skills/computer-use/TOOLS.json +99 -0
  203. package/src/config/bundled-skills/computer-use/tools/computer-use-sequence.ts +12 -0
  204. package/src/config/bundled-skills/media-processing/services/preprocess.ts +2 -1
  205. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +2 -1
  206. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -1
  207. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  208. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  209. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
  210. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  211. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -1
  212. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -1
  213. package/src/config/bundled-skills/schedule/SKILL.md +3 -4
  214. package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
  215. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
  216. package/src/config/bundled-skills/skill-management/TOOLS.json +6 -2
  217. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -2
  218. package/src/config/default-profile-catalog.ts +1 -1
  219. package/src/config/env.ts +2 -2
  220. package/src/config/feature-flag-registry.json +33 -13
  221. package/src/config/loader.ts +2 -0
  222. package/src/config/memory-v3-gate.test.ts +31 -0
  223. package/src/config/memory-v3-gate.ts +20 -16
  224. package/src/config/platform-identity.ts +201 -0
  225. package/src/config/platform-rehydration.ts +11 -27
  226. package/src/config/schema.ts +2 -2
  227. package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
  228. package/src/config/schemas/__tests__/voice.test.ts +148 -0
  229. package/src/config/schemas/calls.ts +1 -28
  230. package/src/config/schemas/live-voice.ts +1 -173
  231. package/src/config/schemas/mcp.ts +1 -1
  232. package/src/config/schemas/memory-retrospective.ts +10 -0
  233. package/src/config/schemas/services.ts +18 -0
  234. package/src/config/schemas/skills.ts +0 -14
  235. package/src/config/schemas/voice.ts +150 -0
  236. package/src/credential-execution/process-manager.test.ts +18 -19
  237. package/src/credential-health/credential-health-service.ts +11 -17
  238. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
  239. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
  240. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
  241. package/src/daemon/app-change-notify.ts +45 -0
  242. package/src/daemon/config-watcher.ts +13 -194
  243. package/src/daemon/conversation-error.ts +3 -2
  244. package/src/daemon/conversation-media-retry.ts +2 -1
  245. package/src/daemon/conversation-process.ts +35 -64
  246. package/src/daemon/conversation-runtime-assembly.ts +20 -7
  247. package/src/daemon/conversation-skill-tools.ts +13 -31
  248. package/src/daemon/conversation-store.ts +1 -1
  249. package/src/daemon/conversation-surfaces.ts +21 -10
  250. package/src/daemon/conversation-tool-setup.ts +45 -59
  251. package/src/daemon/conversation.ts +0 -3
  252. package/src/daemon/dictation-profile-store.ts +2 -1
  253. package/src/daemon/handlers/shared.ts +2 -1
  254. package/src/daemon/handlers/skills.ts +6 -3
  255. package/src/daemon/host-bash-proxy.ts +8 -12
  256. package/src/daemon/host-browser-proxy.ts +15 -1
  257. package/src/daemon/host-cu-proxy.ts +224 -19
  258. package/src/daemon/host-file-proxy.ts +8 -10
  259. package/src/daemon/host-proxy-preactivation.ts +2 -1
  260. package/src/daemon/host-transfer-proxy.ts +14 -18
  261. package/src/daemon/lifecycle.ts +0 -5
  262. package/src/daemon/mcp-reload-service.ts +5 -5
  263. package/src/daemon/message-types/sync.ts +1 -0
  264. package/src/daemon/orphan-reaper.ts +1 -1
  265. package/src/daemon/providers-setup.ts +1 -1
  266. package/src/daemon/reaction-history-render.ts +2 -1
  267. package/src/daemon/shutdown-handlers.ts +1 -9
  268. package/src/daemon/skill-memory-refresh.ts +64 -0
  269. package/src/daemon/tool-side-effects.ts +7 -18
  270. package/src/desktop/__tests__/fake-desktop.ts +122 -0
  271. package/src/desktop/desktop-chrome-policy.test.ts +79 -0
  272. package/src/desktop/desktop-chrome-policy.ts +47 -0
  273. package/src/desktop/desktop-chrome-session.ts +37 -0
  274. package/src/desktop/desktop-dependencies.test.ts +102 -0
  275. package/src/desktop/desktop-dependencies.ts +308 -0
  276. package/src/desktop/desktop-feature.test.ts +18 -0
  277. package/src/desktop/desktop-feature.ts +19 -0
  278. package/src/desktop/desktop-panel-config.test.ts +135 -0
  279. package/src/desktop/desktop-panel-config.ts +149 -0
  280. package/src/desktop/desktop-session-manager.test.ts +730 -0
  281. package/src/desktop/desktop-session-manager.ts +850 -0
  282. package/src/desktop/desktop-stream-bridge.test.ts +395 -0
  283. package/src/desktop/desktop-stream-bridge.ts +214 -0
  284. package/src/desktop/desktop-wallpaper.test.ts +191 -0
  285. package/src/desktop/desktop-wallpaper.ts +113 -0
  286. package/src/desktop/desktop-window-drag.ts +177 -0
  287. package/src/desktop/desktop-window-manager-config.test.ts +230 -0
  288. package/src/desktop/desktop-window-manager-config.ts +238 -0
  289. package/src/desktop/desktop-window-theme.ts +170 -0
  290. package/src/heartbeat/heartbeat-service.ts +21 -3
  291. package/src/home/__tests__/feed-writer.test.ts +22 -0
  292. package/src/home/feed-writer.ts +11 -2
  293. package/src/hooks/types.ts +33 -0
  294. package/src/i18n/AGENTS.md +22 -0
  295. package/src/i18n/__tests__/i18n.test.ts +108 -0
  296. package/src/i18n/index.ts +16 -0
  297. package/src/i18n/locales.ts +93 -0
  298. package/src/i18n/messages.ts +61 -0
  299. package/src/i18n/title.ts +25 -0
  300. package/src/inbound/platform-callback-registration.ts +4 -5
  301. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +121 -0
  302. package/src/live-voice/__tests__/live-voice-events.test.ts +8 -7
  303. package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
  304. package/src/live-voice/__tests__/live-voice-progress.test.ts +94 -15
  305. package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +238 -0
  306. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  307. package/src/live-voice/__tests__/protocol.test.ts +39 -0
  308. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
  309. package/src/live-voice/__tests__/session-controls.test.ts +79 -0
  310. package/src/live-voice/live-voice-photo.ts +25 -6
  311. package/src/live-voice/live-voice-session.ts +297 -699
  312. package/src/live-voice/protocol.ts +60 -2
  313. package/src/live-voice/session-controls.ts +111 -0
  314. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  315. package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
  316. package/src/mcp/effective-config.ts +3 -3
  317. package/src/mcp/mcp-header-store.ts +29 -45
  318. package/src/mcp/spec-schema.ts +111 -0
  319. package/src/mcp/startup.ts +8 -4
  320. package/src/mcp/workspace-mcp-config.ts +188 -0
  321. package/src/messaging/provider.ts +10 -3
  322. package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
  323. package/src/messaging/providers/callback-routing.ts +18 -0
  324. package/src/messaging/providers/channel-transport.ts +56 -0
  325. package/src/messaging/providers/discord/transport.ts +17 -0
  326. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
  327. package/src/messaging/providers/slack/adapter.ts +7 -44
  328. package/src/messaging/providers/slack/transport.ts +16 -0
  329. package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
  330. package/src/messaging/providers/telegram-bot/client.ts +0 -26
  331. package/src/messaging/providers/telegram-bot/transport.ts +20 -0
  332. package/src/messaging/providers/whatsapp/adapter.ts +1 -48
  333. package/src/messaging/providers/whatsapp/transport.ts +18 -0
  334. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
  335. package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
  336. package/src/notifications/__tests__/guardian-feed-projection.test.ts +158 -2
  337. package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
  338. package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
  339. package/src/notifications/adapters/macos.ts +3 -0
  340. package/src/notifications/assistant-reply-producer.ts +8 -3
  341. package/src/notifications/conversation-pairing.ts +36 -16
  342. package/src/notifications/delivered-post-record.ts +11 -1
  343. package/src/notifications/guardian-feed-projection.ts +125 -5
  344. package/src/notifications/home-feed-side-effect.ts +55 -7
  345. package/src/notifications/schedule-result-producer.ts +66 -22
  346. package/src/oauth/AGENTS.md +2 -0
  347. package/src/oauth/__tests__/seed-providers-managed.test.ts +149 -0
  348. package/src/oauth/byo-connection.test.ts +105 -0
  349. package/src/oauth/byo-connection.ts +36 -6
  350. package/src/oauth/connection-resolver.test.ts +100 -0
  351. package/src/oauth/connection-resolver.ts +120 -3
  352. package/src/oauth/provider-serializer.ts +9 -0
  353. package/src/oauth/scope-utils.ts +24 -0
  354. package/src/oauth/seed-providers.ts +314 -27
  355. package/src/outbound-proxy/certs.ts +3 -5
  356. package/src/permissions/checker.test.ts +11 -6
  357. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  358. package/src/persistence/conversation-crud.ts +71 -63
  359. package/src/persistence/conversation-lineage.ts +12 -4
  360. package/src/persistence/conversation-queries.ts +2 -2
  361. package/src/persistence/conversation-title-placeholders.ts +23 -0
  362. package/src/persistence/conversation-title-service.ts +9 -26
  363. package/src/persistence/embeddings/qdrant-manager.ts +163 -18
  364. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  365. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  366. package/src/persistence/message-cursor.ts +78 -0
  367. package/src/persistence/message-reads.ts +30 -0
  368. package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
  369. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
  370. package/src/persistence/schema/memory-core.ts +5 -0
  371. package/src/platform/client.test.ts +4 -2
  372. package/src/platform/client.ts +2 -10
  373. package/src/plugin-api/constants.ts +2 -0
  374. package/src/plugin-api/index.ts +1 -0
  375. package/src/plugin-api/types.ts +2 -0
  376. package/src/plugins/defaults/index.ts +2 -0
  377. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
  378. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
  379. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
  380. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
  381. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
  382. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +5 -0
  383. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
  384. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
  385. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
  386. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
  387. package/src/plugins/defaults/memory/context-search/agent-runner.ts +3 -2
  388. package/src/plugins/defaults/memory/context-search/format.ts +3 -2
  389. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +6 -3
  390. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +3 -2
  391. package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -7
  392. package/src/plugins/defaults/memory/hooks/init.ts +10 -0
  393. package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
  394. package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
  395. package/src/plugins/defaults/memory/host-utils.ts +10 -0
  396. package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
  397. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
  398. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
  399. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
  400. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
  401. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
  402. package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
  403. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  404. package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
  405. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
  406. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
  407. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
  408. package/src/plugins/defaults/memory/startup.ts +28 -2
  409. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
  410. package/src/plugins/defaults/memory/substrate/page-index.ts +6 -1
  411. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  412. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +4 -2
  413. package/src/plugins/defaults/memory/v1/graph/extraction.ts +4 -2
  414. package/src/plugins/defaults/memory/v1/graph/retriever.ts +4 -1
  415. package/src/plugins/defaults/memory/v2/reranker.ts +6 -1
  416. package/src/plugins/defaults/memory/v3/__tests__/card.test.ts +24 -0
  417. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +125 -0
  418. package/src/plugins/defaults/memory/v3/__tests__/sections.test.ts +26 -0
  419. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +30 -0
  420. package/src/plugins/defaults/memory/v3/card.ts +3 -2
  421. package/src/plugins/defaults/memory/v3/pool-select.ts +5 -3
  422. package/src/plugins/defaults/memory/v3/sections.ts +9 -3
  423. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +5 -2
  424. package/src/plugins/defaults/memory/worker.ts +6 -0
  425. package/src/plugins/mcp-servers.ts +15 -75
  426. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  427. package/src/providers/__tests__/retry-callsite.test.ts +41 -0
  428. package/src/providers/__tests__/retry-outbound-sanitize.test.ts +176 -0
  429. package/src/providers/anthropic/client.ts +0 -47
  430. package/src/providers/gemini/client.ts +9 -23
  431. package/src/providers/gemini-thought-signature.test.ts +63 -0
  432. package/src/providers/gemini-thought-signature.ts +44 -0
  433. package/src/providers/inference/__tests__/endpoint-probe.test.ts +42 -3
  434. package/src/providers/inference/auth.ts +2 -3
  435. package/src/providers/inference/connection-availability.ts +27 -8
  436. package/src/providers/inference/endpoint-probe.ts +10 -1
  437. package/src/providers/inference/resolve-auth.ts +22 -4
  438. package/src/providers/inference/service-account-token.ts +248 -0
  439. package/src/providers/model-catalog.ts +34 -0
  440. package/src/providers/model-intents.ts +10 -5
  441. package/src/providers/openai/__tests__/chat-completions-output-token-limit.test.ts +141 -0
  442. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
  443. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
  444. package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
  445. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
  446. package/src/providers/openai/chat-completions-provider.ts +212 -79
  447. package/src/providers/openai/codex-models.ts +0 -4
  448. package/src/providers/openai/google-thought-signature.ts +320 -0
  449. package/src/providers/opencode/client.test.ts +73 -4
  450. package/src/providers/opencode/client.ts +60 -3
  451. package/src/providers/openrouter/client.ts +20 -0
  452. package/src/providers/outbound-request-sanitize.ts +96 -0
  453. package/src/providers/retry.ts +30 -19
  454. package/src/providers/unparseable-tool-args.ts +5 -1
  455. package/src/runtime/AGENTS.md +3 -1
  456. package/src/runtime/__tests__/channel-send.test.ts +443 -0
  457. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
  458. package/src/runtime/__tests__/host-observe.test.ts +40 -1
  459. package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
  460. package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
  461. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
  462. package/src/runtime/agent-wake.ts +2 -1
  463. package/src/runtime/assistant-event-hub.ts +21 -3
  464. package/src/runtime/auth/same-actor.ts +38 -3
  465. package/src/runtime/channel-send.ts +316 -0
  466. package/src/runtime/host-observe.ts +11 -1
  467. package/src/runtime/http-server.ts +160 -151
  468. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +16 -5
  469. package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
  470. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +275 -0
  471. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
  472. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
  473. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
  474. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
  475. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
  476. package/src/runtime/routes/acp-claude-auth-routes.ts +11 -3
  477. package/src/runtime/routes/acp-routes.test.ts +15 -0
  478. package/src/runtime/routes/acp-routes.ts +72 -9
  479. package/src/runtime/routes/app-management-routes.ts +90 -2
  480. package/src/runtime/routes/app-routes.ts +1 -1
  481. package/src/runtime/routes/channel-send-routes.ts +85 -0
  482. package/src/runtime/routes/conversation-list-routes.ts +14 -3
  483. package/src/runtime/routes/conversation-query-routes.ts +40 -0
  484. package/src/runtime/routes/conversation-routes.ts +16 -3
  485. package/src/runtime/routes/credential-routes.ts +20 -14
  486. package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
  487. package/src/runtime/routes/desktop-setup-routes.ts +35 -0
  488. package/src/runtime/routes/diagnostics-routes.ts +61 -4
  489. package/src/runtime/routes/events-routes.ts +4 -0
  490. package/src/runtime/routes/host-cu-routes.ts +6 -0
  491. package/src/runtime/routes/index.ts +4 -2
  492. package/src/runtime/routes/log-export-routes.ts +2 -0
  493. package/src/runtime/routes/mcp-auth-routes.ts +39 -47
  494. package/src/runtime/routes/migration-routes.ts +6 -20
  495. package/src/runtime/routes/oauth-commands-routes.ts +50 -0
  496. package/src/runtime/routes/oauth-providers.ts +7 -0
  497. package/src/runtime/routes/platform-routes.ts +18 -15
  498. package/src/runtime/routes/workspace-routes.test.ts +55 -26
  499. package/src/runtime/routes/workspace-routes.ts +46 -63
  500. package/src/runtime/routes/workspace-tree-walk.test.ts +244 -0
  501. package/src/runtime/routes/workspace-tree-walk.ts +240 -0
  502. package/src/runtime/routes/workspace-utils.ts +17 -0
  503. package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
  504. package/src/runtime/services/conversation-serializer.ts +16 -3
  505. package/src/runtime/sync/resource-sync-events.ts +2 -1
  506. package/src/schedule/run-script.ts +2 -1
  507. package/src/schedule/schedule-store.ts +9 -2
  508. package/src/schedule/worker.ts +1 -1
  509. package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
  510. package/src/security/ces-http-record-backend.ts +120 -0
  511. package/src/security/ces-rpc-record-backend.ts +7 -0
  512. package/src/security/untrusted-content.ts +3 -1
  513. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
  514. package/src/skills/workspace-skill-md-signature.ts +50 -0
  515. package/src/subagent/consult-context.ts +12 -25
  516. package/src/tools/__tests__/side-effect-abort-guard.test.ts +1 -0
  517. package/src/tools/acp/list-agents.test.ts +3 -1
  518. package/src/tools/acp/spawn.test.ts +146 -11
  519. package/src/tools/acp/spawn.ts +21 -3
  520. package/src/tools/acp/status.test.ts +94 -0
  521. package/src/tools/acp/status.ts +40 -3
  522. package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
  523. package/src/tools/browser/browser-execution.ts +1 -1
  524. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +9 -3
  525. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +2 -1
  526. package/src/tools/browser/cdp-client/extension-cdp-client.ts +3 -2
  527. package/src/tools/browser/network-recorder.ts +2 -1
  528. package/src/tools/capability-offer.test.ts +22 -2
  529. package/src/tools/capability-offer.ts +16 -1
  530. package/src/tools/computer-use/definitions.ts +121 -2
  531. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
  532. package/src/tools/credentials/metadata-store.ts +100 -0
  533. package/src/tools/filesystem/search.ts +2 -1
  534. package/src/tools/host-filesystem/edit.test.ts +2 -1
  535. package/src/tools/host-filesystem/edit.ts +2 -2
  536. package/src/tools/host-filesystem/read.test.ts +2 -1
  537. package/src/tools/host-filesystem/read.ts +2 -2
  538. package/src/tools/host-filesystem/transfer.test.ts +2 -1
  539. package/src/tools/host-filesystem/transfer.ts +2 -2
  540. package/src/tools/host-filesystem/write.test.ts +2 -1
  541. package/src/tools/host-filesystem/write.ts +2 -2
  542. package/src/tools/host-terminal/host-shell.test.ts +16 -0
  543. package/src/tools/host-terminal/host-shell.ts +4 -20
  544. package/src/tools/network/web-search.ts +6 -1
  545. package/src/tools/policy-context.ts +6 -7
  546. package/src/tools/skills/scaffold-managed.ts +61 -3
  547. package/src/tools/subagent/spawn.ts +5 -8
  548. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
  549. package/src/tools/terminal/sanitized-bash.ts +105 -0
  550. package/src/tools/ui-surface/channel-variants.ts +10 -59
  551. package/src/tools/watch/watch-retro-report.ts +0 -8
  552. package/src/types/host-capabilities.ts +5 -3
  553. package/src/util/__tests__/language-subtag.test.ts +23 -1
  554. package/src/util/ca-bundle.ts +13 -0
  555. package/src/util/language-subtag.ts +16 -0
  556. package/src/util/platform.ts +8 -0
  557. package/src/util/unicode.ts +15 -1
  558. package/src/watch/watch-retro.ts +6 -8
  559. package/src/watch/watch-timeline.ts +2 -2
  560. package/src/workflows/run-manager.ts +2 -1
  561. package/src/workspace/git-porcelain.ts +66 -0
  562. package/src/workspace/git-service.ts +6 -55
  563. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
  564. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
  565. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
  566. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
  567. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
  568. package/src/workspace/migrations/registry.ts +6 -0
  569. package/src/__tests__/app-source-watcher.test.ts +0 -218
  570. package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
  571. package/src/__tests__/speaker-identification.test.ts +0 -56
  572. package/src/calls/speaker-identification.ts +0 -233
  573. package/src/daemon/app-source-watcher.ts +0 -218
  574. package/src/messaging/providers/whatsapp/client.ts +0 -29
  575. package/src/runtime/routes/debug-bash-routes.ts +0 -173
  576. package/src/util/fs-watcher-error.ts +0 -36
  577. /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
@@ -0,0 +1,188 @@
1
+ /**
2
+ * User-owned MCP servers in `$VELLUM_WORKSPACE_DIR/mcp.json`.
3
+ *
4
+ * The file is an Agent Plugins 1.0.0 document (`$schema` + `mcpServers`).
5
+ * This module is the only reader and writer of that file. Presence of a
6
+ * server starts it. Headers are never persisted here.
7
+ */
8
+
9
+ import {
10
+ existsSync,
11
+ mkdirSync,
12
+ readFileSync,
13
+ renameSync,
14
+ writeFileSync,
15
+ } from "node:fs";
16
+ import { dirname } from "node:path";
17
+
18
+ import type { McpConfig, McpServerConfig } from "../config/schemas/mcp.js";
19
+ import { McpConfigSchema } from "../config/schemas/mcp.js";
20
+ import { getLogger } from "../util/logger.js";
21
+ import { getWorkspaceMcpConfigPath } from "../util/platform.js";
22
+ import {
23
+ AGENT_PLUGINS_MCP_SCHEMA_URL,
24
+ projectSpecServerToTransport,
25
+ projectTransportToSpecServer,
26
+ SpecMcpDocumentSchema,
27
+ SpecMcpServerSchema,
28
+ } from "./spec-schema.js";
29
+
30
+ const log = getLogger("workspace-mcp-config");
31
+
32
+ export type WorkspaceMcpFileState = "missing" | "ok" | "invalid";
33
+
34
+ export interface WorkspaceMcpLoadResult {
35
+ readonly config: McpConfig;
36
+ readonly issues: readonly string[];
37
+ readonly fileState: WorkspaceMcpFileState;
38
+ }
39
+
40
+ /**
41
+ * Read `/workspace/mcp.json` and project it onto the in-memory MCP config.
42
+ *
43
+ * A missing file is an empty server map. An invalid file disables the
44
+ * workspace half only (plugin servers still load) and is reported so a
45
+ * write can refuse to overwrite it.
46
+ */
47
+ export function readWorkspaceMcpFile(
48
+ path: string = getWorkspaceMcpConfigPath(),
49
+ ): WorkspaceMcpLoadResult {
50
+ if (!existsSync(path)) {
51
+ return {
52
+ config: McpConfigSchema.parse({}),
53
+ issues: [],
54
+ fileState: "missing",
55
+ };
56
+ }
57
+
58
+ let raw: string;
59
+ try {
60
+ raw = readFileSync(path, "utf8");
61
+ } catch (err) {
62
+ const message = `mcp.json unreadable: ${err instanceof Error ? err.message : String(err)}`;
63
+ return {
64
+ config: McpConfigSchema.parse({}),
65
+ issues: [message],
66
+ fileState: "invalid",
67
+ };
68
+ }
69
+
70
+ let json: unknown;
71
+ try {
72
+ json = JSON.parse(raw);
73
+ } catch (err) {
74
+ const message = `mcp.json invalid JSON: ${err instanceof Error ? err.message : String(err)}`;
75
+ return {
76
+ config: McpConfigSchema.parse({}),
77
+ issues: [message],
78
+ fileState: "invalid",
79
+ };
80
+ }
81
+
82
+ const document = SpecMcpDocumentSchema.safeParse(json);
83
+ if (!document.success) {
84
+ return {
85
+ config: McpConfigSchema.parse({}),
86
+ issues: ['mcp.json is missing a valid "mcpServers" object'],
87
+ fileState: "invalid",
88
+ };
89
+ }
90
+
91
+ const servers: Record<string, McpServerConfig> = {};
92
+ const issues: string[] = [];
93
+
94
+ for (const [serverKey, rawEntry] of Object.entries(document.data.mcpServers)) {
95
+ if (serverKey.trim().length === 0) {
96
+ issues.push("server key must be a non-empty string; skipping");
97
+ continue;
98
+ }
99
+
100
+ const entry = SpecMcpServerSchema.safeParse(rawEntry);
101
+ if (!entry.success) {
102
+ issues.push(
103
+ `invalid server entry "${serverKey}": ${entry.error.issues
104
+ .map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`)
105
+ .join("; ")}`,
106
+ );
107
+ continue;
108
+ }
109
+
110
+ if (entry.data.type === "stdio" && entry.data.cwd) {
111
+ issues.push(
112
+ `"${serverKey}": "cwd" is not supported by this host and was ignored; the server runs in the assistant's working directory`,
113
+ );
114
+ }
115
+
116
+ servers[serverKey] = {
117
+ transport: projectSpecServerToTransport(entry.data),
118
+ };
119
+ }
120
+
121
+ return {
122
+ config: { servers },
123
+ issues,
124
+ fileState: "ok",
125
+ };
126
+ }
127
+
128
+ /** Load the workspace MCP config, logging any declaration problems. */
129
+ export function loadWorkspaceMcpConfig(): McpConfig {
130
+ const result = readWorkspaceMcpFile();
131
+ for (const issue of result.issues) {
132
+ log.warn(issue);
133
+ }
134
+ return result.config;
135
+ }
136
+
137
+ /**
138
+ * Refuse a write when `mcp.json` exists but is not a usable document, so a
139
+ * later add/remove cannot clobber a hand-edit the user still means to fix.
140
+ */
141
+ export function assertWorkspaceMcpWritable(): void {
142
+ const result = readWorkspaceMcpFile();
143
+ if (result.fileState === "invalid") {
144
+ throw new Error(result.issues[0] ?? "mcp.json is not a valid document");
145
+ }
146
+ }
147
+
148
+ /** Persist the in-memory workspace MCP config as a spec-pure document. */
149
+ export function saveWorkspaceMcpConfig(config: McpConfig): void {
150
+ assertWorkspaceMcpWritable();
151
+ const path = getWorkspaceMcpConfigPath();
152
+ const mcpServers: Record<string, unknown> = {};
153
+ for (const [id, server] of Object.entries(config.servers)) {
154
+ mcpServers[id] = projectTransportToSpecServer(server.transport);
155
+ }
156
+
157
+ const document = {
158
+ $schema: AGENT_PLUGINS_MCP_SCHEMA_URL,
159
+ mcpServers,
160
+ };
161
+
162
+ mkdirSync(dirname(path), { recursive: true });
163
+ const tmpPath = `${path}.tmp`;
164
+ writeFileSync(tmpPath, JSON.stringify(document, null, 2) + "\n");
165
+ renameSync(tmpPath, path);
166
+ }
167
+
168
+ /**
169
+ * Replace `config.mcp` on a settings/export payload with the projected
170
+ * workspace servers (transport only, headers stripped). Old clients that
171
+ * still read `mcp.servers` from `/v1/config` keep seeing the same shape.
172
+ */
173
+ export function overlayWorkspaceMcpForConfigRead(config: unknown): void {
174
+ if (config === null || typeof config !== "object" || Array.isArray(config)) {
175
+ return;
176
+ }
177
+ const root = config as Record<string, unknown>;
178
+ const { servers } = loadWorkspaceMcpConfig();
179
+ const projected: Record<string, unknown> = {};
180
+ for (const [id, server] of Object.entries(servers)) {
181
+ const { headers: _headers, ...safeTransport } = server.transport as Record<
182
+ string,
183
+ unknown
184
+ >;
185
+ projected[id] = { transport: safeTransport };
186
+ }
187
+ root.mcp = { servers: projected };
188
+ }
@@ -56,15 +56,22 @@ export interface MessagingProvider {
56
56
  query: string,
57
57
  options?: SearchOptions,
58
58
  ): Promise<SearchResult>;
59
- sendMessage(
59
+
60
+ // ── Optional operations (platforms implement what they support) ───
61
+
62
+ /**
63
+ * Send through the provider's own API. A provider whose channel has a
64
+ * direct transport omits this: its sends go through the transport, which
65
+ * is the one send implementation per addressable channel, and the
66
+ * messaging tool routes there before it looks for this method.
67
+ */
68
+ sendMessage?(
60
69
  connection: OAuthConnection | undefined,
61
70
  conversationId: string,
62
71
  text: string,
63
72
  options?: SendOptions,
64
73
  ): Promise<SendResult>;
65
74
 
66
- // ── Optional operations (platforms implement what they support) ───
67
-
68
75
  getThreadReplies?(
69
76
  connection: OAuthConnection | undefined,
70
77
  conversationId: string,
@@ -0,0 +1,95 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ // The addressing capability resolves a named chat into exactly what each
3
+ // transport's own operations read: the callback URL `channelForCallback`
4
+ // resolves and the params `deliver` consumes. Resolution is local, so these
5
+ // assertions make no platform call; the tests import the transports'
6
+ // modules, which pull in their provider-API senders, so the senders'
7
+ // credential lookups are stubbed to keep the import inert.
8
+ import { mock } from "bun:test";
9
+
10
+ const actualSecureKeys = await import("../../../security/secure-keys.js");
11
+ mock.module("../../../security/secure-keys.js", () => ({
12
+ ...actualSecureKeys,
13
+ getSecureKeyAsync: async () => null,
14
+ getSecureKeyResultAsync: async () => ({ value: null }),
15
+ }));
16
+
17
+ const { channelForCallback } = await import("../callback-routing.js");
18
+ const { discordTransport } = await import("../discord/transport.js");
19
+ const { slackTransport } = await import("../slack/transport.js");
20
+ const { telegramTransport } = await import("../telegram-bot/transport.js");
21
+ const { whatsappTransport } = await import("../whatsapp/transport.js");
22
+
23
+ describe("addressFor resolves to the channel's own routing state", () => {
24
+ test("slack: a chat, optionally under a thread", () => {
25
+ const room = slackTransport.addressFor!({ kind: "chat", chatId: "C123" });
26
+ expect(room).toEqual({
27
+ ctx: { callbackUrl: "/deliver/slack", params: {} },
28
+ chatId: "C123",
29
+ });
30
+ const thread = slackTransport.addressFor!({
31
+ kind: "chat",
32
+ chatId: "C123",
33
+ threadId: "1690000000.000001",
34
+ });
35
+ expect(thread).toEqual({
36
+ ctx: {
37
+ callbackUrl: "/deliver/slack?threadTs=1690000000.000001",
38
+ params: { threadTs: "1690000000.000001" },
39
+ },
40
+ chatId: "C123",
41
+ threadId: "1690000000.000001",
42
+ });
43
+ expect(channelForCallback(thread!.ctx.callbackUrl)).toBe("slack");
44
+ });
45
+
46
+ test("telegram: a chat with a topic; binds on send", () => {
47
+ expect(
48
+ telegramTransport.addressFor!({
49
+ kind: "chat",
50
+ chatId: "123",
51
+ threadId: "42",
52
+ }),
53
+ ).toEqual({
54
+ ctx: {
55
+ callbackUrl: "/deliver/telegram?threadId=42",
56
+ params: { threadId: "42" },
57
+ },
58
+ chatId: "123",
59
+ threadId: "42",
60
+ });
61
+ expect(telegramTransport.bindsChatOnProactiveSend).toBe(true);
62
+ });
63
+
64
+ test("discord: a chat with a thread, which is its own channel", () => {
65
+ const thread = discordTransport.addressFor!({
66
+ kind: "chat",
67
+ chatId: "C1",
68
+ threadId: "T1",
69
+ });
70
+ expect(thread).toEqual({
71
+ ctx: {
72
+ callbackUrl: "/deliver/discord?threadId=T1",
73
+ params: { threadId: "T1" },
74
+ },
75
+ chatId: "C1",
76
+ threadId: "T1",
77
+ });
78
+ expect(channelForCallback(thread!.ctx.callbackUrl)).toBe("discord");
79
+ expect(discordTransport.bindsChatOnProactiveSend).toBeUndefined();
80
+ });
81
+
82
+ test("whatsapp: the number is the chat; no threads; binds on send", () => {
83
+ expect(
84
+ whatsappTransport.addressFor!({
85
+ kind: "chat",
86
+ chatId: "12125550100",
87
+ threadId: "ignored",
88
+ }),
89
+ ).toEqual({
90
+ ctx: { callbackUrl: "/deliver/whatsapp", params: {} },
91
+ chatId: "12125550100",
92
+ });
93
+ expect(whatsappTransport.bindsChatOnProactiveSend).toBe(true);
94
+ });
95
+ });
@@ -1,4 +1,5 @@
1
1
  import type { ChannelId } from "../../channels/types.js";
2
+ import type { CallbackContext } from "./channel-transport.js";
2
3
 
3
4
  /**
4
5
  * Channels whose outbound replies the assistant delivers directly to the
@@ -22,6 +23,23 @@ export type DirectDeliveryChannel = (typeof DIRECT_DELIVERY_CHANNELS)[number];
22
23
 
23
24
  const CALLBACK_PREFIX = "/deliver/";
24
25
 
26
+ /**
27
+ * The callback context a channel's operations read for a direct delivery
28
+ * that no inbound message brought a callback for: the channel's `/deliver`
29
+ * path with its per-channel params, spelled the way `channelForCallback`
30
+ * resolves it and the way the transports read it.
31
+ */
32
+ export function directDeliveryContext(
33
+ channel: DirectDeliveryChannel,
34
+ params: Readonly<Record<string, string>> = {},
35
+ ): CallbackContext {
36
+ const query = new URLSearchParams(params).toString();
37
+ return {
38
+ callbackUrl: `${CALLBACK_PREFIX}${channel}${query ? `?${query}` : ""}`,
39
+ params,
40
+ };
41
+ }
42
+
25
43
  /**
26
44
  * Resolve a gateway callback URL to the direct-delivery channel that owns it, or
27
45
  * `undefined` when no channel delivers it directly.
@@ -97,6 +97,35 @@ export interface ReactionTarget {
97
97
  readonly action: "add" | "remove";
98
98
  }
99
99
 
100
+ /**
101
+ * Where a proactive send goes, in the caller's vocabulary rather than any
102
+ * channel's: a chat the caller already knows, optionally a thread within it.
103
+ * How a thread is spelled is each channel's own business.
104
+ *
105
+ * Tagged with its kind so a second way of naming a destination is additive
106
+ * rather than a reinterpretation of `chatId`. Reaching a person who has not
107
+ * been named as a chat is deliberately not a kind here: on Slack and Discord
108
+ * it costs a platform call whose result must be resolved at send time rather
109
+ * than stored, and nothing asks for it yet.
110
+ */
111
+ export type ProactiveTarget = {
112
+ readonly kind: "chat";
113
+ readonly chatId: string;
114
+ readonly threadId?: string;
115
+ };
116
+
117
+ /**
118
+ * A proactive target resolved into what this channel's operations read: the
119
+ * callback context carrying the channel's routing state, the chat id
120
+ * `deliver` posts to, and the thread the post lands in, absent when it is
121
+ * not in one.
122
+ */
123
+ export interface ProactiveAddress {
124
+ readonly ctx: CallbackContext;
125
+ readonly chatId: string;
126
+ readonly threadId?: string;
127
+ }
128
+
100
129
  /**
101
130
  * Direct outbound delivery for one channel, wrapping the channel's provider-API
102
131
  * send functions behind a uniform surface. Transports are registered statically
@@ -111,6 +140,33 @@ export interface ChannelTransport {
111
140
  /** Canonical source channel id, e.g. `"slack"`. */
112
141
  readonly channel: ChannelId;
113
142
 
143
+ /**
144
+ * Resolve a target the caller names into this channel's own routing state,
145
+ * for a send that no inbound message brought a callback for.
146
+ *
147
+ * Implementing it is the whole of declaring that the channel can be
148
+ * addressed from a named chat. A transport that can only answer on the
149
+ * callback an inbound message carried omits it, and a caller reads the
150
+ * omission as "not addressable" rather than switching on the channel's
151
+ * name. Returning `undefined` for a target this channel cannot address is
152
+ * the same answer for that target alone.
153
+ *
154
+ * Resolution is local: it reads the target and the channel's own
155
+ * vocabulary, and makes no platform call. A destination that has to be
156
+ * resolved over the network belongs behind its own capability, so this one
157
+ * stays cheap enough to call on every send.
158
+ */
159
+ addressFor?(target: ProactiveTarget): ProactiveAddress | undefined;
160
+
161
+ /**
162
+ * Whether a proactive send to a chat must also bind that chat's inbound
163
+ * conversation, so the next message from the chat resolves to the same
164
+ * conversation the post lives in. Declared by a channel whose inbound
165
+ * conversations are keyed per chat and can be reset between sends; omitted
166
+ * by one whose inbound binding is made at ingress and keyed by thread.
167
+ */
168
+ readonly bindsChatOnProactiveSend?: boolean;
169
+
114
170
  /** Deliver a rendered reply (text / approval / attachments). */
115
171
  deliver(
116
172
  ctx: CallbackContext,
@@ -1,6 +1,7 @@
1
1
  import { ChannelDeliveryError } from "@vellumai/gateway-client/http-delivery";
2
2
 
3
3
  import { getLogger } from "../../../util/logger.js";
4
+ import { directDeliveryContext } from "../callback-routing.js";
4
5
  import type {
5
6
  CallbackContext,
6
7
  ChannelTransport,
@@ -51,6 +52,22 @@ async function sendTarget(
51
52
  export const discordTransport: ChannelTransport = {
52
53
  channel: "discord",
53
54
 
55
+ /**
56
+ * A chat is a channel id, with `threadId` naming the thread, which is its
57
+ * own channel. Reaching a person who has not been named as a chat would
58
+ * mean opening their DM first, a platform call this resolution does not
59
+ * make; the inbound `dm` param below is how an event-carried callback says
60
+ * its `chatId` is a recipient rather than a room.
61
+ */
62
+ addressFor(target) {
63
+ const threadId = target.threadId?.trim();
64
+ return {
65
+ ctx: directDeliveryContext("discord", threadId ? { threadId } : {}),
66
+ chatId: target.chatId,
67
+ ...(threadId ? { threadId } : {}),
68
+ };
69
+ },
70
+
54
71
  // Discord clears a typing indicator after ten seconds.
55
72
  activityRefreshMs: 8_000,
56
73
 
@@ -162,10 +162,11 @@ describe("Slack adapter token routing", () => {
162
162
  globalThis.fetch = originalFetch;
163
163
  });
164
164
 
165
- test("bot-token only: reads and writes both authenticate with the bot token (regression guard for pre-user-token behavior)", async () => {
165
+ test("bot-token only: reads authenticate with the bot token (regression guard for pre-user-token behavior)", async () => {
166
166
  // With only a bot token stored, reads must fall back to the bot token
167
167
  // so the adapter keeps working for installs that haven't re-consented
168
- // the user scope. Writes stay on the bot token always.
168
+ // the user scope. Writes are not the adapter's: they go through the
169
+ // channel transport's senders, which always post as the bot.
169
170
  const resolved = await slackProvider.resolveConnection!();
170
171
  expect(resolved).toBeUndefined();
171
172
 
@@ -176,18 +177,12 @@ describe("Slack adapter token routing", () => {
176
177
  );
177
178
  expect(readCall).toBeDefined();
178
179
  expect(readCall!.authorization).toBe(`Bearer ${BOT_TOKEN}`);
179
-
180
- // Write path: sendMessage → /chat.postMessage must also use bot token.
181
- await slackProvider.sendMessage(undefined, "C123", "hello");
182
- const writeCall = captured.find((c) => c.url.includes("/chat.postMessage"));
183
- expect(writeCall).toBeDefined();
184
- expect(writeCall!.authorization).toBe(`Bearer ${BOT_TOKEN}`);
185
180
  });
186
181
 
187
- test("bot + user tokens: reads authenticate with the user token, writes with the bot token", async () => {
182
+ test("bot + user tokens: reads authenticate with the user token", async () => {
188
183
  // With both tokens stored, reads MUST flip to the user token so the
189
- // adapter can see channels the user is in but the bot isn't. Writes
190
- // MUST stay on the bot token so posts come from the bot identity.
184
+ // adapter can see channels the user is in but the bot isn't. Posts are
185
+ // the transport's and come from the bot identity there.
191
186
  getSecureKeyAsyncMock.mockImplementation(async (key: string) => {
192
187
  if (key === credentialKey("slack_channel", "bot_token")) {
193
188
  return BOT_TOKEN;
@@ -217,12 +212,6 @@ describe("Slack adapter token routing", () => {
217
212
  expect(historyCall).toBeDefined();
218
213
  expect(historyCall!.authorization).toBe(`Bearer ${USER_TOKEN}`);
219
214
 
220
- // Writes: sendMessage → bot token.
221
- await slackProvider.sendMessage(undefined, "C123", "hello");
222
- const sendCall = captured.find((c) => c.url.includes("/chat.postMessage"));
223
- expect(sendCall).toBeDefined();
224
- expect(sendCall!.authorization).toBe(`Bearer ${BOT_TOKEN}`);
225
-
226
215
  // markRead → user token. conversations.mark sets the read cursor for
227
216
  // the authenticated identity, so it must use the same token whose unread
228
217
  // counts the adapter exposes.
@@ -28,8 +28,6 @@ import type {
28
28
  Message,
29
29
  SearchOptions,
30
30
  SearchResult,
31
- SendOptions,
32
- SendResult,
33
31
  } from "../../provider-types.js";
34
32
  import { resolveSlackAuth } from "./auth.js";
35
33
  import * as slack from "./client.js";
@@ -144,24 +142,12 @@ function getReadAuth(connection?: OAuthConnection): OAuthConnection | string {
144
142
  return getSlackAuth(connection);
145
143
  }
146
144
 
147
- // SAFETY: content-creating writes (postMessage, updateMessage, deleteMessage,
148
- // reactions) MUST use the bot token. Using the user token would post as the
149
- // user, not as the bot. State-changing methods that target the authenticated
150
- // identity's own state (e.g. conversations.mark) should use the read auth so
151
- // the cursor matches the perspective the adapter exposes.
152
- /**
153
- * Resolve auth for content-creating write operations (postMessage and any
154
- * future reactions, joins, leaves, updates, or deletes).
155
- */
156
- function getWriteAuth(connection?: OAuthConnection): OAuthConnection | string {
157
- if (connection) {
158
- return connection;
159
- }
160
- if (_cachedSlackWriteAuth) {
161
- return _cachedSlackWriteAuth;
162
- }
163
- return getSlackAuth(connection);
164
- }
145
+ // SAFETY: content-creating writes (posts, updates, deletes, reactions) go
146
+ // through the channel transport's senders, which authenticate with the bot
147
+ // token so posts come from the bot identity. State-changing methods that
148
+ // target the authenticated identity's own state (e.g. conversations.mark)
149
+ // use the read auth so the cursor matches the perspective the adapter
150
+ // exposes.
165
151
 
166
152
  /**
167
153
  * Resolve the bot token (raw string) and pass it to `fn`. Returns the
@@ -171,7 +157,7 @@ function getWriteAuth(connection?: OAuthConnection): OAuthConnection | string {
171
157
  * (`OAuthConnection.withToken`) for callers that need a raw token to hand
172
158
  * to a non-Slack-client API call — currently `downloadSlackFile` for inline
173
159
  * file/image fetches. Slack-client method calls should keep going through
174
- * `getReadAuth` / `getWriteAuth` and pass the union through.
160
+ * `getReadAuth` and pass the union through.
175
161
  */
176
162
  export async function withSlackBotToken<T>(
177
163
  account: string | undefined,
@@ -830,29 +816,6 @@ export const slackProvider: MessagingProvider = {
830
816
  };
831
817
  },
832
818
 
833
- async sendMessage(
834
- connection: OAuthConnection | undefined,
835
- conversationId: string,
836
- text: string,
837
- options?: SendOptions,
838
- ): Promise<SendResult> {
839
- const auth = getWriteAuth(connection);
840
- const resp = await slack.postMessage(
841
- auth,
842
- conversationId,
843
- text,
844
- options?.threadId,
845
- );
846
- // The thread reported is the one the post landed in, which is the one
847
- // requested, since chat.postMessage threads exactly where it is told.
848
- return {
849
- id: resp.ts,
850
- timestamp: parseFloat(resp.ts) * 1000,
851
- conversationId: resp.channel,
852
- ...(options?.threadId ? { threadId: options.threadId } : {}),
853
- };
854
- },
855
-
856
819
  async getThreadReplies(
857
820
  connection: OAuthConnection | undefined,
858
821
  conversationId: string,
@@ -3,6 +3,7 @@ import { ChannelDeliveryError } from "@vellumai/gateway-client/http-delivery";
3
3
 
4
4
  import { extractThreadTsFromCallbackUrl } from "../../../channels/slack-callback-url.js";
5
5
  import { getLogger } from "../../../util/logger.js";
6
+ import { directDeliveryContext } from "../callback-routing.js";
6
7
  import type { ChannelTransport } from "../channel-transport.js";
7
8
  import { SLACK_STREAM_MARKDOWN_LIMIT } from "./api.js";
8
9
  import {
@@ -25,6 +26,21 @@ function mutedBlocks(text: string): KnownBlock[] {
25
26
  export const slackTransport: ChannelTransport = {
26
27
  channel: "slack",
27
28
 
29
+ /**
30
+ * A chat is a channel or DM id, with `threadTs` naming the thread to post
31
+ * under. Reaching a person who has not been named as a chat would mean
32
+ * opening the DM first (`conversations.open`), a platform call this
33
+ * resolution does not make.
34
+ */
35
+ addressFor(target) {
36
+ const threadTs = target.threadId?.trim();
37
+ return {
38
+ ctx: directDeliveryContext("slack", threadTs ? { threadTs } : {}),
39
+ chatId: target.chatId,
40
+ ...(threadTs ? { threadId: threadTs } : {}),
41
+ };
42
+ },
43
+
28
44
  describeReactionEmoji: describeSlackReactionEmoji,
29
45
 
30
46
  async react(target) {
@@ -6,9 +6,6 @@
6
6
 
7
7
  import type { OAuthConnection } from "../../../oauth/connection.js";
8
8
  import { getConnectionByProvider } from "../../../oauth/oauth-store.js";
9
- import { getOrCreateConversation } from "../../../persistence/conversation-key-store.js";
10
- import { buildScopedConversationKey } from "../../../persistence/delivery-crud.js";
11
- import { upsertOutboundBinding } from "../../../persistence/external-conversation-store.js";
12
9
  import { credentialKey } from "../../../security/credential-key.js";
13
10
  import { getSecureKeyAsync } from "../../../security/secure-keys.js";
14
11
  import type { MessagingProvider } from "../../provider.js";
@@ -20,8 +17,6 @@ import type {
20
17
  Message,
21
18
  SearchOptions,
22
19
  SearchResult,
23
- SendOptions,
24
- SendResult,
25
20
  } from "../../provider-types.js";
26
21
  import * as telegram from "./client.js";
27
22
 
@@ -34,7 +29,7 @@ export const telegramBotMessagingProvider: MessagingProvider = {
34
29
  id: "telegram",
35
30
  displayName: "Telegram",
36
31
  credentialService: "telegram",
37
- capabilities: new Set(["send"]),
32
+ capabilities: new Set(),
38
33
 
39
34
  async isConnected(): Promise<boolean> {
40
35
  const conn = getConnectionByProvider("telegram");
@@ -95,48 +90,6 @@ export const telegramBotMessagingProvider: MessagingProvider = {
95
90
  }
96
91
  },
97
92
 
98
- async sendMessage(
99
- _connection: OAuthConnection | undefined,
100
- conversationId: string,
101
- text: string,
102
- _options?: SendOptions,
103
- ): Promise<SendResult> {
104
- const sent = await telegram.sendMessage(conversationId, text);
105
- if (!sent.lastMessageId) {
106
- throw new Error(
107
- "Telegram accepted the message but returned no message id",
108
- );
109
- }
110
-
111
- // Upsert external conversation binding so deleted/reset syncs are
112
- // resurrected when an outbound message is sent. This ensures the
113
- // conversation key mapping and binding exist for the next inbound.
114
- try {
115
- const sourceChannel = "telegram";
116
- const conversationKey = buildScopedConversationKey(
117
- sourceChannel,
118
- conversationId,
119
- );
120
- const { conversationId: internalId } =
121
- getOrCreateConversation(conversationKey);
122
- upsertOutboundBinding({
123
- conversationId: internalId,
124
- sourceChannel,
125
- externalChatId: conversationId,
126
- });
127
- } catch {
128
- // Best-effort — don't fail the send if binding upsert fails
129
- }
130
-
131
- // The id is the one Telegram assigned, so a later reaction, edit, or
132
- // delete carrying that id resolves to this send.
133
- return {
134
- id: sent.lastMessageId,
135
- timestamp: Date.now(),
136
- conversationId,
137
- };
138
- },
139
-
140
93
  async listConversations(
141
94
  _connection?: OAuthConnection,
142
95
  _options?: ListOptions,