@vellumai/assistant 0.11.2 → 0.11.3-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 (720) hide show
  1. package/ARCHITECTURE.md +58 -3
  2. package/docs/trusted-contact-access.md +9 -5
  3. package/docs/vellum-doctor.md +195 -0
  4. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +39 -0
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +10 -0
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +6 -0
  7. package/node_modules/@vellumai/ces-client/src/index.ts +1 -0
  8. package/node_modules/@vellumai/ces-client/src/rpc-client.ts +19 -2
  9. package/node_modules/@vellumai/environments/package.json +2 -1
  10. package/node_modules/@vellumai/environments/src/__tests__/cloud-assistant-hub-url.test.ts +38 -0
  11. package/node_modules/@vellumai/environments/src/__tests__/install-layout.test.ts +98 -0
  12. package/node_modules/@vellumai/environments/src/__tests__/package-boundary.test.ts +5 -5
  13. package/node_modules/@vellumai/environments/src/index.ts +17 -6
  14. package/node_modules/@vellumai/environments/src/install-layout.ts +49 -0
  15. package/node_modules/@vellumai/environments/src/seeds.ts +29 -0
  16. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +39 -0
  17. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +10 -0
  18. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +6 -0
  19. package/node_modules/@vellumai/gateway-client/src/index.ts +4 -0
  20. package/node_modules/@vellumai/gateway-client/src/verification-session-contract.ts +91 -6
  21. package/node_modules/@vellumai/ipc-server-utils/src/endpoint.test.ts +36 -0
  22. package/node_modules/@vellumai/ipc-server-utils/src/endpoint.ts +142 -0
  23. package/node_modules/@vellumai/ipc-server-utils/src/index.ts +2 -0
  24. package/node_modules/@vellumai/ipc-server-utils/src/listen-options.ts +3 -0
  25. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +15 -1
  26. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +17 -2
  27. package/node_modules/@vellumai/service-contracts/src/channels.ts +39 -0
  28. package/node_modules/@vellumai/service-contracts/src/ingress.ts +10 -0
  29. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +6 -0
  30. package/node_modules/@vellumai/slack-text/src/index.ts +16 -2
  31. package/openapi.yaml +589 -30
  32. package/package.json +1 -1
  33. package/src/__tests__/agent-image-optimize.test.ts +4 -4
  34. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +113 -66
  35. package/src/__tests__/agent-loop-override-profile.test.ts +38 -48
  36. package/src/__tests__/agent-loop-subagent-billing-headers.test.ts +166 -0
  37. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +1 -0
  38. package/src/__tests__/agent-wake-override-profile.test.ts +1 -0
  39. package/src/__tests__/answered-question-persistence.test.ts +240 -0
  40. package/src/__tests__/anthropic-provider.test.ts +165 -30
  41. package/src/__tests__/app-control-flow.test.ts +9 -8
  42. package/src/__tests__/assistant-event-hub.test.ts +48 -0
  43. package/src/__tests__/attachments-store-heic-normalize.test.ts +16 -12
  44. package/src/__tests__/attachments-store.test.ts +130 -68
  45. package/src/__tests__/attachments.test.ts +36 -36
  46. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  47. package/src/__tests__/call-start-guardian-guard.test.ts +18 -4
  48. package/src/__tests__/canned-reply-release.test.ts +86 -0
  49. package/src/__tests__/channel-guardian.test.ts +59 -29
  50. package/src/__tests__/channel-readiness-telegram-remote.test.ts +154 -0
  51. package/src/__tests__/channel-setup-panel-ack.test.ts +6 -5
  52. package/src/__tests__/client-os-metadata-persistence.test.ts +103 -4
  53. package/src/__tests__/compactor-image-manifest-trust.test.ts +1 -1
  54. package/src/__tests__/consult-deadline.test.ts +65 -0
  55. package/src/__tests__/conversation-agent-loop.test.ts +438 -2
  56. package/src/__tests__/conversation-disk-view-integration.test.ts +5 -1
  57. package/src/__tests__/conversation-disk-view.test.ts +4 -4
  58. package/src/__tests__/conversation-error.test.ts +22 -3
  59. package/src/__tests__/conversation-fork-crud.test.ts +5 -1
  60. package/src/__tests__/conversation-fork-referential.test.ts +352 -0
  61. package/src/__tests__/conversation-fork-retrospective.test.ts +5 -1
  62. package/src/__tests__/conversation-group-tools.test.ts +32 -2
  63. package/src/__tests__/conversation-inference-profile-route.test.ts +27 -1
  64. package/src/__tests__/conversation-initial-prompt.test.ts +51 -22
  65. package/src/__tests__/conversation-key-store-bootstrap-cleanup.test.ts +119 -0
  66. package/src/__tests__/conversation-key-store-origin.test.ts +105 -0
  67. package/src/__tests__/conversation-list-recency-ordering.test.ts +104 -0
  68. package/src/__tests__/conversation-origin-at-creation.test.ts +153 -0
  69. package/src/__tests__/conversation-origin-channel-filter.test.ts +95 -0
  70. package/src/__tests__/conversation-placement-promotion.test.ts +172 -0
  71. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +123 -0
  72. package/src/__tests__/conversation-queue.test.ts +433 -1
  73. package/src/__tests__/conversation-retry-route.test.ts +3 -1
  74. package/src/__tests__/conversation-routes-hidden-queue.test.ts +64 -1
  75. package/src/__tests__/conversation-store-ephemeral.test.ts +4 -0
  76. package/src/__tests__/conversation-store.test.ts +26 -10
  77. package/src/__tests__/conversation-summarize-route.test.ts +1 -1
  78. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +15 -6
  79. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -5
  80. package/src/__tests__/conversation-surfaces-app-control.test.ts +12 -11
  81. package/src/__tests__/conversation-surfaces-app-open.test.ts +11 -5
  82. package/src/__tests__/conversation-surfaces-data-persist.test.ts +5 -4
  83. package/src/__tests__/conversation-surfaces-history-restored-completion.test.ts +967 -0
  84. package/src/__tests__/conversation-surfaces-persisted-info.test.ts +312 -0
  85. package/src/__tests__/conversation-surfaces-queued-emit.test.ts +6 -5
  86. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +12 -8
  87. package/src/__tests__/conversation-surfaces-standalone.test.ts +21 -11
  88. package/src/__tests__/conversation-surfaces-state-update.test.ts +7 -11
  89. package/src/__tests__/conversation-surfaces-table-action.test.ts +7 -6
  90. package/src/__tests__/conversation-surfaces-task-progress.test.ts +11 -6
  91. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +9 -6
  92. package/src/__tests__/conversation-tool-setup-attribution.test.ts +70 -7
  93. package/src/__tests__/conversation-wait-for-idle.test.ts +6 -11
  94. package/src/__tests__/credential-prompt-route.test.ts +57 -7
  95. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  96. package/src/__tests__/cu-unified-flow.test.ts +89 -28
  97. package/src/__tests__/custom-group-listing-visibility.test.ts +1 -1
  98. package/src/__tests__/db-schedule-syntax-migration.test.ts +4 -1
  99. package/src/__tests__/discord-access-request-privacy.test.ts +366 -0
  100. package/src/__tests__/discord-requester-notice-privacy.test.ts +87 -0
  101. package/src/__tests__/disk-pressure-guard.test.ts +55 -55
  102. package/src/__tests__/disk-pressure-routes.test.ts +10 -10
  103. package/src/__tests__/disk-pressure-tools.test.ts +2 -2
  104. package/src/__tests__/disk-usage.test.ts +35 -20
  105. package/src/__tests__/document-sync-tags.test.ts +379 -0
  106. package/src/__tests__/document-update-default-surface.test.ts +28 -0
  107. package/src/__tests__/dynamic-skill-background-guard.test.ts +2 -2
  108. package/src/__tests__/emit-signal-routing-intent.test.ts +3 -4
  109. package/src/__tests__/ensure-conversation-exists.test.ts +8 -5
  110. package/src/__tests__/events-dev-bypass-actor.test.ts +183 -1
  111. package/src/__tests__/evict-conversations-for-reload.test.ts +90 -0
  112. package/src/__tests__/file-list-tool.test.ts +12 -10
  113. package/src/__tests__/file-ops-service.test.ts +151 -44
  114. package/src/__tests__/file-read-container-boundary.test.ts +137 -0
  115. package/src/__tests__/file-read-tool.test.ts +21 -9
  116. package/src/__tests__/file-url-path-guard.test.ts +83 -0
  117. package/src/__tests__/filesystem-tools.test.ts +82 -66
  118. package/src/__tests__/guardian-outbound-http.test.ts +8 -8
  119. package/src/__tests__/helpers/gateway-verification-sessions-stub.ts +14 -1
  120. package/src/__tests__/helpers/hub-clients.ts +42 -0
  121. package/src/__tests__/helpers/mock-conversation.ts +46 -0
  122. package/src/__tests__/helpers/verification-sessions-ipc-sim.ts +61 -57
  123. package/src/__tests__/history-repair.test.ts +112 -12
  124. package/src/__tests__/host-bash-proxy.test.ts +10 -5
  125. package/src/__tests__/host-bash-routes.test.ts +67 -0
  126. package/src/__tests__/host-browser-proxy.test.ts +2 -2
  127. package/src/__tests__/host-cu-proxy.test.ts +3 -7
  128. package/src/__tests__/host-file-proxy.test.ts +3 -7
  129. package/src/__tests__/host-file-read-tool.test.ts +5 -1
  130. package/src/__tests__/host-transfer-proxy-targeted.test.ts +6 -4
  131. package/src/__tests__/http-conversation-lineage.test.ts +2 -5
  132. package/src/__tests__/http-user-message-parity.test.ts +44 -0
  133. package/src/__tests__/identity-routes.test.ts +13 -13
  134. package/src/__tests__/image-conversion.test.ts +29 -26
  135. package/src/__tests__/image-source-path-reinject.test.ts +2 -2
  136. package/src/__tests__/inference-profile-session-handler.test.ts +95 -0
  137. package/src/__tests__/inference-profile-session-ipc.test.ts +25 -0
  138. package/src/__tests__/introduction-card-resolver.test.ts +6 -1
  139. package/src/__tests__/list-messages-attachments.test.ts +56 -32
  140. package/src/__tests__/list-messages-background-tool-completion.test.ts +4 -4
  141. package/src/__tests__/list-messages-client-message-id.test.ts +2 -2
  142. package/src/__tests__/list-messages-hidden-metadata.test.ts +16 -16
  143. package/src/__tests__/list-messages-page-latest.test.ts +61 -61
  144. package/src/__tests__/list-messages-provider-error.test.ts +6 -6
  145. package/src/__tests__/list-messages-queued.test.ts +49 -6
  146. package/src/__tests__/list-messages-system-card.test.ts +4 -4
  147. package/src/__tests__/list-messages-tool-merge.test.ts +9 -9
  148. package/src/__tests__/llm-resolver-override-or-default.test.ts +141 -0
  149. package/src/__tests__/llm-usage-store.test.ts +55 -0
  150. package/src/__tests__/manual-token-reconciliation.test.ts +45 -0
  151. package/src/__tests__/mcp-tool-annotations-risk.test.ts +133 -0
  152. package/src/__tests__/media-stream-server-integration.test.ts +36 -0
  153. package/src/__tests__/migration-export-http.test.ts +1 -1
  154. package/src/__tests__/migration-import-commit-http.test.ts +1 -1
  155. package/src/__tests__/migration-import-preflight-http.test.ts +1 -1
  156. package/src/__tests__/migration-validate-http.test.ts +1 -1
  157. package/src/__tests__/mtime-cache.test.ts +65 -3
  158. package/src/__tests__/notification-platform-adapter.test.ts +104 -0
  159. package/src/__tests__/notification-schedule-notify-dedup.test.ts +1 -1
  160. package/src/__tests__/oauth-provider-profiles.test.ts +4 -2
  161. package/src/__tests__/oauth-provider-seed-logos.test.ts +65 -0
  162. package/src/__tests__/oauth-provider-serializer.test.ts +17 -0
  163. package/src/__tests__/oauth-providers-routes.test.ts +1 -0
  164. package/src/__tests__/openai-responses-prompt-cache.test.ts +5 -2
  165. package/src/__tests__/path-policy.test.ts +164 -1
  166. package/src/__tests__/persist-media-references.test.ts +2 -2
  167. package/src/__tests__/persist-unsendable-image-downscale.test.ts +5 -5
  168. package/src/__tests__/persist-unsendable-image.test.ts +23 -23
  169. package/src/__tests__/persona-resolver.test.ts +154 -0
  170. package/src/__tests__/plugin-api-webhook-url.test.ts +170 -0
  171. package/src/__tests__/plugin-import-boundary-guard.test.ts +6 -0
  172. package/src/__tests__/processing-flag-persist-failure.test.ts +15 -10
  173. package/src/__tests__/profile-availability-incomplete.test.ts +113 -0
  174. package/src/__tests__/profiler-routes.test.ts +13 -13
  175. package/src/__tests__/prompt-cache-cross-turn-stability.test.ts +593 -0
  176. package/src/__tests__/proxy-approval-callback.test.ts +3 -3
  177. package/src/__tests__/queued-message-cancel-and-steer.test.ts +189 -0
  178. package/src/__tests__/queued-message-steer-actor-scoping.test.ts +164 -0
  179. package/src/__tests__/resolve-trust-class.test.ts +20 -0
  180. package/src/__tests__/run-conversation-turn-persistence.test.ts +1 -1
  181. package/src/__tests__/run-due-schedules.test.ts +174 -0
  182. package/src/__tests__/runtime-attachment-metadata.test.ts +15 -7
  183. package/src/__tests__/scaffold-managed-skill-tool.test.ts +166 -0
  184. package/src/__tests__/schedule-routes.test.ts +730 -8
  185. package/src/__tests__/schedule-store.test.ts +540 -0
  186. package/src/__tests__/schedule-tools.test.ts +103 -0
  187. package/src/__tests__/scheduler-wake.test.ts +5 -0
  188. package/src/__tests__/secret-routes-platform-proxy.test.ts +29 -1
  189. package/src/__tests__/secure-keys.test.ts +91 -0
  190. package/src/__tests__/server-history-render.test.ts +97 -7
  191. package/src/__tests__/size-guard.test.ts +8 -8
  192. package/src/__tests__/skill-docs-sync-guard.test.ts +3 -2
  193. package/src/__tests__/skill-tool-factory.test.ts +247 -0
  194. package/src/__tests__/skills-install-staging.test.ts +15 -6
  195. package/src/__tests__/slack-edit-ordering-characterization.test.ts +103 -0
  196. package/src/__tests__/slack-inbound-verification.test.ts +4 -1
  197. package/src/__tests__/slack-mention-binding-invariance.test.ts +103 -0
  198. package/src/__tests__/slack-mention-provider-leak-audit.test.ts +135 -0
  199. package/src/__tests__/starter-task-flow.test.ts +5 -4
  200. package/src/__tests__/steer-on-enqueue-question.test.ts +3 -2
  201. package/src/__tests__/stt-language-catalog-parity.test.ts +38 -0
  202. package/src/__tests__/subagent-allowlist-validation.test.ts +26 -4
  203. package/src/__tests__/subagent-cron-run-attribution.test.ts +301 -0
  204. package/src/__tests__/subagent-disposal.test.ts +10 -0
  205. package/src/__tests__/subagent-fork-notifications.test.ts +10 -0
  206. package/src/__tests__/subagent-fork-prompt-role.test.ts +85 -11
  207. package/src/__tests__/subagent-fork-spawn.test.ts +15 -7
  208. package/src/__tests__/subagent-manager-notify.test.ts +148 -0
  209. package/src/__tests__/subagent-notify-parent.test.ts +1 -1
  210. package/src/__tests__/subagent-persistence.test.ts +141 -0
  211. package/src/__tests__/subagent-role-registry.test.ts +172 -63
  212. package/src/__tests__/subagent-role-resolution.test.ts +88 -0
  213. package/src/__tests__/subagent-spawn-and-await.test.ts +48 -0
  214. package/src/__tests__/subagent-spawn-tool-fork.test.ts +111 -10
  215. package/src/__tests__/subagent-terminal-message.test.ts +88 -1
  216. package/src/__tests__/subagent-tool-gate-mode.test.ts +225 -10
  217. package/src/__tests__/subagent-tools.test.ts +1901 -108
  218. package/src/__tests__/surface-completion-compaction-boundary.test.ts +124 -0
  219. package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +298 -0
  220. package/src/__tests__/system-prompt.test.ts +82 -7
  221. package/src/__tests__/tool-grant-request-escalation.test.ts +40 -1
  222. package/src/__tests__/tool-result-spool.test.ts +74 -18
  223. package/src/__tests__/tool-side-effects-documents.test.ts +174 -0
  224. package/src/__tests__/tool-side-effects-hook-failures.test.ts +129 -0
  225. package/src/__tests__/tools-audio-read.test.ts +10 -10
  226. package/src/__tests__/tools-get-route.test.ts +85 -12
  227. package/src/__tests__/tools-list-cli.test.ts +171 -0
  228. package/src/__tests__/top-level-renderer.test.ts +54 -1
  229. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +68 -1
  230. package/src/__tests__/trusted-contact-multichannel.test.ts +4 -1
  231. package/src/__tests__/turn-tail-chain.test.ts +135 -0
  232. package/src/__tests__/ui-choice-copy-surfaces.test.ts +6 -4
  233. package/src/__tests__/ui-visual-surface.test.ts +5 -4
  234. package/src/__tests__/ui-work-result-surface.test.ts +5 -4
  235. package/src/__tests__/unified-turn-context-visible-app.test.ts +7 -8
  236. package/src/__tests__/usage-routes.test.ts +62 -0
  237. package/src/__tests__/verification-outbound-delivery.test.ts +176 -0
  238. package/src/__tests__/visible-app-context.test.ts +20 -4
  239. package/src/__tests__/web-search-history.test.ts +96 -0
  240. package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +6 -6
  241. package/src/__tests__/workspace-migration-140-repair-seed-pinned-memory-v3-live.test.ts +330 -0
  242. package/src/acp/failure-error.ts +3 -5
  243. package/src/agent/attachments.ts +26 -24
  244. package/src/agent/history-repair/history-repair.ts +27 -26
  245. package/src/agent/image-optimize.ts +6 -6
  246. package/src/agent/loop.ts +40 -22
  247. package/src/agent/message-types.ts +3 -3
  248. package/src/api/constants/document-tools.ts +55 -0
  249. package/src/api/events/question-answered.ts +53 -0
  250. package/src/api/events/tool-result.ts +7 -0
  251. package/src/api/index.ts +13 -0
  252. package/src/api/responses/conversation-message.ts +11 -0
  253. package/src/api/surfaces-oauth-connect.test.ts +96 -0
  254. package/src/api/surfaces.ts +36 -8
  255. package/src/approvals/guardian-channel-delivery.ts +83 -0
  256. package/src/approvals/guardian-expiry-notifier.ts +13 -11
  257. package/src/approvals/guardian-request-resolvers.ts +222 -135
  258. package/src/calls/__tests__/voice-session-bridge.test.ts +50 -0
  259. package/src/calls/media-stream-server.ts +9 -1
  260. package/src/calls/voice-session-bridge.ts +38 -0
  261. package/src/channels/__tests__/gateway-verification-sessions.test.ts +3 -3
  262. package/src/channels/config.ts +7 -2
  263. package/src/channels/gateway-verification-sessions.ts +7 -2
  264. package/src/channels/types.ts +7 -1
  265. package/src/cli/commands/__tests__/cache.test.ts +7 -4
  266. package/src/cli/commands/__tests__/cli-test-harness.ts +82 -0
  267. package/src/cli/commands/__tests__/conversations-wake.test.ts +51 -6
  268. package/src/cli/commands/__tests__/inference-models.test.ts +14 -0
  269. package/src/cli/commands/__tests__/inference-profiles.test.ts +44 -1
  270. package/src/cli/commands/__tests__/inference-providers.test.ts +7 -0
  271. package/src/cli/commands/__tests__/plugins.test.ts +692 -0
  272. package/src/cli/commands/__tests__/schedules.test.ts +136 -37
  273. package/src/cli/commands/conversations.ts +7 -3
  274. package/src/cli/commands/credentials.help.ts +7 -0
  275. package/src/cli/commands/inference-profiles.ts +9 -3
  276. package/src/cli/commands/inference.help.ts +16 -2
  277. package/src/cli/commands/mcp.help.ts +4 -2
  278. package/src/cli/commands/memory/memory-retrospective.ts +18 -0
  279. package/src/cli/commands/notifications.ts +3 -6
  280. package/src/cli/commands/oauth/connect-surface-guidance.test.ts +16 -0
  281. package/src/cli/commands/oauth/connect-surface-guidance.ts +25 -7
  282. package/src/cli/commands/oauth/connect.test.ts +26 -1
  283. package/src/cli/commands/oauth/connect.ts +4 -1
  284. package/src/cli/commands/oauth/index.help.ts +3 -1
  285. package/src/cli/commands/oauth/providers.ts +3 -0
  286. package/src/cli/commands/plugins.help.ts +25 -2
  287. package/src/cli/commands/plugins.ts +289 -10
  288. package/src/cli/commands/schedules.help.ts +14 -12
  289. package/src/cli/commands/schedules.ts +19 -5
  290. package/src/cli/commands/tools.help.ts +4 -2
  291. package/src/cli/commands/tools.ts +38 -0
  292. package/src/cli/lib/__tests__/inspect-plugin.test.ts +1 -0
  293. package/src/cli/lib/__tests__/install-from-github.test.ts +79 -0
  294. package/src/cli/lib/__tests__/install-from-platform.test.ts +47 -1
  295. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +86 -7
  296. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +122 -0
  297. package/src/cli/lib/install-from-github.ts +61 -0
  298. package/src/cli/lib/install-from-platform.ts +13 -2
  299. package/src/cli/lib/plugin-surfaces.ts +124 -4
  300. package/src/cli/lib/upgrade-plugin.ts +18 -0
  301. package/src/cli.ts +1 -1
  302. package/src/config/__tests__/deployment-context-defaults.test.ts +73 -0
  303. package/src/config/__tests__/memory-retrospective-schema.test.ts +40 -0
  304. package/src/config/__tests__/plugin-updates-schema.test.ts +43 -0
  305. package/src/config/__tests__/profile-tool-support.test.ts +131 -0
  306. package/src/config/bundled-skills/AGENTS.md +1 -1
  307. package/src/config/bundled-skills/app-builder/SKILL.md +1 -1
  308. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +3 -3
  309. package/src/config/bundled-skills/schedule/SKILL.md +5 -1
  310. package/src/config/bundled-skills/schedule/TOOLS.json +2 -2
  311. package/src/config/bundled-skills/settings/TOOLS.json +2 -2
  312. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +1 -0
  313. package/src/config/bundled-skills/subagent/SKILL.md +53 -22
  314. package/src/config/bundled-skills/subagent/TOOLS.json +14 -13
  315. package/src/config/bundled-skills/visualize/SKILL.md +1 -1
  316. package/src/config/default-profile-catalog.ts +6 -2
  317. package/src/config/feature-flag-registry.json +25 -17
  318. package/src/config/llm-resolver.ts +45 -15
  319. package/src/config/loader.ts +70 -1
  320. package/src/config/profile-tool-support.ts +110 -0
  321. package/src/config/sanitize-for-transfer.ts +11 -0
  322. package/src/config/schema.ts +4 -0
  323. package/src/config/schemas/__tests__/stt.test.ts +20 -5
  324. package/src/config/schemas/channels.ts +13 -0
  325. package/src/config/schemas/memory-retrospective.ts +25 -0
  326. package/src/config/schemas/plugin-updates.ts +55 -0
  327. package/src/config/schemas/services.ts +8 -4
  328. package/src/config/schemas/stt.ts +17 -5
  329. package/src/config/schemas/timeouts.ts +1 -1
  330. package/src/contacts/__tests__/guardian-delivery-reader.test.ts +16 -0
  331. package/src/contacts/guardian-delivery-reader.ts +29 -2
  332. package/src/context/compactor.ts +4 -4
  333. package/src/context/post-turn-tool-result-truncation.ts +32 -16
  334. package/src/context/strip-injections.ts +42 -0
  335. package/src/context/tool-result-spool.ts +32 -18
  336. package/src/credential-execution/ces-runtime.ts +3 -0
  337. package/src/credential-execution/client.ts +22 -1
  338. package/src/credential-execution/process-manager.test.ts +99 -0
  339. package/src/credential-execution/process-manager.ts +62 -16
  340. package/src/daemon/__tests__/abort-null-controller.test.ts +141 -5
  341. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +8 -12
  342. package/src/daemon/__tests__/install-assistant-command.test.ts +234 -0
  343. package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +2 -4
  344. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +20 -29
  345. package/src/daemon/channel-ui-capability.ts +4 -4
  346. package/src/daemon/conversation-agent-loop-handlers.ts +117 -61
  347. package/src/daemon/conversation-agent-loop.ts +318 -148
  348. package/src/daemon/conversation-attachments.ts +1 -1
  349. package/src/daemon/conversation-error.ts +8 -1
  350. package/src/daemon/conversation-initial-prompt.ts +16 -38
  351. package/src/daemon/conversation-lifecycle.ts +154 -32
  352. package/src/daemon/conversation-messaging.ts +106 -43
  353. package/src/daemon/conversation-process.ts +131 -33
  354. package/src/daemon/conversation-queue-manager.ts +10 -0
  355. package/src/daemon/conversation-runtime-assembly.ts +9 -4
  356. package/src/daemon/conversation-store.ts +52 -3
  357. package/src/daemon/conversation-surface-state.ts +9 -0
  358. package/src/daemon/conversation-surfaces.ts +403 -210
  359. package/src/daemon/conversation-tool-setup.ts +56 -7
  360. package/src/daemon/conversation-turn-finalize.ts +134 -89
  361. package/src/daemon/conversation.ts +111 -27
  362. package/src/daemon/disk-pressure-guard-lifecycle.ts +2 -2
  363. package/src/daemon/disk-pressure-guard.ts +3 -3
  364. package/src/daemon/doordash-steps.ts +5 -5
  365. package/src/daemon/external-plugins-bootstrap.ts +7 -1
  366. package/src/daemon/handlers/config-channels.ts +48 -5
  367. package/src/daemon/handlers/conversation-history.ts +1 -1
  368. package/src/daemon/handlers/conversations.ts +62 -17
  369. package/src/daemon/handlers/shared.ts +12 -0
  370. package/src/daemon/handlers/skills.ts +11 -1
  371. package/src/daemon/identity-helpers.ts +38 -10
  372. package/src/daemon/install-assistant-command.ts +322 -0
  373. package/src/daemon/lifecycle.ts +14 -3
  374. package/src/daemon/message-types/conversations.ts +6 -7
  375. package/src/daemon/message-types/sync.ts +1 -0
  376. package/src/daemon/persist-media-references.ts +23 -14
  377. package/src/daemon/process-message.ts +6 -6
  378. package/src/daemon/tool-setup-types.ts +19 -122
  379. package/src/daemon/tool-side-effects.ts +17 -94
  380. package/src/daemon/trust-context-types.ts +39 -0
  381. package/src/daemon/trust-context.ts +14 -6
  382. package/src/daemon/turn-tail-chain.ts +73 -0
  383. package/src/daemon/web-search-history.ts +47 -33
  384. package/src/heartbeat/heartbeat-service.ts +6 -6
  385. package/src/ipc/__tests__/socket-path.test.ts +21 -0
  386. package/src/ipc/assistant-server.ts +17 -10
  387. package/src/ipc/cli-client.ts +10 -1
  388. package/src/ipc/routes/__tests__/documents-sync-ipc-routes.test.ts +56 -0
  389. package/src/ipc/routes/documents-sync-ipc-routes.ts +42 -0
  390. package/src/ipc/socket-cleanup.ts +9 -7
  391. package/src/ipc/socket-path.ts +4 -96
  392. package/src/live-voice/__tests__/live-voice-archive.test.ts +16 -16
  393. package/src/live-voice/__tests__/live-voice-attach-image.test.ts +209 -0
  394. package/src/live-voice/__tests__/live-voice-connection.test.ts +20 -0
  395. package/src/live-voice/__tests__/live-voice-integration.test.ts +7 -5
  396. package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +265 -0
  397. package/src/live-voice/__tests__/live-voice-stt.test.ts +6 -1
  398. package/src/live-voice/__tests__/live-voice-vad.test.ts +35 -15
  399. package/src/live-voice/__tests__/protocol.test.ts +52 -0
  400. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +3 -0
  401. package/src/live-voice/live-voice-archive.ts +13 -13
  402. package/src/live-voice/live-voice-connection.ts +7 -1
  403. package/src/live-voice/live-voice-photo.ts +169 -0
  404. package/src/live-voice/live-voice-session.ts +108 -4
  405. package/src/live-voice/protocol.ts +89 -1
  406. package/src/mcp/client.ts +10 -0
  407. package/src/messaging/providers/__tests__/callback-routing.test.ts +5 -1
  408. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +66 -4
  409. package/src/messaging/providers/callback-routing.ts +1 -0
  410. package/src/messaging/providers/discord/api.ts +312 -0
  411. package/src/messaging/providers/discord/dm-delivery.test.ts +204 -0
  412. package/src/messaging/providers/discord/render.test.ts +204 -0
  413. package/src/messaging/providers/discord/render.ts +195 -0
  414. package/src/messaging/providers/discord/send.test.ts +179 -0
  415. package/src/messaging/providers/discord/send.ts +212 -0
  416. package/src/messaging/providers/discord/transport.ts +81 -0
  417. package/src/messaging/providers/index.ts +3 -1
  418. package/src/messaging/providers/retry-policy.test.ts +247 -0
  419. package/src/messaging/providers/retry-policy.ts +206 -0
  420. package/src/messaging/providers/slack/mention-source.test.ts +285 -0
  421. package/src/messaging/providers/slack/mention-source.ts +287 -0
  422. package/src/messaging/providers/telegram-bot/api.ts +47 -130
  423. package/src/messaging/providers/whatsapp/api.ts +47 -136
  424. package/src/monitoring/__tests__/db-integrity-sample.test.ts +4 -1
  425. package/src/monitoring/__tests__/plugin-auto-update.test.ts +332 -0
  426. package/src/monitoring/db-integrity-sample.ts +6 -1
  427. package/src/monitoring/plugin-auto-update.ts +359 -0
  428. package/src/monitoring/resource-sampler.ts +21 -5
  429. package/src/monitoring/worker.ts +13 -0
  430. package/src/notifications/__tests__/assistant-reply-producer.test.ts +375 -2
  431. package/src/notifications/__tests__/broadcaster.test.ts +37 -1
  432. package/src/notifications/__tests__/copy-composer.test.ts +165 -0
  433. package/src/notifications/__tests__/emit-signal-pipeline-failure.test.ts +205 -0
  434. package/src/notifications/__tests__/notification-utils.test.ts +249 -0
  435. package/src/notifications/adapters/platform.ts +31 -2
  436. package/src/notifications/assistant-reply-producer.ts +79 -32
  437. package/src/notifications/broadcaster.ts +30 -10
  438. package/src/notifications/copy-composer.ts +69 -0
  439. package/src/notifications/emit-signal.ts +82 -15
  440. package/src/notifications/events-store.ts +10 -2
  441. package/src/notifications/home-feed-side-effect.ts +6 -3
  442. package/src/notifications/notification-utils.ts +173 -5
  443. package/src/notifications/signal.ts +12 -0
  444. package/src/oauth/AGENTS.md +4 -2
  445. package/src/oauth/byo-connection.ts +6 -1
  446. package/src/oauth/credential-token-resolver.ts +3 -10
  447. package/src/oauth/manual-token-connection.ts +29 -64
  448. package/src/oauth/manual-token-providers.ts +62 -0
  449. package/src/oauth/provider-serializer.ts +13 -0
  450. package/src/oauth/seed-providers.ts +62 -11
  451. package/src/onboarding/onboarding-events-store.ts +91 -1
  452. package/src/permissions/question-prompter.test.ts +4 -0
  453. package/src/permissions/question-prompter.ts +21 -4
  454. package/src/persistence/__tests__/conversation-lineage.test.ts +248 -0
  455. package/src/persistence/attachments-store.ts +15 -15
  456. package/src/persistence/conversation-attention-store.ts +30 -10
  457. package/src/persistence/conversation-bootstrap.ts +17 -0
  458. package/src/persistence/conversation-crud.ts +363 -97
  459. package/src/persistence/conversation-group-migration.ts +1 -1
  460. package/src/persistence/conversation-key-store.ts +38 -10
  461. package/src/persistence/conversation-lineage.ts +226 -0
  462. package/src/persistence/conversation-plugin-facade.ts +28 -3
  463. package/src/persistence/conversation-queries.ts +207 -33
  464. package/src/persistence/conversation-types.ts +107 -1
  465. package/src/persistence/jobs-store.ts +32 -8
  466. package/src/persistence/llm-usage-store.ts +22 -0
  467. package/src/persistence/migrations/361-normalize-managed-connection-rows.test.ts +168 -0
  468. package/src/persistence/migrations/361-normalize-managed-connection-rows.ts +76 -0
  469. package/src/persistence/migrations/362-add-conversation-subagent-kind.test.ts +187 -0
  470. package/src/persistence/migrations/362-add-conversation-subagent-kind.ts +93 -0
  471. package/src/persistence/migrations/363-backfill-schedule-inference-profile.test.ts +229 -0
  472. package/src/persistence/migrations/363-backfill-schedule-inference-profile.ts +127 -0
  473. package/src/persistence/migrations/364-add-schedule-source-key.test.ts +120 -0
  474. package/src/persistence/migrations/364-add-schedule-source-key.ts +43 -0
  475. package/src/persistence/migrations/365-add-conversation-fork-strategy.ts +27 -0
  476. package/src/persistence/schema/conversations.ts +32 -0
  477. package/src/persistence/schema/infrastructure.ts +3 -0
  478. package/src/persistence/steps.ts +30 -0
  479. package/src/persistence/subagent-store.ts +148 -6
  480. package/src/platform/managed-speech.test.ts +20 -0
  481. package/src/platform/managed-speech.ts +9 -0
  482. package/src/plugin-api/index.ts +8 -0
  483. package/src/plugin-api/webhook-url.ts +151 -0
  484. package/src/plugins/__tests__/installed-plugin-dirs.test.ts +82 -0
  485. package/src/plugins/__tests__/source-fingerprint.test.ts +5 -1
  486. package/src/plugins/collect-source-versions.ts +4 -27
  487. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +5 -5
  488. package/src/plugins/defaults/exploration-drift/package.json +1 -1
  489. package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +4 -2
  490. package/src/plugins/defaults/image-recovery/recover.ts +67 -57
  491. package/src/plugins/defaults/index.ts +1 -1
  492. package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +64 -18
  493. package/src/plugins/defaults/memory/__tests__/jobs-worker-outcome-contract.test.ts +242 -0
  494. package/src/plugins/defaults/memory/__tests__/jobs-worker-retrospective-sweep.test.ts +24 -1
  495. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +48 -3
  496. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +511 -19
  497. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +39 -3
  498. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +493 -0
  499. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +5 -4
  500. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +5 -1
  501. package/src/plugins/defaults/memory/job-handlers.ts +87 -5
  502. package/src/plugins/defaults/memory/jobs-worker.ts +60 -6
  503. package/src/plugins/defaults/memory/memory-retrospective-constants.ts +11 -0
  504. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +29 -0
  505. package/src/plugins/defaults/memory/memory-retrospective-job.ts +342 -60
  506. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +23 -3
  507. package/src/plugins/defaults/memory/substrate/consolidation-lock.ts +9 -4
  508. package/src/plugins/defaults/memory/substrate/sweep-job.ts +7 -0
  509. package/src/plugins/defaults/memory/v1/graph/injection.ts +1 -1
  510. package/src/plugins/defaults/memory/v2/router.ts +8 -0
  511. package/src/plugins/defaults/turn-context/unified-turn-context.ts +1 -2
  512. package/src/plugins/external-plugin-loader.ts +17 -8
  513. package/src/plugins/installed-plugin-dirs.ts +101 -0
  514. package/src/plugins/mtime-cache.ts +34 -22
  515. package/src/plugins/types.ts +69 -8
  516. package/src/prompts/persona-resolver.ts +6 -2
  517. package/src/prompts/system-prompt.ts +72 -17
  518. package/src/providers/__tests__/inference.test.ts +40 -0
  519. package/src/providers/__tests__/preflight-resolved-config.test.ts +58 -1
  520. package/src/providers/__tests__/retry-callsite.test.ts +67 -0
  521. package/src/providers/__tests__/vellum-connection-routing.test.ts +42 -11
  522. package/src/providers/anthropic/client.ts +112 -95
  523. package/src/providers/connection-resolution.ts +35 -1
  524. package/src/providers/gemini/client.ts +5 -5
  525. package/src/providers/inference/adapter-factory.ts +16 -10
  526. package/src/providers/inference/auth.ts +16 -0
  527. package/src/providers/inference/connection-availability.ts +164 -12
  528. package/src/providers/inference/connections.ts +25 -1
  529. package/src/providers/media-resolve.ts +21 -11
  530. package/src/providers/model-catalog.ts +15 -0
  531. package/src/providers/openai/chat-completions-provider.ts +4 -4
  532. package/src/providers/openai/responses-provider.ts +12 -25
  533. package/src/providers/registry.ts +9 -4
  534. package/src/providers/retry.ts +25 -0
  535. package/src/providers/server-tool-pairing.ts +134 -0
  536. package/src/providers/speech-to-text/__tests__/resolve.test.ts +187 -6
  537. package/src/providers/speech-to-text/deepgram.ts +1 -0
  538. package/src/providers/speech-to-text/resolve.ts +80 -10
  539. package/src/providers/speech-to-text/vellum-managed.ts +7 -1
  540. package/src/providers/speech-to-text/vellum-speech-relay-connection.ts +3 -1
  541. package/src/providers/types.ts +26 -6
  542. package/src/providers/vellum-model-routing.ts +6 -7
  543. package/src/runtime/AGENTS.md +2 -0
  544. package/src/runtime/__tests__/agent-wake.test.ts +16 -16
  545. package/src/runtime/__tests__/background-job-runner.test.ts +0 -144
  546. package/src/runtime/__tests__/desktop-presence.test.ts +162 -0
  547. package/src/runtime/agent-wake.ts +63 -24
  548. package/src/runtime/assistant-event-hub.ts +87 -12
  549. package/src/runtime/auth/__tests__/guard-tests.test.ts +2 -0
  550. package/src/runtime/auth/__tests__/scopes.test.ts +20 -0
  551. package/src/runtime/auth/same-actor.ts +56 -8
  552. package/src/runtime/auth/scopes.ts +12 -0
  553. package/src/runtime/auth/types.ts +2 -0
  554. package/src/runtime/background-job-runner.ts +5 -27
  555. package/src/runtime/channel-readiness-service.ts +59 -3
  556. package/src/runtime/channel-readiness-types.ts +18 -0
  557. package/src/runtime/desktop-presence.ts +69 -0
  558. package/src/runtime/http-server.ts +12 -0
  559. package/src/runtime/local-actor-identity.ts +11 -9
  560. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -0
  561. package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
  562. package/src/runtime/routes/__tests__/client-routes.test.ts +186 -14
  563. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +547 -1
  564. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +162 -7
  565. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +14 -10
  566. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +340 -3
  567. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +28 -1
  568. package/src/runtime/routes/__tests__/plugins-routes.test.ts +173 -1
  569. package/src/runtime/routes/__tests__/question-routes.test.ts +64 -0
  570. package/src/runtime/routes/__tests__/retrospective-routes.test.ts +20 -0
  571. package/src/runtime/routes/__tests__/skill-history-route.test.ts +116 -0
  572. package/src/runtime/routes/__tests__/wake-conversation-routes.test.ts +60 -2
  573. package/src/runtime/routes/archive-utils.ts +16 -12
  574. package/src/runtime/routes/attachment-routes.ts +13 -6
  575. package/src/runtime/routes/canned-reply-release.ts +54 -0
  576. package/src/runtime/routes/channel-verification-routes.ts +2 -27
  577. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +5 -0
  578. package/src/runtime/routes/client-routes.ts +114 -2
  579. package/src/runtime/routes/conversation-cli-routes.ts +10 -12
  580. package/src/runtime/routes/conversation-list-routes.ts +59 -10
  581. package/src/runtime/routes/conversation-management-routes.ts +47 -18
  582. package/src/runtime/routes/conversation-query-routes.ts +32 -53
  583. package/src/runtime/routes/conversation-routes.ts +360 -293
  584. package/src/runtime/routes/credential-prompt-routes.ts +23 -2
  585. package/src/runtime/routes/debug-routes.ts +4 -1
  586. package/src/runtime/routes/default-provider-routes.ts +7 -11
  587. package/src/runtime/routes/documents-routes.ts +44 -1
  588. package/src/runtime/routes/events-routes.ts +28 -1
  589. package/src/runtime/routes/host-app-control-routes.ts +2 -0
  590. package/src/runtime/routes/host-bash-routes.ts +2 -0
  591. package/src/runtime/routes/host-browser-routes.ts +2 -0
  592. package/src/runtime/routes/host-cu-routes.ts +2 -0
  593. package/src/runtime/routes/host-file-routes.ts +2 -0
  594. package/src/runtime/routes/host-transfer-routes.ts +4 -0
  595. package/src/runtime/routes/identity-routes.ts +6 -6
  596. package/src/runtime/routes/inbound-message-handler.ts +5 -5
  597. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +7 -3
  598. package/src/runtime/routes/inbound-stages/background-dispatch.ts +14 -0
  599. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -2
  600. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +14 -11
  601. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -17
  602. package/src/runtime/routes/inference-profile-availability-guard.ts +171 -0
  603. package/src/runtime/routes/inference-profile-session-handler.ts +26 -2
  604. package/src/runtime/routes/inference-profile-session-routes.ts +2 -1
  605. package/src/runtime/routes/inference-profiles-routes.ts +152 -73
  606. package/src/runtime/routes/inference-provider-connection-routes.ts +0 -16
  607. package/src/runtime/routes/llm-call-sites-routes.ts +13 -0
  608. package/src/runtime/routes/log-export-routes.ts +1 -1
  609. package/src/runtime/routes/notification-routes.ts +0 -14
  610. package/src/runtime/routes/oauth-providers.ts +1 -0
  611. package/src/runtime/routes/plugins-routes.ts +106 -1
  612. package/src/runtime/routes/profiler-routes.ts +4 -2
  613. package/src/runtime/routes/question-routes.ts +28 -10
  614. package/src/runtime/routes/retrospective-routes.ts +9 -3
  615. package/src/runtime/routes/schedule-routes.ts +210 -12
  616. package/src/runtime/routes/secret-routes.ts +40 -23
  617. package/src/runtime/routes/settings-routes.ts +41 -13
  618. package/src/runtime/routes/skills-routes.ts +79 -0
  619. package/src/runtime/routes/ui-snapshot-routes.ts +1 -0
  620. package/src/runtime/routes/wake-conversation-routes.ts +23 -0
  621. package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
  622. package/src/runtime/services/conversation-serializer.ts +24 -9
  623. package/src/runtime/sync/documents-sidecar-publish.test.ts +101 -0
  624. package/src/runtime/sync/resource-sync-events.ts +79 -1
  625. package/src/runtime/sync/worker-daemon-notify.test.ts +34 -0
  626. package/src/runtime/sync/worker-daemon-notify.ts +44 -6
  627. package/src/runtime/verification-outbound-actions.ts +250 -222
  628. package/src/runtime/verification-templates.ts +41 -30
  629. package/src/schedule/__tests__/plugin-schedule-declarations.test.ts +614 -0
  630. package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +1066 -0
  631. package/src/schedule/inference-profile.ts +64 -3
  632. package/src/schedule/plugin-schedule-declarations.ts +518 -0
  633. package/src/schedule/plugin-schedule-reconciler.ts +605 -0
  634. package/src/schedule/plugin-schedules-gate.ts +23 -0
  635. package/src/schedule/schedule-store.ts +520 -26
  636. package/src/schedule/scheduler.ts +38 -0
  637. package/src/security/credential-backend.ts +51 -4
  638. package/src/security/encrypted-store.ts +64 -42
  639. package/src/security/secure-keys.ts +55 -7
  640. package/src/skills/catalog-install.ts +20 -7
  641. package/src/skills/inline-command-runner.ts +2 -7
  642. package/src/skills/skill-history.test.ts +269 -0
  643. package/src/skills/skill-history.ts +250 -0
  644. package/src/skills/skillssh-registry.ts +1 -1
  645. package/src/stt/daemon-batch-transcriber.ts +8 -6
  646. package/src/subagent/__tests__/consult-prompt.test.ts +25 -0
  647. package/src/subagent/consult-prompt.ts +3 -1
  648. package/src/subagent/consult-transcript.ts +1 -1
  649. package/src/subagent/manager.ts +304 -44
  650. package/src/subagent/progress-events.ts +35 -0
  651. package/src/subagent/role-resolution.ts +88 -0
  652. package/src/subagent/types.ts +302 -67
  653. package/src/subagent/validate-allowlists.ts +3 -4
  654. package/src/telegram/__tests__/webhook-health.test.ts +73 -1
  655. package/src/telegram/webhook-health.ts +101 -3
  656. package/src/telemetry/__tests__/telemetry-event-fixtures.ts +2 -0
  657. package/src/telemetry/activation-funnel.ts +6 -1
  658. package/src/telemetry/live-voice-funnel.ts +75 -0
  659. package/src/telemetry/telemetry-event-sources.ts +6 -0
  660. package/src/telemetry/telemetry-wire-source.json +1 -1
  661. package/src/telemetry/telemetry-wire.generated.ts +2 -0
  662. package/src/telemetry/types.ts +23 -4
  663. package/src/telemetry/usage-telemetry-reporter.test.ts +4 -0
  664. package/src/tools/__tests__/subagent-workflow-tool-input-schemas.test.ts +1 -1
  665. package/src/tools/ask-question/ask-question-tool.test.ts +118 -3
  666. package/src/tools/ask-question/ask-question-tool.ts +56 -2
  667. package/src/tools/browser/browser-execution.ts +1 -1
  668. package/src/tools/calls/call-start.ts +8 -5
  669. package/src/tools/conversation-groups/move_to_group.ts +10 -12
  670. package/src/tools/document/document-tool.ts +11 -4
  671. package/src/tools/execution-timeout.ts +9 -2
  672. package/src/tools/executor.ts +7 -6
  673. package/src/tools/filesystem/edit.ts +1 -1
  674. package/src/tools/filesystem/list.ts +1 -1
  675. package/src/tools/filesystem/read.ts +7 -13
  676. package/src/tools/filesystem/search.ts +23 -21
  677. package/src/tools/filesystem/write.ts +4 -1
  678. package/src/tools/host-filesystem/edit.ts +7 -2
  679. package/src/tools/host-filesystem/read.ts +21 -8
  680. package/src/tools/host-filesystem/write.ts +10 -2
  681. package/src/tools/host-terminal/host-shell.ts +2 -2
  682. package/src/tools/mcp/mcp-tool-factory.ts +25 -1
  683. package/src/tools/permission-checker.ts +8 -3
  684. package/src/tools/schedule/delete.ts +13 -1
  685. package/src/tools/schedule/list.ts +14 -3
  686. package/src/tools/schedule/update.ts +56 -33
  687. package/src/tools/shared/filesystem/audio-read.ts +6 -4
  688. package/src/tools/shared/filesystem/file-ops-service.ts +176 -116
  689. package/src/tools/shared/filesystem/image-read.ts +12 -12
  690. package/src/tools/shared/filesystem/path-policy.ts +70 -5
  691. package/src/tools/shared/filesystem/size-guard.ts +6 -6
  692. package/src/tools/shared/input-misuse.ts +72 -0
  693. package/src/tools/skills/scaffold-managed.ts +82 -0
  694. package/src/tools/skills/skill-tool-factory.ts +14 -1
  695. package/src/tools/subagent/consult-deadline.ts +12 -7
  696. package/src/tools/subagent/message.ts +3 -1
  697. package/src/tools/subagent/read.ts +66 -4
  698. package/src/tools/subagent/spawn.ts +500 -78
  699. package/src/tools/tool-approval-handler.ts +45 -4
  700. package/src/tools/types.ts +22 -3
  701. package/src/tools/ui-surface/surface-shape-docs.ts +1 -1
  702. package/src/tools/ui-surface/visual-validation.ts +2 -0
  703. package/src/usage/__tests__/subagent-attribution.test.ts +109 -0
  704. package/src/usage/subagent-attribution.ts +108 -0
  705. package/src/util/__tests__/sqlite-retry.test.ts +112 -0
  706. package/src/util/abort-reasons.ts +16 -0
  707. package/src/util/ansi.ts +44 -0
  708. package/src/util/disk-usage.ts +17 -13
  709. package/src/util/image-conversion.ts +20 -13
  710. package/src/util/schedule-source-key.ts +23 -0
  711. package/src/util/worker-process.ts +7 -1
  712. package/src/workspace/git-service.ts +33 -9
  713. package/src/workspace/migrations/140-repair-seed-pinned-memory-v3-live.ts +260 -0
  714. package/src/workspace/migrations/141-stt-english-default-to-multilingual.ts +111 -0
  715. package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +126 -0
  716. package/src/workspace/migrations/registry.ts +4 -0
  717. package/src/workspace/top-level-renderer.ts +15 -2
  718. package/src/__tests__/tool-side-effects-slack-dm.test.ts +0 -357
  719. package/src/daemon/install-symlink.ts +0 -207
  720. package/src/notifications/deferred-emit.ts +0 -147
@@ -36,11 +36,23 @@ const whatsapp = {
36
36
  const a2a = {
37
37
  deliverA2AReply: mock((..._args: unknown[]) => Promise.resolve({ ok: true })),
38
38
  };
39
+ const discord = {
40
+ sendDiscordReply: mock((..._args: unknown[]) =>
41
+ Promise.resolve({ lastMessageId: "discord-id" }),
42
+ ),
43
+ sendDiscordTypingIndicator: mock((..._args: unknown[]) =>
44
+ Promise.resolve(true),
45
+ ),
46
+ sendDiscordAttachments: mock((..._args: unknown[]) =>
47
+ Promise.resolve({ allFailed: false, failureCount: 0, totalCount: 0 }),
48
+ ),
49
+ };
39
50
 
40
51
  mock.module("../slack/send.js", () => slack);
41
52
  mock.module("../telegram-bot/send.js", () => telegram);
42
53
  mock.module("../whatsapp/send.js", () => whatsapp);
43
54
  mock.module("../a2a/deliver.js", () => a2a);
55
+ mock.module("../discord/send.js", () => discord);
44
56
  mock.module("../../../util/logger.js", () => ({
45
57
  getLogger: () => ({ debug() {}, info() {}, warn() {}, error() {} }),
46
58
  }));
@@ -57,7 +69,7 @@ function payload(
57
69
  }
58
70
 
59
71
  beforeEach(() => {
60
- for (const group of [slack, telegram, whatsapp, a2a]) {
72
+ for (const group of [slack, telegram, whatsapp, a2a, discord]) {
61
73
  for (const spy of Object.values(group)) {
62
74
  spy.mockClear();
63
75
  }
@@ -78,19 +90,25 @@ describe("routing", () => {
78
90
  expect(
79
91
  getTransportForCallback(`${BASE}/deliver/a2a?taskId=t1`)?.channel,
80
92
  ).toBe("a2a");
93
+ expect(getTransportForCallback(`${BASE}/deliver/discord`)?.channel).toBe(
94
+ "discord",
95
+ );
81
96
  });
82
97
 
83
98
  test("isDirectDelivery is true for owned paths, false otherwise", () => {
84
99
  expect(isDirectDelivery(`${BASE}/deliver/slack`)).toBe(true);
85
100
  expect(isDirectDelivery(`${BASE}/deliver/a2a?taskId=t1`)).toBe(true);
86
- expect(isDirectDelivery(`${BASE}/deliver/discord`)).toBe(false);
101
+ expect(isDirectDelivery(`${BASE}/deliver/discord`)).toBe(true);
87
102
  expect(isDirectDelivery(`${BASE}/v1/messages`)).toBe(false);
88
103
  expect(
89
104
  isDirectDelivery(
90
105
  `${BASE}/v1/internal/managed-gateway/outbound-send/?route_id=r1`,
91
106
  ),
92
107
  ).toBe(false);
93
- expect(getTransportForCallback(`${BASE}/deliver/discord`)).toBeUndefined();
108
+ // `phone` is a canonical channel with no direct-delivery transport, so it
109
+ // stands in for the never-registered case discord used to cover.
110
+ expect(isDirectDelivery(`${BASE}/deliver/phone`)).toBe(false);
111
+ expect(getTransportForCallback(`${BASE}/deliver/phone`)).toBeUndefined();
94
112
  });
95
113
  });
96
114
 
@@ -203,12 +221,56 @@ describe("capability gating across channels", () => {
203
221
  expect(a2a.deliverA2AReply).toHaveBeenCalledTimes(1);
204
222
  expect(a2a.deliverA2AReply.mock.calls[0][0]).toBe(url);
205
223
  });
224
+
225
+ test("Discord text routes to sendDiscordReply, targeting the channel", async () => {
226
+ await deliverDirect(`${BASE}/deliver/discord`, payload({ text: "hi" }));
227
+ expect(discord.sendDiscordReply).toHaveBeenCalledTimes(1);
228
+ expect(discord.sendDiscordReply.mock.calls[0][0]).toEqual({
229
+ channelId: "C1",
230
+ });
231
+ expect(discord.sendDiscordReply.mock.calls[0][1]).toBe("hi");
232
+ });
233
+
234
+ test("Discord threads a reply to the thread id, not the parent channel", async () => {
235
+ await deliverDirect(
236
+ `${BASE}/deliver/discord?threadId=T9`,
237
+ payload({ text: "hi" }),
238
+ );
239
+ expect(discord.sendDiscordReply).toHaveBeenCalledTimes(1);
240
+ // A Discord thread is itself a channel: the reply must post to the thread
241
+ // snowflake, never to the parent channel the payload's chatId carries.
242
+ expect(discord.sendDiscordReply.mock.calls[0][0]).toEqual({
243
+ channelId: "T9",
244
+ });
245
+ });
246
+
247
+ test("a typing payload to Discord falls through to deliver (no sendTyping)", async () => {
248
+ // Only the Telegram heartbeat produces `chatAction: "typing"`, and it is
249
+ // gated on sourceChannel === "telegram", so Discord can never receive one.
250
+ // The transport implements no sendTyping, so this falls through.
251
+ await deliverDirect(
252
+ `${BASE}/deliver/discord`,
253
+ payload({ chatAction: "typing", text: "hi" }),
254
+ );
255
+ expect(discord.sendDiscordReply).toHaveBeenCalledTimes(1);
256
+ });
257
+
258
+ test("a Slack-only stream payload to Discord falls through to deliver", async () => {
259
+ await deliverDirect(
260
+ `${BASE}/deliver/discord`,
261
+ payload({
262
+ text: "hi",
263
+ slackStream: { action: "start", threadTs: "1700.5" },
264
+ }),
265
+ );
266
+ expect(discord.sendDiscordReply).toHaveBeenCalledTimes(1);
267
+ });
206
268
  });
207
269
 
208
270
  describe("unsupported callback", () => {
209
271
  test("throws when no transport owns the callback", async () => {
210
272
  await expect(
211
- deliverDirect(`${BASE}/deliver/discord`, payload({ text: "hi" })),
273
+ deliverDirect(`${BASE}/deliver/phone`, payload({ text: "hi" })),
212
274
  ).rejects.toThrow(/unsupported callback/);
213
275
  });
214
276
  });
@@ -15,6 +15,7 @@ const DIRECT_DELIVERY_CHANNELS = [
15
15
  "telegram",
16
16
  "whatsapp",
17
17
  "a2a",
18
+ "discord",
18
19
  ] as const satisfies readonly ChannelId[];
19
20
 
20
21
  export type DirectDeliveryChannel = (typeof DIRECT_DELIVERY_CHANNELS)[number];
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Discord REST client for direct outbound messaging.
3
+ *
4
+ * Calls the Discord HTTP API directly using the `discord_channel:bot_token`
5
+ * credential, the same token the gateway's Discord Gateway client identifies
6
+ * with, read from the secure store rather than passed across the process
7
+ * boundary. Retry and error classification mirror the sibling Telegram client
8
+ * so delivery behaviour is consistent across channels.
9
+ *
10
+ * Wire shapes verified against the official docs:
11
+ * https://docs.discord.com/developers/resources/message
12
+ * https://docs.discord.com/developers/topics/rate-limits
13
+ * https://docs.discord.com/developers/reference
14
+ */
15
+
16
+ import { credentialKey } from "../../../security/credential-key.js";
17
+ import { getSecureKeyResultAsync } from "../../../security/secure-keys.js";
18
+ import { BackendUnavailableError, ConfigError } from "../../../util/errors.js";
19
+ import { getLogger } from "../../../util/logger.js";
20
+ import { retryableCall } from "../retry-policy.js";
21
+
22
+ const log = getLogger("discord-api");
23
+
24
+ const DISCORD_API_BASE_URL = "https://discord.com/api/v10";
25
+
26
+ /**
27
+ * Discord requires HTTP API clients to identify themselves with a User-Agent
28
+ * of this documented form (`https://docs.discord.com/developers/reference`).
29
+ */
30
+ const DISCORD_USER_AGENT =
31
+ "DiscordBot (https://github.com/vellum-ai/vellum-assistant, 1.0)";
32
+
33
+ const DISCORD_DEFAULT_MAX_RETRIES = 3;
34
+ const DISCORD_DEFAULT_INITIAL_BACKOFF_MS = 1000;
35
+ const DISCORD_DEFAULT_TIMEOUT_MS = 15_000;
36
+
37
+ /**
38
+ * Cap on a server-advertised `retry_after`. Discord can hand back a multi-hour
39
+ * wait when a bot is globally limited; sleeping that long inside a delivery
40
+ * would hold the per-conversation single-flight open indefinitely. Past this
41
+ * bound the call fails and the channel retry sweep owns the redelivery.
42
+ */
43
+ const DISCORD_MAX_RETRY_AFTER_MS = 60_000;
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Error types
47
+ // ---------------------------------------------------------------------------
48
+
49
+ /**
50
+ * A Discord REST call that returned a non-retryable client error: a 4xx that
51
+ * is not a 429. The request itself is the problem (the channel, the
52
+ * permissions, or the payload), so retrying the same call is pointless.
53
+ *
54
+ * Callers branch on this type to degrade gracefully, e.g. an attachment that
55
+ * Discord refuses is reported to the channel instead of retried. Branch on the
56
+ * type rather than on Discord's numeric error code: the docs treat that code
57
+ * list as open, and new codes are added without notice.
58
+ */
59
+ export class DiscordNonRetryableError extends Error {
60
+ readonly status: number;
61
+
62
+ constructor(status: number, message: string) {
63
+ super(message);
64
+ this.name = "DiscordNonRetryableError";
65
+ this.status = status;
66
+ }
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Internal helpers
71
+ // ---------------------------------------------------------------------------
72
+
73
+ interface DiscordErrorBody {
74
+ message?: string;
75
+ retry_after?: number;
76
+ global?: boolean;
77
+ }
78
+
79
+ function parseErrorBody(body: string): DiscordErrorBody {
80
+ try {
81
+ return JSON.parse(body) as DiscordErrorBody;
82
+ } catch {
83
+ return {};
84
+ }
85
+ }
86
+
87
+ /**
88
+ * The wait Discord asked for, as a `Retry-After` seconds value.
89
+ *
90
+ * Discord carries it in the 429 JSON body as well as the header, in seconds
91
+ * (fractional) either way, and the body is the more precise of the two.
92
+ */
93
+ function retryAfterFrom(_response: Response, body: string): string | null {
94
+ const parsed = parseErrorBody(body);
95
+ return typeof parsed.retry_after === "number" && parsed.retry_after > 0
96
+ ? String(parsed.retry_after)
97
+ : null;
98
+ }
99
+
100
+ /**
101
+ * Run a Discord REST call with retries.
102
+ *
103
+ * `T | undefined` because an endpoint may answer 204 with no body; callers
104
+ * that need a response shape assert on it themselves.
105
+ */
106
+ function discordCall<T>(
107
+ route: string,
108
+ doFetch: () => Promise<Response>,
109
+ ): Promise<T | undefined> {
110
+ return retryableCall<T | undefined>({
111
+ provider: "Discord",
112
+ operation: route,
113
+ maxRetries: DISCORD_DEFAULT_MAX_RETRIES,
114
+ initialBackoffMs: DISCORD_DEFAULT_INITIAL_BACKOFF_MS,
115
+ maxDelayMs: DISCORD_MAX_RETRY_AFTER_MS,
116
+ log,
117
+ doFetch,
118
+ detailFrom: (body) => parseErrorBody(body).message,
119
+ retryAfterFrom,
120
+ nonRetryableError: ({ status, message }) =>
121
+ new DiscordNonRetryableError(status, message),
122
+ decode: (body) => {
123
+ if (!body) {
124
+ return undefined;
125
+ }
126
+ try {
127
+ return JSON.parse(body) as T;
128
+ } catch {
129
+ throw new Error(
130
+ `Discord ${route} returned an unparseable response body`,
131
+ );
132
+ }
133
+ },
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Run a Discord REST call with retries.
139
+ *
140
+ * Retryable: 429 (rate limited) and 5xx, plus transport failures. Everything
141
+ * else surfaces as {@link DiscordNonRetryableError}.
142
+ *
143
+ * `T | undefined` because an endpoint may answer 204 with no body; callers
144
+ * that need a response shape assert on it themselves.
145
+ */
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // Credential resolution
149
+ // ---------------------------------------------------------------------------
150
+
151
+ async function resolveBotToken(): Promise<string> {
152
+ const result = await getSecureKeyResultAsync(
153
+ credentialKey("discord_channel", "bot_token"),
154
+ );
155
+ if (result.value) {
156
+ return result.value;
157
+ }
158
+ // Distinguish a transient credential-store outage from a token that was
159
+ // genuinely never set. Both surface as an absent value, but only the latter
160
+ // is an operator misconfiguration; the former is retryable infrastructure
161
+ // unavailability that callers should not treat as "not configured".
162
+ if (result.unreachable) {
163
+ throw new BackendUnavailableError("Discord credential store unreachable");
164
+ }
165
+ throw new ConfigError("Discord bot token not configured");
166
+ }
167
+
168
+ // ---------------------------------------------------------------------------
169
+ // Public API
170
+ // ---------------------------------------------------------------------------
171
+
172
+ /** The subset of Discord's message object that delivery reads back. */
173
+ export interface DiscordMessage {
174
+ id: string;
175
+ }
176
+
177
+ /** The subset of Discord's channel object that DM resolution reads back. */
178
+ interface DiscordChannel {
179
+ id: string;
180
+ }
181
+
182
+ /**
183
+ * Resolved DM channels, keyed by recipient user snowflake.
184
+ *
185
+ * The mapping is stable, so this never goes stale: Discord's create-DM route
186
+ * returns the existing channel when one exists rather than opening a second
187
+ * (https://docs.discord.com/developers/resources/user). The cache is here
188
+ * because that same page warns that opening DMs in volume gets a bot rate
189
+ * limited or blocked from opening new ones, and a per-recipient DM lane would
190
+ * otherwise re-open on every notice.
191
+ */
192
+ const dmChannelIds = new Map<string, string>();
193
+
194
+ /**
195
+ * Bound on the cache. Entries are one small string pair each, and the eviction
196
+ * below is insertion-order rather than least-recently-used: the entries this
197
+ * cache exists to protect (the guardian, a handful of contacts) are the ones
198
+ * created first, and evicting one only costs a single extra API call.
199
+ */
200
+ const MAX_CACHED_DM_CHANNELS = 512;
201
+
202
+ /**
203
+ * Opens already in progress, so concurrent callers for the same recipient
204
+ * share one request rather than racing to make the same one. A verification
205
+ * send and a guardian expiry notice can address the same person in the same
206
+ * tick, and the cache alone does not help there: neither has populated it yet.
207
+ */
208
+ const inFlightDmOpens = new Map<string, Promise<string>>();
209
+
210
+ /**
211
+ * Open the DM channel with a Discord user and return its snowflake.
212
+ *
213
+ * Discord has no "look up my DM with this person" route: a bot names a
214
+ * recipient and is handed the channel, creating it the first time and
215
+ * receiving the same one after that. So reaching one person privately is
216
+ * always this call followed by an ordinary channel send, and the DM channel is
217
+ * a resolved address rather than something the caller can be told up front.
218
+ */
219
+ export function openDiscordDmChannel(recipientUserId: string): Promise<string> {
220
+ const cached = dmChannelIds.get(recipientUserId);
221
+ if (cached) {
222
+ return Promise.resolve(cached);
223
+ }
224
+
225
+ const existing = inFlightDmOpens.get(recipientUserId);
226
+ if (existing) {
227
+ return existing;
228
+ }
229
+
230
+ const open = (async (): Promise<string> => {
231
+ const channel = await callDiscordApi<DiscordChannel>(
232
+ "POST",
233
+ "/users/@me/channels",
234
+ { recipient_id: recipientUserId },
235
+ );
236
+ if (typeof channel?.id !== "string" || channel.id.length === 0) {
237
+ throw new Error(
238
+ "Discord create-DM returned no channel id for the recipient",
239
+ );
240
+ }
241
+
242
+ if (dmChannelIds.size >= MAX_CACHED_DM_CHANNELS) {
243
+ const oldest = dmChannelIds.keys().next();
244
+ if (!oldest.done) {
245
+ dmChannelIds.delete(oldest.value);
246
+ }
247
+ }
248
+ dmChannelIds.set(recipientUserId, channel.id);
249
+ log.debug({ recipientUserId }, "Opened Discord DM channel");
250
+ return channel.id;
251
+ })().finally(() => {
252
+ // Cleared on failure too, so a transient error does not pin every later
253
+ // caller to the same rejected promise.
254
+ inFlightDmOpens.delete(recipientUserId);
255
+ });
256
+
257
+ inFlightDmOpens.set(recipientUserId, open);
258
+ return open;
259
+ }
260
+
261
+ /** Drop the resolved and in-flight DM channel state. Test-only seam. */
262
+ export function resetDiscordDmChannelCache(): void {
263
+ dmChannelIds.clear();
264
+ inFlightDmOpens.clear();
265
+ }
266
+
267
+ /** Call a Discord REST route with a JSON body. */
268
+ export async function callDiscordApi<T>(
269
+ method: "POST" | "PATCH",
270
+ route: string,
271
+ body: Record<string, unknown>,
272
+ ): Promise<T | undefined> {
273
+ const botToken = await resolveBotToken();
274
+ return discordCall<T>(route, () =>
275
+ fetch(`${DISCORD_API_BASE_URL}${route}`, {
276
+ method,
277
+ headers: {
278
+ Authorization: `Bot ${botToken}`,
279
+ "Content-Type": "application/json",
280
+ "User-Agent": DISCORD_USER_AGENT,
281
+ },
282
+ body: JSON.stringify(body),
283
+ signal: AbortSignal.timeout(DISCORD_DEFAULT_TIMEOUT_MS),
284
+ }),
285
+ );
286
+ }
287
+
288
+ /**
289
+ * Call a Discord REST route with a `multipart/form-data` body.
290
+ *
291
+ * Discord's documented upload shape: files ride as `files[n]` parts and the
292
+ * JSON body moves to a `payload_json` part.
293
+ */
294
+ export async function callDiscordApiMultipart<T>(
295
+ route: string,
296
+ form: FormData,
297
+ ): Promise<T | undefined> {
298
+ const botToken = await resolveBotToken();
299
+ return discordCall<T>(route, () =>
300
+ fetch(`${DISCORD_API_BASE_URL}${route}`, {
301
+ method: "POST",
302
+ headers: {
303
+ Authorization: `Bot ${botToken}`,
304
+ // Content-Type is deliberately unset so fetch derives the multipart
305
+ // boundary from the FormData body.
306
+ "User-Agent": DISCORD_USER_AGENT,
307
+ },
308
+ body: form,
309
+ signal: AbortSignal.timeout(DISCORD_DEFAULT_TIMEOUT_MS),
310
+ }),
311
+ );
312
+ }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * The Discord private-delivery lane, asserted against a stubbed fetch.
3
+ *
4
+ * Discord has no route that looks a DM up by recipient, so reaching one person
5
+ * is always "open the channel, then post to it". Two things about that are
6
+ * silent when wrong and are pinned here: the open call must use the documented
7
+ * create-DM route, and a delivery marked `dm` must post to the channel that
8
+ * call returned rather than to the id it was handed. The second is the one
9
+ * that matters: the id handed in is a *user* snowflake, and a Discord user id
10
+ * and channel id are both bare digits, so posting it unresolved does not fail
11
+ * loudly. It either 404s or, worse, addresses whatever channel happens to
12
+ * carry that id.
13
+ */
14
+
15
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
16
+
17
+ const BOT_TOKEN = "discord-bot-token";
18
+
19
+ const realSecureKeys = await import("../../../security/secure-keys.js");
20
+ mock.module("../../../security/secure-keys.js", () => ({
21
+ ...realSecureKeys,
22
+ getSecureKeyResultAsync: async () => ({
23
+ value: BOT_TOKEN,
24
+ unreachable: false,
25
+ }),
26
+ }));
27
+
28
+ // Pulled in by `send.js`; stubbed so importing the transport does not reach
29
+ // the attachment store. No test here sends an attachment.
30
+ mock.module("../../../persistence/attachments-store.js", () => ({
31
+ getAttachmentContent: () => null,
32
+ }));
33
+
34
+ mock.module("../../../util/logger.js", () => ({
35
+ getLogger: () => ({ debug() {}, info() {}, warn() {}, error() {} }),
36
+ }));
37
+
38
+ const { openDiscordDmChannel, resetDiscordDmChannelCache } =
39
+ await import("./api.js");
40
+ const { discordTransport } = await import("./transport.js");
41
+
42
+ const originalFetch = globalThis.fetch;
43
+
44
+ interface Captured {
45
+ url: string;
46
+ method: string;
47
+ body: unknown;
48
+ }
49
+
50
+ let calls: Captured[] = [];
51
+
52
+ /** The user snowflake a notice is addressed to. */
53
+ const RECIPIENT = "900000000000000042";
54
+ /** The DM channel Discord hands back for that recipient. */
55
+ const DM_CHANNEL = "800000000000000099";
56
+ /** The public guild channel the requester was seen in. */
57
+ const GUILD_CHANNEL = "700000000000000001";
58
+
59
+ /**
60
+ * Answer the create-DM route with a channel and every other route with a
61
+ * message. Keyed on the route so a mis-routed call is visible as a wrong
62
+ * payload rather than quietly succeeding.
63
+ */
64
+ function stubFetch(): void {
65
+ globalThis.fetch = (async (url: string | URL, init?: RequestInit) => {
66
+ const href = String(url);
67
+ calls.push({
68
+ url: href,
69
+ method: init?.method ?? "GET",
70
+ body: init?.body,
71
+ });
72
+ const payload = href.endsWith("/users/@me/channels")
73
+ ? { id: DM_CHANNEL }
74
+ : { id: "msg-1" };
75
+ return new Response(JSON.stringify(payload), {
76
+ status: 200,
77
+ headers: { "content-type": "application/json" },
78
+ });
79
+ }) as typeof fetch;
80
+ }
81
+
82
+ beforeEach(() => {
83
+ calls = [];
84
+ resetDiscordDmChannelCache();
85
+ stubFetch();
86
+ });
87
+
88
+ afterEach(() => {
89
+ globalThis.fetch = originalFetch;
90
+ });
91
+
92
+ describe("openDiscordDmChannel", () => {
93
+ test("POSTs the documented create-DM route with recipient_id", async () => {
94
+ const channelId = await openDiscordDmChannel(RECIPIENT);
95
+
96
+ expect(channelId).toBe(DM_CHANNEL);
97
+ expect(calls).toHaveLength(1);
98
+ expect(calls[0].url).toBe("https://discord.com/api/v10/users/@me/channels");
99
+ expect(calls[0].method).toBe("POST");
100
+ expect(JSON.parse(String(calls[0].body))).toEqual({
101
+ recipient_id: RECIPIENT,
102
+ });
103
+ });
104
+
105
+ test("resolves a repeat recipient without a second API call", async () => {
106
+ // Discord returns the existing channel rather than opening a second, so
107
+ // this is not about correctness. It is about the documented rate limit:
108
+ // a bot that opens DMs too quickly gets blocked from opening new ones,
109
+ // and a per-notice lane would re-open on every delivery.
110
+ await openDiscordDmChannel(RECIPIENT);
111
+ await openDiscordDmChannel(RECIPIENT);
112
+ await openDiscordDmChannel(RECIPIENT);
113
+
114
+ expect(calls).toHaveLength(1);
115
+ });
116
+
117
+ test("concurrent opens for one recipient share a single request", async () => {
118
+ // The cache cannot help here: nothing has populated it yet. A verification
119
+ // send and a guardian notice can address the same person in one tick, and
120
+ // Discord blocks a bot that opens DMs too quickly.
121
+ const [a, b, c] = await Promise.all([
122
+ openDiscordDmChannel(RECIPIENT),
123
+ openDiscordDmChannel(RECIPIENT),
124
+ openDiscordDmChannel(RECIPIENT),
125
+ ]);
126
+
127
+ expect(calls).toHaveLength(1);
128
+ expect([a, b, c]).toEqual([DM_CHANNEL, DM_CHANNEL, DM_CHANNEL]);
129
+ });
130
+
131
+ test("a channel-less answer throws, and is not pinned for later callers", async () => {
132
+ globalThis.fetch = (async () =>
133
+ new Response(JSON.stringify({}), {
134
+ status: 200,
135
+ headers: { "content-type": "application/json" },
136
+ })) as unknown as typeof fetch;
137
+ await expect(openDiscordDmChannel(RECIPIENT)).rejects.toThrow(
138
+ /no channel id/i,
139
+ );
140
+
141
+ // A transient failure must not leave every later caller sharing the
142
+ // rejected promise.
143
+ stubFetch();
144
+ await expect(openDiscordDmChannel(RECIPIENT)).resolves.toBe(DM_CHANNEL);
145
+ });
146
+
147
+ test("opens a separate channel per recipient", async () => {
148
+ await openDiscordDmChannel(RECIPIENT);
149
+ await openDiscordDmChannel("900000000000000043");
150
+
151
+ expect(calls).toHaveLength(2);
152
+ expect(JSON.parse(String(calls[1].body))).toEqual({
153
+ recipient_id: "900000000000000043",
154
+ });
155
+ });
156
+
157
+ });
158
+
159
+ describe("discordTransport with dm=1", () => {
160
+ test("delivers to the opened DM channel, never to the id it was handed", async () => {
161
+ await discordTransport.deliver(
162
+ { callbackUrl: "/deliver/discord?dm=1", params: { dm: "1" } },
163
+ { chatId: RECIPIENT, text: "Your access request was declined." },
164
+ );
165
+
166
+ const messageCalls = calls.filter((c) => c.url.includes("/messages"));
167
+ expect(messageCalls).toHaveLength(1);
168
+ expect(messageCalls[0].url).toBe(
169
+ `https://discord.com/api/v10/channels/${DM_CHANNEL}/messages`,
170
+ );
171
+ // The recipient's own snowflake must never appear as a channel in the
172
+ // route: that is the unresolved-user-id delivery this lane exists to stop.
173
+ expect(messageCalls[0].url).not.toContain(RECIPIENT);
174
+ });
175
+
176
+ test("a dm delivery ignores threadId, which addresses a guild thread", async () => {
177
+ await discordTransport.deliver(
178
+ {
179
+ callbackUrl: "/deliver/discord?dm=1",
180
+ params: { dm: "1", threadId: "700000000000000009" },
181
+ },
182
+ { chatId: RECIPIENT, text: "expired" },
183
+ );
184
+
185
+ const messageCalls = calls.filter((c) => c.url.includes("/messages"));
186
+ expect(messageCalls[0].url).toContain(DM_CHANNEL);
187
+ expect(messageCalls[0].url).not.toContain("700000000000000009");
188
+ });
189
+
190
+ test("without dm, chatId is still a channel and no DM is opened", async () => {
191
+ // The ordinary in-channel reply must not start routing through a DM.
192
+ await discordTransport.deliver(
193
+ { callbackUrl: "/deliver/discord", params: {} },
194
+ { chatId: GUILD_CHANNEL, text: "hello" },
195
+ );
196
+
197
+ expect(calls.some((c) => c.url.endsWith("/users/@me/channels"))).toBe(
198
+ false,
199
+ );
200
+ expect(calls[0].url).toBe(
201
+ `https://discord.com/api/v10/channels/${GUILD_CHANNEL}/messages`,
202
+ );
203
+ });
204
+ });