@vellumai/assistant 0.11.2 → 0.11.3-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (709) 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-queue.test.ts +265 -1
  72. package/src/__tests__/conversation-retry-route.test.ts +3 -1
  73. package/src/__tests__/conversation-store-ephemeral.test.ts +4 -0
  74. package/src/__tests__/conversation-store.test.ts +26 -10
  75. package/src/__tests__/conversation-summarize-route.test.ts +1 -1
  76. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +15 -6
  77. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -5
  78. package/src/__tests__/conversation-surfaces-app-control.test.ts +12 -11
  79. package/src/__tests__/conversation-surfaces-app-open.test.ts +11 -5
  80. package/src/__tests__/conversation-surfaces-data-persist.test.ts +5 -4
  81. package/src/__tests__/conversation-surfaces-history-restored-completion.test.ts +967 -0
  82. package/src/__tests__/conversation-surfaces-persisted-info.test.ts +312 -0
  83. package/src/__tests__/conversation-surfaces-queued-emit.test.ts +6 -5
  84. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +12 -8
  85. package/src/__tests__/conversation-surfaces-standalone.test.ts +21 -11
  86. package/src/__tests__/conversation-surfaces-state-update.test.ts +7 -11
  87. package/src/__tests__/conversation-surfaces-table-action.test.ts +7 -6
  88. package/src/__tests__/conversation-surfaces-task-progress.test.ts +11 -6
  89. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +9 -6
  90. package/src/__tests__/conversation-tool-setup-attribution.test.ts +70 -7
  91. package/src/__tests__/conversation-wait-for-idle.test.ts +6 -11
  92. package/src/__tests__/credential-prompt-route.test.ts +57 -7
  93. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  94. package/src/__tests__/cu-unified-flow.test.ts +89 -28
  95. package/src/__tests__/custom-group-listing-visibility.test.ts +1 -1
  96. package/src/__tests__/db-schedule-syntax-migration.test.ts +4 -1
  97. package/src/__tests__/discord-access-request-privacy.test.ts +366 -0
  98. package/src/__tests__/discord-requester-notice-privacy.test.ts +87 -0
  99. package/src/__tests__/disk-pressure-guard.test.ts +55 -55
  100. package/src/__tests__/disk-pressure-routes.test.ts +10 -10
  101. package/src/__tests__/disk-pressure-tools.test.ts +2 -2
  102. package/src/__tests__/disk-usage.test.ts +35 -20
  103. package/src/__tests__/document-sync-tags.test.ts +379 -0
  104. package/src/__tests__/document-update-default-surface.test.ts +28 -0
  105. package/src/__tests__/dynamic-skill-background-guard.test.ts +2 -2
  106. package/src/__tests__/emit-signal-routing-intent.test.ts +3 -4
  107. package/src/__tests__/ensure-conversation-exists.test.ts +8 -5
  108. package/src/__tests__/events-dev-bypass-actor.test.ts +183 -1
  109. package/src/__tests__/evict-conversations-for-reload.test.ts +90 -0
  110. package/src/__tests__/file-list-tool.test.ts +12 -10
  111. package/src/__tests__/file-ops-service.test.ts +151 -44
  112. package/src/__tests__/file-read-container-boundary.test.ts +137 -0
  113. package/src/__tests__/file-read-tool.test.ts +21 -9
  114. package/src/__tests__/file-url-path-guard.test.ts +83 -0
  115. package/src/__tests__/filesystem-tools.test.ts +82 -66
  116. package/src/__tests__/guardian-outbound-http.test.ts +8 -8
  117. package/src/__tests__/helpers/gateway-verification-sessions-stub.ts +14 -1
  118. package/src/__tests__/helpers/hub-clients.ts +42 -0
  119. package/src/__tests__/helpers/mock-conversation.ts +46 -0
  120. package/src/__tests__/helpers/verification-sessions-ipc-sim.ts +61 -57
  121. package/src/__tests__/history-repair.test.ts +112 -12
  122. package/src/__tests__/host-bash-proxy.test.ts +10 -5
  123. package/src/__tests__/host-bash-routes.test.ts +67 -0
  124. package/src/__tests__/host-browser-proxy.test.ts +2 -2
  125. package/src/__tests__/host-cu-proxy.test.ts +3 -7
  126. package/src/__tests__/host-file-proxy.test.ts +3 -7
  127. package/src/__tests__/host-file-read-tool.test.ts +5 -1
  128. package/src/__tests__/host-transfer-proxy-targeted.test.ts +6 -4
  129. package/src/__tests__/http-conversation-lineage.test.ts +2 -5
  130. package/src/__tests__/http-user-message-parity.test.ts +44 -0
  131. package/src/__tests__/identity-routes.test.ts +13 -13
  132. package/src/__tests__/image-conversion.test.ts +29 -26
  133. package/src/__tests__/image-source-path-reinject.test.ts +2 -2
  134. package/src/__tests__/inference-profile-session-handler.test.ts +95 -0
  135. package/src/__tests__/inference-profile-session-ipc.test.ts +25 -0
  136. package/src/__tests__/introduction-card-resolver.test.ts +6 -1
  137. package/src/__tests__/list-messages-attachments.test.ts +56 -32
  138. package/src/__tests__/list-messages-background-tool-completion.test.ts +4 -4
  139. package/src/__tests__/list-messages-client-message-id.test.ts +2 -2
  140. package/src/__tests__/list-messages-hidden-metadata.test.ts +16 -16
  141. package/src/__tests__/list-messages-page-latest.test.ts +61 -61
  142. package/src/__tests__/list-messages-provider-error.test.ts +6 -6
  143. package/src/__tests__/list-messages-queued.test.ts +49 -6
  144. package/src/__tests__/list-messages-system-card.test.ts +4 -4
  145. package/src/__tests__/list-messages-tool-merge.test.ts +9 -9
  146. package/src/__tests__/llm-resolver-override-or-default.test.ts +141 -0
  147. package/src/__tests__/llm-usage-store.test.ts +55 -0
  148. package/src/__tests__/manual-token-reconciliation.test.ts +45 -0
  149. package/src/__tests__/mcp-tool-annotations-risk.test.ts +133 -0
  150. package/src/__tests__/media-stream-server-integration.test.ts +36 -0
  151. package/src/__tests__/migration-export-http.test.ts +1 -1
  152. package/src/__tests__/migration-import-commit-http.test.ts +1 -1
  153. package/src/__tests__/migration-import-preflight-http.test.ts +1 -1
  154. package/src/__tests__/migration-validate-http.test.ts +1 -1
  155. package/src/__tests__/mtime-cache.test.ts +65 -3
  156. package/src/__tests__/notification-platform-adapter.test.ts +104 -0
  157. package/src/__tests__/notification-schedule-notify-dedup.test.ts +1 -1
  158. package/src/__tests__/oauth-provider-profiles.test.ts +4 -2
  159. package/src/__tests__/oauth-provider-serializer.test.ts +17 -0
  160. package/src/__tests__/oauth-providers-routes.test.ts +1 -0
  161. package/src/__tests__/openai-responses-prompt-cache.test.ts +5 -2
  162. package/src/__tests__/path-policy.test.ts +164 -1
  163. package/src/__tests__/persist-media-references.test.ts +2 -2
  164. package/src/__tests__/persist-unsendable-image-downscale.test.ts +5 -5
  165. package/src/__tests__/persist-unsendable-image.test.ts +23 -23
  166. package/src/__tests__/persona-resolver.test.ts +154 -0
  167. package/src/__tests__/plugin-api-webhook-url.test.ts +170 -0
  168. package/src/__tests__/plugin-import-boundary-guard.test.ts +6 -0
  169. package/src/__tests__/processing-flag-persist-failure.test.ts +15 -10
  170. package/src/__tests__/profile-availability-incomplete.test.ts +113 -0
  171. package/src/__tests__/profiler-routes.test.ts +13 -13
  172. package/src/__tests__/prompt-cache-cross-turn-stability.test.ts +593 -0
  173. package/src/__tests__/proxy-approval-callback.test.ts +3 -3
  174. package/src/__tests__/queued-message-cancel-and-steer.test.ts +189 -0
  175. package/src/__tests__/queued-message-steer-actor-scoping.test.ts +164 -0
  176. package/src/__tests__/resolve-trust-class.test.ts +20 -0
  177. package/src/__tests__/run-conversation-turn-persistence.test.ts +1 -1
  178. package/src/__tests__/run-due-schedules.test.ts +174 -0
  179. package/src/__tests__/runtime-attachment-metadata.test.ts +15 -7
  180. package/src/__tests__/scaffold-managed-skill-tool.test.ts +166 -0
  181. package/src/__tests__/schedule-routes.test.ts +730 -8
  182. package/src/__tests__/schedule-store.test.ts +540 -0
  183. package/src/__tests__/schedule-tools.test.ts +103 -0
  184. package/src/__tests__/scheduler-wake.test.ts +5 -0
  185. package/src/__tests__/secret-routes-platform-proxy.test.ts +29 -1
  186. package/src/__tests__/secure-keys.test.ts +91 -0
  187. package/src/__tests__/server-history-render.test.ts +97 -7
  188. package/src/__tests__/size-guard.test.ts +8 -8
  189. package/src/__tests__/skill-docs-sync-guard.test.ts +3 -2
  190. package/src/__tests__/skill-tool-factory.test.ts +247 -0
  191. package/src/__tests__/skills-install-staging.test.ts +15 -6
  192. package/src/__tests__/slack-edit-ordering-characterization.test.ts +103 -0
  193. package/src/__tests__/slack-inbound-verification.test.ts +4 -1
  194. package/src/__tests__/slack-mention-binding-invariance.test.ts +103 -0
  195. package/src/__tests__/slack-mention-provider-leak-audit.test.ts +135 -0
  196. package/src/__tests__/starter-task-flow.test.ts +5 -4
  197. package/src/__tests__/steer-on-enqueue-question.test.ts +3 -2
  198. package/src/__tests__/stt-language-catalog-parity.test.ts +38 -0
  199. package/src/__tests__/subagent-allowlist-validation.test.ts +26 -4
  200. package/src/__tests__/subagent-cron-run-attribution.test.ts +301 -0
  201. package/src/__tests__/subagent-disposal.test.ts +10 -0
  202. package/src/__tests__/subagent-fork-notifications.test.ts +10 -0
  203. package/src/__tests__/subagent-fork-prompt-role.test.ts +85 -11
  204. package/src/__tests__/subagent-fork-spawn.test.ts +15 -7
  205. package/src/__tests__/subagent-manager-notify.test.ts +148 -0
  206. package/src/__tests__/subagent-notify-parent.test.ts +1 -1
  207. package/src/__tests__/subagent-persistence.test.ts +141 -0
  208. package/src/__tests__/subagent-role-registry.test.ts +172 -63
  209. package/src/__tests__/subagent-role-resolution.test.ts +88 -0
  210. package/src/__tests__/subagent-spawn-and-await.test.ts +48 -0
  211. package/src/__tests__/subagent-spawn-tool-fork.test.ts +111 -10
  212. package/src/__tests__/subagent-terminal-message.test.ts +88 -1
  213. package/src/__tests__/subagent-tool-gate-mode.test.ts +225 -10
  214. package/src/__tests__/subagent-tools.test.ts +1901 -108
  215. package/src/__tests__/surface-completion-compaction-boundary.test.ts +124 -0
  216. package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +298 -0
  217. package/src/__tests__/system-prompt.test.ts +82 -7
  218. package/src/__tests__/tool-result-spool.test.ts +74 -18
  219. package/src/__tests__/tool-side-effects-documents.test.ts +174 -0
  220. package/src/__tests__/tool-side-effects-hook-failures.test.ts +129 -0
  221. package/src/__tests__/tools-audio-read.test.ts +10 -10
  222. package/src/__tests__/tools-get-route.test.ts +85 -12
  223. package/src/__tests__/tools-list-cli.test.ts +171 -0
  224. package/src/__tests__/top-level-renderer.test.ts +54 -1
  225. package/src/__tests__/trusted-contact-multichannel.test.ts +4 -1
  226. package/src/__tests__/turn-tail-chain.test.ts +135 -0
  227. package/src/__tests__/ui-choice-copy-surfaces.test.ts +6 -4
  228. package/src/__tests__/ui-visual-surface.test.ts +5 -4
  229. package/src/__tests__/ui-work-result-surface.test.ts +5 -4
  230. package/src/__tests__/unified-turn-context-visible-app.test.ts +7 -8
  231. package/src/__tests__/usage-routes.test.ts +62 -0
  232. package/src/__tests__/verification-outbound-delivery.test.ts +176 -0
  233. package/src/__tests__/visible-app-context.test.ts +20 -4
  234. package/src/__tests__/web-search-history.test.ts +96 -0
  235. package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +6 -6
  236. package/src/__tests__/workspace-migration-140-repair-seed-pinned-memory-v3-live.test.ts +330 -0
  237. package/src/acp/failure-error.ts +3 -5
  238. package/src/agent/attachments.ts +26 -24
  239. package/src/agent/history-repair/history-repair.ts +27 -26
  240. package/src/agent/image-optimize.ts +6 -6
  241. package/src/agent/loop.ts +40 -22
  242. package/src/agent/message-types.ts +3 -3
  243. package/src/api/constants/document-tools.ts +55 -0
  244. package/src/api/events/question-answered.ts +53 -0
  245. package/src/api/events/tool-result.ts +7 -0
  246. package/src/api/index.ts +13 -0
  247. package/src/api/responses/conversation-message.ts +11 -0
  248. package/src/api/surfaces-oauth-connect.test.ts +96 -0
  249. package/src/api/surfaces.ts +36 -8
  250. package/src/approvals/guardian-channel-delivery.ts +83 -0
  251. package/src/approvals/guardian-expiry-notifier.ts +13 -11
  252. package/src/approvals/guardian-request-resolvers.ts +216 -132
  253. package/src/calls/__tests__/voice-session-bridge.test.ts +50 -0
  254. package/src/calls/media-stream-server.ts +9 -1
  255. package/src/calls/voice-session-bridge.ts +38 -0
  256. package/src/channels/__tests__/gateway-verification-sessions.test.ts +3 -3
  257. package/src/channels/config.ts +7 -2
  258. package/src/channels/gateway-verification-sessions.ts +7 -2
  259. package/src/channels/types.ts +7 -1
  260. package/src/cli/commands/__tests__/cache.test.ts +7 -4
  261. package/src/cli/commands/__tests__/cli-test-harness.ts +82 -0
  262. package/src/cli/commands/__tests__/conversations-wake.test.ts +51 -6
  263. package/src/cli/commands/__tests__/inference-models.test.ts +14 -0
  264. package/src/cli/commands/__tests__/inference-profiles.test.ts +44 -1
  265. package/src/cli/commands/__tests__/inference-providers.test.ts +7 -0
  266. package/src/cli/commands/__tests__/plugins.test.ts +692 -0
  267. package/src/cli/commands/__tests__/schedules.test.ts +136 -37
  268. package/src/cli/commands/conversations.ts +7 -3
  269. package/src/cli/commands/credentials.help.ts +7 -0
  270. package/src/cli/commands/inference-profiles.ts +9 -3
  271. package/src/cli/commands/inference.help.ts +16 -2
  272. package/src/cli/commands/mcp.help.ts +4 -2
  273. package/src/cli/commands/memory/memory-retrospective.ts +18 -0
  274. package/src/cli/commands/notifications.ts +3 -6
  275. package/src/cli/commands/oauth/connect-surface-guidance.test.ts +16 -0
  276. package/src/cli/commands/oauth/connect-surface-guidance.ts +25 -7
  277. package/src/cli/commands/oauth/connect.test.ts +26 -1
  278. package/src/cli/commands/oauth/connect.ts +4 -1
  279. package/src/cli/commands/oauth/index.help.ts +3 -1
  280. package/src/cli/commands/oauth/providers.ts +3 -0
  281. package/src/cli/commands/plugins.help.ts +25 -2
  282. package/src/cli/commands/plugins.ts +289 -10
  283. package/src/cli/commands/schedules.help.ts +14 -12
  284. package/src/cli/commands/schedules.ts +19 -5
  285. package/src/cli/commands/tools.help.ts +4 -2
  286. package/src/cli/commands/tools.ts +38 -0
  287. package/src/cli/lib/__tests__/inspect-plugin.test.ts +1 -0
  288. package/src/cli/lib/__tests__/install-from-github.test.ts +79 -0
  289. package/src/cli/lib/__tests__/install-from-platform.test.ts +47 -1
  290. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +86 -7
  291. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +122 -0
  292. package/src/cli/lib/install-from-github.ts +61 -0
  293. package/src/cli/lib/install-from-platform.ts +13 -2
  294. package/src/cli/lib/plugin-surfaces.ts +124 -4
  295. package/src/cli/lib/upgrade-plugin.ts +18 -0
  296. package/src/cli.ts +1 -1
  297. package/src/config/__tests__/deployment-context-defaults.test.ts +73 -0
  298. package/src/config/__tests__/memory-retrospective-schema.test.ts +40 -0
  299. package/src/config/__tests__/plugin-updates-schema.test.ts +43 -0
  300. package/src/config/__tests__/profile-tool-support.test.ts +131 -0
  301. package/src/config/bundled-skills/AGENTS.md +1 -1
  302. package/src/config/bundled-skills/app-builder/SKILL.md +1 -1
  303. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +3 -3
  304. package/src/config/bundled-skills/schedule/SKILL.md +5 -1
  305. package/src/config/bundled-skills/schedule/TOOLS.json +2 -2
  306. package/src/config/bundled-skills/settings/TOOLS.json +2 -2
  307. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +1 -0
  308. package/src/config/bundled-skills/subagent/SKILL.md +53 -22
  309. package/src/config/bundled-skills/subagent/TOOLS.json +14 -13
  310. package/src/config/bundled-skills/visualize/SKILL.md +1 -1
  311. package/src/config/default-profile-catalog.ts +6 -2
  312. package/src/config/feature-flag-registry.json +25 -17
  313. package/src/config/llm-resolver.ts +45 -15
  314. package/src/config/loader.ts +70 -1
  315. package/src/config/profile-tool-support.ts +110 -0
  316. package/src/config/sanitize-for-transfer.ts +11 -0
  317. package/src/config/schema.ts +4 -0
  318. package/src/config/schemas/__tests__/stt.test.ts +20 -5
  319. package/src/config/schemas/channels.ts +13 -0
  320. package/src/config/schemas/memory-retrospective.ts +25 -0
  321. package/src/config/schemas/plugin-updates.ts +55 -0
  322. package/src/config/schemas/services.ts +8 -4
  323. package/src/config/schemas/stt.ts +17 -5
  324. package/src/contacts/__tests__/guardian-delivery-reader.test.ts +16 -0
  325. package/src/contacts/guardian-delivery-reader.ts +29 -2
  326. package/src/context/compactor.ts +4 -4
  327. package/src/context/post-turn-tool-result-truncation.ts +32 -16
  328. package/src/context/strip-injections.ts +42 -0
  329. package/src/context/tool-result-spool.ts +32 -18
  330. package/src/credential-execution/ces-runtime.ts +3 -0
  331. package/src/credential-execution/client.ts +22 -1
  332. package/src/credential-execution/process-manager.test.ts +99 -0
  333. package/src/credential-execution/process-manager.ts +62 -16
  334. package/src/daemon/__tests__/abort-null-controller.test.ts +141 -5
  335. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +8 -12
  336. package/src/daemon/__tests__/install-assistant-command.test.ts +234 -0
  337. package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +2 -4
  338. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +20 -29
  339. package/src/daemon/channel-ui-capability.ts +4 -4
  340. package/src/daemon/conversation-agent-loop-handlers.ts +117 -61
  341. package/src/daemon/conversation-agent-loop.ts +296 -145
  342. package/src/daemon/conversation-attachments.ts +1 -1
  343. package/src/daemon/conversation-error.ts +8 -1
  344. package/src/daemon/conversation-initial-prompt.ts +16 -38
  345. package/src/daemon/conversation-lifecycle.ts +154 -32
  346. package/src/daemon/conversation-messaging.ts +85 -42
  347. package/src/daemon/conversation-process.ts +76 -28
  348. package/src/daemon/conversation-runtime-assembly.ts +9 -4
  349. package/src/daemon/conversation-store.ts +52 -3
  350. package/src/daemon/conversation-surface-state.ts +9 -0
  351. package/src/daemon/conversation-surfaces.ts +403 -210
  352. package/src/daemon/conversation-tool-setup.ts +56 -7
  353. package/src/daemon/conversation-turn-finalize.ts +134 -89
  354. package/src/daemon/conversation.ts +105 -27
  355. package/src/daemon/disk-pressure-guard-lifecycle.ts +2 -2
  356. package/src/daemon/disk-pressure-guard.ts +3 -3
  357. package/src/daemon/doordash-steps.ts +5 -5
  358. package/src/daemon/external-plugins-bootstrap.ts +7 -1
  359. package/src/daemon/handlers/config-channels.ts +48 -5
  360. package/src/daemon/handlers/conversation-history.ts +1 -1
  361. package/src/daemon/handlers/conversations.ts +62 -17
  362. package/src/daemon/handlers/shared.ts +12 -0
  363. package/src/daemon/handlers/skills.ts +11 -1
  364. package/src/daemon/identity-helpers.ts +38 -10
  365. package/src/daemon/install-assistant-command.ts +322 -0
  366. package/src/daemon/lifecycle.ts +14 -3
  367. package/src/daemon/message-types/conversations.ts +6 -7
  368. package/src/daemon/message-types/sync.ts +1 -0
  369. package/src/daemon/persist-media-references.ts +23 -14
  370. package/src/daemon/process-message.ts +6 -6
  371. package/src/daemon/tool-setup-types.ts +19 -122
  372. package/src/daemon/tool-side-effects.ts +17 -94
  373. package/src/daemon/trust-context.ts +14 -6
  374. package/src/daemon/turn-tail-chain.ts +73 -0
  375. package/src/daemon/web-search-history.ts +47 -33
  376. package/src/heartbeat/heartbeat-service.ts +6 -6
  377. package/src/ipc/__tests__/socket-path.test.ts +21 -0
  378. package/src/ipc/assistant-server.ts +17 -10
  379. package/src/ipc/cli-client.ts +10 -1
  380. package/src/ipc/routes/__tests__/documents-sync-ipc-routes.test.ts +56 -0
  381. package/src/ipc/routes/documents-sync-ipc-routes.ts +42 -0
  382. package/src/ipc/socket-cleanup.ts +9 -7
  383. package/src/ipc/socket-path.ts +4 -96
  384. package/src/live-voice/__tests__/live-voice-archive.test.ts +16 -16
  385. package/src/live-voice/__tests__/live-voice-attach-image.test.ts +209 -0
  386. package/src/live-voice/__tests__/live-voice-connection.test.ts +20 -0
  387. package/src/live-voice/__tests__/live-voice-integration.test.ts +7 -5
  388. package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +265 -0
  389. package/src/live-voice/__tests__/live-voice-stt.test.ts +6 -1
  390. package/src/live-voice/__tests__/live-voice-vad.test.ts +35 -15
  391. package/src/live-voice/__tests__/protocol.test.ts +52 -0
  392. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +3 -0
  393. package/src/live-voice/live-voice-archive.ts +13 -13
  394. package/src/live-voice/live-voice-connection.ts +7 -1
  395. package/src/live-voice/live-voice-photo.ts +169 -0
  396. package/src/live-voice/live-voice-session.ts +108 -4
  397. package/src/live-voice/protocol.ts +89 -1
  398. package/src/mcp/client.ts +10 -0
  399. package/src/messaging/providers/__tests__/callback-routing.test.ts +5 -1
  400. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +66 -4
  401. package/src/messaging/providers/callback-routing.ts +1 -0
  402. package/src/messaging/providers/discord/api.ts +312 -0
  403. package/src/messaging/providers/discord/dm-delivery.test.ts +204 -0
  404. package/src/messaging/providers/discord/render.test.ts +204 -0
  405. package/src/messaging/providers/discord/render.ts +195 -0
  406. package/src/messaging/providers/discord/send.test.ts +179 -0
  407. package/src/messaging/providers/discord/send.ts +212 -0
  408. package/src/messaging/providers/discord/transport.ts +81 -0
  409. package/src/messaging/providers/index.ts +3 -1
  410. package/src/messaging/providers/retry-policy.test.ts +247 -0
  411. package/src/messaging/providers/retry-policy.ts +206 -0
  412. package/src/messaging/providers/slack/mention-source.test.ts +285 -0
  413. package/src/messaging/providers/slack/mention-source.ts +287 -0
  414. package/src/messaging/providers/telegram-bot/api.ts +47 -130
  415. package/src/messaging/providers/whatsapp/api.ts +47 -136
  416. package/src/monitoring/__tests__/db-integrity-sample.test.ts +4 -1
  417. package/src/monitoring/__tests__/plugin-auto-update.test.ts +332 -0
  418. package/src/monitoring/db-integrity-sample.ts +6 -1
  419. package/src/monitoring/plugin-auto-update.ts +359 -0
  420. package/src/monitoring/resource-sampler.ts +21 -5
  421. package/src/monitoring/worker.ts +13 -0
  422. package/src/notifications/__tests__/assistant-reply-producer.test.ts +375 -2
  423. package/src/notifications/__tests__/broadcaster.test.ts +37 -1
  424. package/src/notifications/__tests__/copy-composer.test.ts +165 -0
  425. package/src/notifications/__tests__/emit-signal-pipeline-failure.test.ts +205 -0
  426. package/src/notifications/__tests__/notification-utils.test.ts +249 -0
  427. package/src/notifications/adapters/platform.ts +31 -2
  428. package/src/notifications/assistant-reply-producer.ts +79 -32
  429. package/src/notifications/broadcaster.ts +30 -10
  430. package/src/notifications/copy-composer.ts +69 -0
  431. package/src/notifications/emit-signal.ts +82 -15
  432. package/src/notifications/events-store.ts +10 -2
  433. package/src/notifications/home-feed-side-effect.ts +6 -3
  434. package/src/notifications/notification-utils.ts +173 -5
  435. package/src/notifications/signal.ts +12 -0
  436. package/src/oauth/byo-connection.ts +6 -1
  437. package/src/oauth/credential-token-resolver.ts +3 -10
  438. package/src/oauth/manual-token-connection.ts +29 -64
  439. package/src/oauth/manual-token-providers.ts +62 -0
  440. package/src/oauth/provider-serializer.ts +13 -0
  441. package/src/oauth/seed-providers.ts +56 -8
  442. package/src/onboarding/onboarding-events-store.ts +91 -1
  443. package/src/permissions/question-prompter.test.ts +4 -0
  444. package/src/permissions/question-prompter.ts +21 -4
  445. package/src/persistence/__tests__/conversation-lineage.test.ts +248 -0
  446. package/src/persistence/attachments-store.ts +15 -15
  447. package/src/persistence/conversation-attention-store.ts +30 -10
  448. package/src/persistence/conversation-bootstrap.ts +17 -0
  449. package/src/persistence/conversation-crud.ts +363 -97
  450. package/src/persistence/conversation-group-migration.ts +1 -1
  451. package/src/persistence/conversation-key-store.ts +38 -10
  452. package/src/persistence/conversation-lineage.ts +226 -0
  453. package/src/persistence/conversation-plugin-facade.ts +28 -3
  454. package/src/persistence/conversation-queries.ts +207 -33
  455. package/src/persistence/conversation-types.ts +107 -1
  456. package/src/persistence/jobs-store.ts +32 -8
  457. package/src/persistence/llm-usage-store.ts +22 -0
  458. package/src/persistence/migrations/361-normalize-managed-connection-rows.test.ts +168 -0
  459. package/src/persistence/migrations/361-normalize-managed-connection-rows.ts +76 -0
  460. package/src/persistence/migrations/362-add-conversation-subagent-kind.test.ts +187 -0
  461. package/src/persistence/migrations/362-add-conversation-subagent-kind.ts +93 -0
  462. package/src/persistence/migrations/363-backfill-schedule-inference-profile.test.ts +229 -0
  463. package/src/persistence/migrations/363-backfill-schedule-inference-profile.ts +127 -0
  464. package/src/persistence/migrations/364-add-schedule-source-key.test.ts +120 -0
  465. package/src/persistence/migrations/364-add-schedule-source-key.ts +43 -0
  466. package/src/persistence/migrations/365-add-conversation-fork-strategy.ts +27 -0
  467. package/src/persistence/schema/conversations.ts +32 -0
  468. package/src/persistence/schema/infrastructure.ts +3 -0
  469. package/src/persistence/steps.ts +30 -0
  470. package/src/persistence/subagent-store.ts +148 -6
  471. package/src/platform/managed-speech.test.ts +20 -0
  472. package/src/platform/managed-speech.ts +9 -0
  473. package/src/plugin-api/index.ts +8 -0
  474. package/src/plugin-api/webhook-url.ts +151 -0
  475. package/src/plugins/__tests__/installed-plugin-dirs.test.ts +82 -0
  476. package/src/plugins/__tests__/source-fingerprint.test.ts +5 -1
  477. package/src/plugins/collect-source-versions.ts +4 -27
  478. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +5 -5
  479. package/src/plugins/defaults/exploration-drift/package.json +1 -1
  480. package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +4 -2
  481. package/src/plugins/defaults/image-recovery/recover.ts +67 -57
  482. package/src/plugins/defaults/index.ts +1 -1
  483. package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +64 -18
  484. package/src/plugins/defaults/memory/__tests__/jobs-worker-outcome-contract.test.ts +242 -0
  485. package/src/plugins/defaults/memory/__tests__/jobs-worker-retrospective-sweep.test.ts +24 -1
  486. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +48 -3
  487. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +511 -19
  488. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +39 -3
  489. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +493 -0
  490. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +5 -4
  491. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +5 -1
  492. package/src/plugins/defaults/memory/job-handlers.ts +87 -5
  493. package/src/plugins/defaults/memory/jobs-worker.ts +60 -6
  494. package/src/plugins/defaults/memory/memory-retrospective-constants.ts +11 -0
  495. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +29 -0
  496. package/src/plugins/defaults/memory/memory-retrospective-job.ts +342 -60
  497. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +23 -3
  498. package/src/plugins/defaults/memory/substrate/consolidation-lock.ts +9 -4
  499. package/src/plugins/defaults/memory/substrate/sweep-job.ts +7 -0
  500. package/src/plugins/defaults/memory/v1/graph/injection.ts +1 -1
  501. package/src/plugins/defaults/memory/v2/router.ts +8 -0
  502. package/src/plugins/defaults/turn-context/unified-turn-context.ts +1 -2
  503. package/src/plugins/external-plugin-loader.ts +17 -8
  504. package/src/plugins/installed-plugin-dirs.ts +101 -0
  505. package/src/plugins/mtime-cache.ts +34 -22
  506. package/src/plugins/types.ts +69 -8
  507. package/src/prompts/persona-resolver.ts +6 -2
  508. package/src/prompts/system-prompt.ts +72 -17
  509. package/src/providers/__tests__/inference.test.ts +40 -0
  510. package/src/providers/__tests__/preflight-resolved-config.test.ts +58 -1
  511. package/src/providers/__tests__/retry-callsite.test.ts +67 -0
  512. package/src/providers/__tests__/vellum-connection-routing.test.ts +42 -11
  513. package/src/providers/anthropic/client.ts +112 -95
  514. package/src/providers/connection-resolution.ts +35 -1
  515. package/src/providers/gemini/client.ts +5 -5
  516. package/src/providers/inference/adapter-factory.ts +16 -10
  517. package/src/providers/inference/auth.ts +16 -0
  518. package/src/providers/inference/connection-availability.ts +164 -12
  519. package/src/providers/inference/connections.ts +25 -1
  520. package/src/providers/media-resolve.ts +21 -11
  521. package/src/providers/model-catalog.ts +15 -0
  522. package/src/providers/openai/chat-completions-provider.ts +4 -4
  523. package/src/providers/openai/responses-provider.ts +12 -25
  524. package/src/providers/registry.ts +9 -4
  525. package/src/providers/retry.ts +25 -0
  526. package/src/providers/server-tool-pairing.ts +134 -0
  527. package/src/providers/speech-to-text/__tests__/resolve.test.ts +187 -6
  528. package/src/providers/speech-to-text/deepgram.ts +1 -0
  529. package/src/providers/speech-to-text/resolve.ts +80 -10
  530. package/src/providers/speech-to-text/vellum-managed.ts +7 -1
  531. package/src/providers/speech-to-text/vellum-speech-relay-connection.ts +3 -1
  532. package/src/providers/types.ts +26 -6
  533. package/src/providers/vellum-model-routing.ts +6 -7
  534. package/src/runtime/AGENTS.md +2 -0
  535. package/src/runtime/__tests__/agent-wake.test.ts +16 -16
  536. package/src/runtime/__tests__/background-job-runner.test.ts +0 -144
  537. package/src/runtime/__tests__/desktop-presence.test.ts +162 -0
  538. package/src/runtime/agent-wake.ts +63 -24
  539. package/src/runtime/assistant-event-hub.ts +87 -12
  540. package/src/runtime/auth/__tests__/guard-tests.test.ts +2 -0
  541. package/src/runtime/auth/__tests__/scopes.test.ts +20 -0
  542. package/src/runtime/auth/same-actor.ts +56 -8
  543. package/src/runtime/auth/scopes.ts +12 -0
  544. package/src/runtime/auth/types.ts +2 -0
  545. package/src/runtime/background-job-runner.ts +5 -27
  546. package/src/runtime/channel-readiness-service.ts +59 -3
  547. package/src/runtime/channel-readiness-types.ts +18 -0
  548. package/src/runtime/desktop-presence.ts +69 -0
  549. package/src/runtime/http-server.ts +12 -0
  550. package/src/runtime/local-actor-identity.ts +11 -9
  551. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -0
  552. package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
  553. package/src/runtime/routes/__tests__/client-routes.test.ts +186 -14
  554. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +547 -1
  555. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +162 -7
  556. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +14 -10
  557. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +340 -3
  558. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +28 -1
  559. package/src/runtime/routes/__tests__/plugins-routes.test.ts +173 -1
  560. package/src/runtime/routes/__tests__/question-routes.test.ts +64 -0
  561. package/src/runtime/routes/__tests__/retrospective-routes.test.ts +20 -0
  562. package/src/runtime/routes/__tests__/skill-history-route.test.ts +116 -0
  563. package/src/runtime/routes/__tests__/wake-conversation-routes.test.ts +60 -2
  564. package/src/runtime/routes/archive-utils.ts +16 -12
  565. package/src/runtime/routes/attachment-routes.ts +13 -6
  566. package/src/runtime/routes/canned-reply-release.ts +54 -0
  567. package/src/runtime/routes/channel-verification-routes.ts +2 -27
  568. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +5 -0
  569. package/src/runtime/routes/client-routes.ts +114 -2
  570. package/src/runtime/routes/conversation-cli-routes.ts +10 -12
  571. package/src/runtime/routes/conversation-list-routes.ts +59 -10
  572. package/src/runtime/routes/conversation-management-routes.ts +47 -18
  573. package/src/runtime/routes/conversation-query-routes.ts +32 -53
  574. package/src/runtime/routes/conversation-routes.ts +350 -293
  575. package/src/runtime/routes/credential-prompt-routes.ts +23 -2
  576. package/src/runtime/routes/debug-routes.ts +4 -1
  577. package/src/runtime/routes/default-provider-routes.ts +7 -11
  578. package/src/runtime/routes/documents-routes.ts +44 -1
  579. package/src/runtime/routes/events-routes.ts +28 -1
  580. package/src/runtime/routes/host-app-control-routes.ts +2 -0
  581. package/src/runtime/routes/host-bash-routes.ts +2 -0
  582. package/src/runtime/routes/host-browser-routes.ts +2 -0
  583. package/src/runtime/routes/host-cu-routes.ts +2 -0
  584. package/src/runtime/routes/host-file-routes.ts +2 -0
  585. package/src/runtime/routes/host-transfer-routes.ts +4 -0
  586. package/src/runtime/routes/identity-routes.ts +6 -6
  587. package/src/runtime/routes/inbound-message-handler.ts +5 -5
  588. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +7 -3
  589. package/src/runtime/routes/inbound-stages/background-dispatch.ts +14 -0
  590. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -2
  591. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +14 -11
  592. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -17
  593. package/src/runtime/routes/inference-profile-availability-guard.ts +171 -0
  594. package/src/runtime/routes/inference-profile-session-handler.ts +26 -2
  595. package/src/runtime/routes/inference-profile-session-routes.ts +2 -1
  596. package/src/runtime/routes/inference-profiles-routes.ts +152 -73
  597. package/src/runtime/routes/inference-provider-connection-routes.ts +0 -16
  598. package/src/runtime/routes/llm-call-sites-routes.ts +13 -0
  599. package/src/runtime/routes/log-export-routes.ts +1 -1
  600. package/src/runtime/routes/notification-routes.ts +0 -14
  601. package/src/runtime/routes/oauth-providers.ts +1 -0
  602. package/src/runtime/routes/plugins-routes.ts +106 -1
  603. package/src/runtime/routes/profiler-routes.ts +4 -2
  604. package/src/runtime/routes/question-routes.ts +28 -10
  605. package/src/runtime/routes/retrospective-routes.ts +9 -3
  606. package/src/runtime/routes/schedule-routes.ts +210 -12
  607. package/src/runtime/routes/secret-routes.ts +40 -23
  608. package/src/runtime/routes/settings-routes.ts +41 -13
  609. package/src/runtime/routes/skills-routes.ts +79 -0
  610. package/src/runtime/routes/ui-snapshot-routes.ts +1 -0
  611. package/src/runtime/routes/wake-conversation-routes.ts +23 -0
  612. package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
  613. package/src/runtime/services/conversation-serializer.ts +24 -9
  614. package/src/runtime/sync/documents-sidecar-publish.test.ts +101 -0
  615. package/src/runtime/sync/resource-sync-events.ts +79 -1
  616. package/src/runtime/sync/worker-daemon-notify.test.ts +34 -0
  617. package/src/runtime/sync/worker-daemon-notify.ts +44 -6
  618. package/src/runtime/verification-outbound-actions.ts +250 -222
  619. package/src/runtime/verification-templates.ts +41 -30
  620. package/src/schedule/__tests__/plugin-schedule-declarations.test.ts +614 -0
  621. package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +1066 -0
  622. package/src/schedule/inference-profile.ts +64 -3
  623. package/src/schedule/plugin-schedule-declarations.ts +518 -0
  624. package/src/schedule/plugin-schedule-reconciler.ts +605 -0
  625. package/src/schedule/plugin-schedules-gate.ts +23 -0
  626. package/src/schedule/schedule-store.ts +520 -26
  627. package/src/schedule/scheduler.ts +38 -0
  628. package/src/security/credential-backend.ts +51 -4
  629. package/src/security/encrypted-store.ts +64 -42
  630. package/src/security/secure-keys.ts +55 -7
  631. package/src/skills/catalog-install.ts +20 -7
  632. package/src/skills/inline-command-runner.ts +2 -7
  633. package/src/skills/skill-history.test.ts +269 -0
  634. package/src/skills/skill-history.ts +250 -0
  635. package/src/skills/skillssh-registry.ts +1 -1
  636. package/src/stt/daemon-batch-transcriber.ts +8 -6
  637. package/src/subagent/__tests__/consult-prompt.test.ts +25 -0
  638. package/src/subagent/consult-prompt.ts +3 -1
  639. package/src/subagent/consult-transcript.ts +1 -1
  640. package/src/subagent/manager.ts +295 -42
  641. package/src/subagent/progress-events.ts +35 -0
  642. package/src/subagent/role-resolution.ts +88 -0
  643. package/src/subagent/types.ts +302 -67
  644. package/src/subagent/validate-allowlists.ts +3 -4
  645. package/src/telegram/__tests__/webhook-health.test.ts +73 -1
  646. package/src/telegram/webhook-health.ts +101 -3
  647. package/src/telemetry/__tests__/telemetry-event-fixtures.ts +2 -0
  648. package/src/telemetry/activation-funnel.ts +6 -1
  649. package/src/telemetry/live-voice-funnel.ts +75 -0
  650. package/src/telemetry/telemetry-event-sources.ts +6 -0
  651. package/src/telemetry/telemetry-wire-source.json +1 -1
  652. package/src/telemetry/telemetry-wire.generated.ts +2 -0
  653. package/src/telemetry/types.ts +23 -4
  654. package/src/telemetry/usage-telemetry-reporter.test.ts +4 -0
  655. package/src/tools/__tests__/subagent-workflow-tool-input-schemas.test.ts +1 -1
  656. package/src/tools/ask-question/ask-question-tool.test.ts +118 -3
  657. package/src/tools/ask-question/ask-question-tool.ts +56 -2
  658. package/src/tools/browser/browser-execution.ts +1 -1
  659. package/src/tools/calls/call-start.ts +8 -5
  660. package/src/tools/conversation-groups/move_to_group.ts +10 -12
  661. package/src/tools/document/document-tool.ts +11 -4
  662. package/src/tools/executor.ts +7 -6
  663. package/src/tools/filesystem/edit.ts +1 -1
  664. package/src/tools/filesystem/list.ts +1 -1
  665. package/src/tools/filesystem/read.ts +7 -13
  666. package/src/tools/filesystem/search.ts +23 -21
  667. package/src/tools/filesystem/write.ts +4 -1
  668. package/src/tools/host-filesystem/edit.ts +7 -2
  669. package/src/tools/host-filesystem/read.ts +21 -8
  670. package/src/tools/host-filesystem/write.ts +10 -2
  671. package/src/tools/host-terminal/host-shell.ts +2 -2
  672. package/src/tools/mcp/mcp-tool-factory.ts +25 -1
  673. package/src/tools/permission-checker.ts +8 -3
  674. package/src/tools/schedule/delete.ts +13 -1
  675. package/src/tools/schedule/list.ts +14 -3
  676. package/src/tools/schedule/update.ts +56 -33
  677. package/src/tools/shared/filesystem/audio-read.ts +6 -4
  678. package/src/tools/shared/filesystem/file-ops-service.ts +176 -116
  679. package/src/tools/shared/filesystem/image-read.ts +12 -12
  680. package/src/tools/shared/filesystem/path-policy.ts +70 -5
  681. package/src/tools/shared/filesystem/size-guard.ts +6 -6
  682. package/src/tools/shared/input-misuse.ts +72 -0
  683. package/src/tools/skills/scaffold-managed.ts +82 -0
  684. package/src/tools/skills/skill-tool-factory.ts +14 -1
  685. package/src/tools/subagent/consult-deadline.ts +12 -7
  686. package/src/tools/subagent/message.ts +3 -1
  687. package/src/tools/subagent/read.ts +66 -4
  688. package/src/tools/subagent/spawn.ts +500 -78
  689. package/src/tools/types.ts +22 -3
  690. package/src/tools/ui-surface/surface-shape-docs.ts +1 -1
  691. package/src/tools/ui-surface/visual-validation.ts +2 -0
  692. package/src/usage/__tests__/subagent-attribution.test.ts +109 -0
  693. package/src/usage/subagent-attribution.ts +108 -0
  694. package/src/util/__tests__/sqlite-retry.test.ts +112 -0
  695. package/src/util/abort-reasons.ts +16 -0
  696. package/src/util/ansi.ts +44 -0
  697. package/src/util/disk-usage.ts +17 -13
  698. package/src/util/image-conversion.ts +20 -13
  699. package/src/util/schedule-source-key.ts +23 -0
  700. package/src/util/worker-process.ts +7 -1
  701. package/src/workspace/git-service.ts +33 -9
  702. package/src/workspace/migrations/140-repair-seed-pinned-memory-v3-live.ts +260 -0
  703. package/src/workspace/migrations/141-stt-english-default-to-multilingual.ts +111 -0
  704. package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +126 -0
  705. package/src/workspace/migrations/registry.ts +4 -0
  706. package/src/workspace/top-level-renderer.ts +15 -2
  707. package/src/__tests__/tool-side-effects-slack-dm.test.ts +0 -357
  708. package/src/daemon/install-symlink.ts +0 -207
  709. package/src/notifications/deferred-emit.ts +0 -147
@@ -28,7 +28,10 @@ import { deliverChannelReply } from "../runtime/gateway-client.js";
28
28
  import { introductionMode } from "../runtime/introduction-policy.js";
29
29
  import * as pendingInteractions from "../runtime/pending-interactions.js";
30
30
  import { getLogger } from "../util/logger.js";
31
- import { resolveDeliverCallbackUrlForChannel } from "./guardian-channel-delivery.js";
31
+ import {
32
+ resolveDeliverCallbackUrlForChannel,
33
+ resolveRequesterDeliveryTarget,
34
+ } from "./guardian-channel-delivery.js";
32
35
 
33
36
  const log = getLogger("guardian-expiry-notifier");
34
37
 
@@ -108,9 +111,10 @@ function releaseExpiredInteraction(request: GuardianRequestWire): void {
108
111
  *
109
112
  * The sweep is timer-driven and holds no inbound reply callback URL, so this
110
113
  * mirrors the resolvers' off-channel (desktop) delivery path: post to the
111
- * callback-less `/deliver/<channel>` route. On Slack the notice is routed to
112
- * the requester's DM via their user id rather than the channel id, so it is
113
- * never posted into a shared channel. No-ops on channels without a deliverable
114
+ * callback-less `/deliver/<channel>` route. On Slack and Discord the notice is
115
+ * routed to the requester's DM via their user id rather than the channel id,
116
+ * so it is never posted into a shared channel (see
117
+ * `resolveRequesterDeliveryTarget`). No-ops on channels without a deliverable
114
118
  * route (e.g. email, the in-app vellum surface) or when the requester chat is
115
119
  * unknown. Best-effort: a delivery failure is logged, never thrown.
116
120
  */
@@ -128,13 +132,11 @@ async function notifyRequesterOfExpiry(
128
132
  return;
129
133
  }
130
134
 
131
- // On Slack, target the requester's DM (their `U…` user id) instead of the
132
- // channel id so the expiry notice stays private. Other channels deliver to
133
- // the requester chat directly.
134
- const targetChatId =
135
- channel === "slack" && requesterExternalUserId
136
- ? requesterExternalUserId
137
- : requesterChatId;
135
+ const targetChatId = resolveRequesterDeliveryTarget({
136
+ channel,
137
+ requesterChatId,
138
+ requesterExternalUserId,
139
+ });
138
140
 
139
141
  try {
140
142
  await deliverChannelReply(deliverUrl, {
@@ -56,7 +56,13 @@ import {
56
56
  } from "../runtime/question-resolution.js";
57
57
  import { TC_GRANT_WAIT_MAX_MS } from "../tools/tool-approval-handler.js";
58
58
  import { getLogger } from "../util/logger.js";
59
- import { resolveDeliverCallbackUrlForChannel } from "./guardian-channel-delivery.js";
59
+ import {
60
+ channelCanAddressOneReaderInBand,
61
+ channelCanCompleteCodeHandshakeInDm,
62
+ channelDeliversToUserId,
63
+ resolveDeliverCallbackUrlForChannel,
64
+ resolveRequesterDeliveryTarget,
65
+ } from "./guardian-channel-delivery.js";
60
66
 
61
67
  const log = getLogger("guardian-request-resolvers");
62
68
 
@@ -97,14 +103,15 @@ function stripThreadTsParam(replyCallbackUrl: string): string {
97
103
  }
98
104
 
99
105
  /**
100
- * Deliver the verification code straight to the requester's Slack DM so the
106
+ * Deliver the verification code straight to the requester's DM so the
101
107
  * guardian is never an out-of-band courier for the secret.
102
108
  *
103
- * Slack is the only channel with a guaranteed private path to the requester:
104
- * posting to their user ID (`U…`) opens a 1:1 DM. The `threadTs` query param is
105
- * dropped because it points at the guardian's channel thread and would raise
106
- * `thread_not_found` in the DM. The code is sent as a durable (non-ephemeral)
107
- * message the requester can refer back to when verifying.
109
+ * Used on the channels with a guaranteed private path to a requester's user
110
+ * id: Slack, where posting to a `U…` id opens a 1:1 DM, and Discord, whose
111
+ * `dm`-marked route resolves a user snowflake to a DM channel. The `threadTs`
112
+ * query param is dropped because it points at the guardian's channel thread
113
+ * and would raise `thread_not_found` in the DM. The code is sent as a durable
114
+ * (non-ephemeral) message the requester can refer back to when verifying.
108
115
  *
109
116
  * The verification session is identity-bound to the requester, so delivering
110
117
  * the code to them directly does not widen who can consume it — it only removes
@@ -112,7 +119,7 @@ function stripThreadTsParam(replyCallbackUrl: string): string {
112
119
  *
113
120
  * Returns whether the code was delivered.
114
121
  */
115
- async function deliverVerificationCodeToSlackRequester(params: {
122
+ async function deliverVerificationCodeToRequester(params: {
116
123
  replyCallbackUrl: string;
117
124
  requesterExternalUserId: string;
118
125
  verificationCode: string;
@@ -133,12 +140,40 @@ async function deliverVerificationCodeToSlackRequester(params: {
133
140
  } catch (err) {
134
141
  log.error(
135
142
  { err, requesterExternalUserId: params.requesterExternalUserId },
136
- "Failed to auto-deliver verification code to Slack requester",
143
+ "Failed to auto-deliver verification code to requester",
137
144
  );
138
145
  return false;
139
146
  }
140
147
  }
141
148
 
149
+ /**
150
+ * The guardian-facing verification-code message.
151
+ *
152
+ * Delivered on up to three routes (in band, a guardian DM follow-up, and a
153
+ * guardian DM when their channel has no in-band route), so the wording lives
154
+ * here rather than being retyped at each one.
155
+ */
156
+ function guardianVerificationCodeText(
157
+ requesterLabel: string,
158
+ secret: string,
159
+ ): string {
160
+ return (
161
+ `You approved access for ${requesterLabel}. ` +
162
+ `Give them this verification code: \`${secret}\`. ` +
163
+ `The code expires in 10 minutes.`
164
+ );
165
+ }
166
+
167
+ /** Requester-facing approval notice when the guardian relays the code. */
168
+ const APPROVED_COURIER_NOTICE =
169
+ "Your access request has been approved! " +
170
+ "Please enter the 6-digit verification code you receive from the guardian.";
171
+
172
+ /** Requester-facing notice when the code could not be handed to the guardian. */
173
+ const APPROVED_CODE_UNDELIVERED_NOTICE =
174
+ "Your access request was approved, but we were unable to " +
175
+ "deliver the verification code. Please try again later.";
176
+
142
177
  /**
143
178
  * Build a requester-facing channel notice (approval/denial/courier text).
144
179
  *
@@ -695,9 +730,15 @@ function deriveAccessRequestDecision(
695
730
  * Deliver a requester-facing decision notice. On-channel decisions reply via
696
731
  * the channel delivery context (ephemeral on Slack shared channels);
697
732
  * off-channel (desktop) decisions post via the channel's deliver URL — on
698
- * Slack routed to the requester's user ID so the notice opens a DM instead
699
- * of posting into a shared channel. Delivery failures are logged, never
733
+ * Slack and Discord routed to the requester's user ID so the notice opens a DM
734
+ * instead of posting into a shared channel. Delivery failures are logged, never
700
735
  * thrown: the notice is best-effort and must not fail the decision.
736
+ *
737
+ * Discord takes the deliver-URL route in both cases, because its in-band route
738
+ * cannot be kept to one reader (see `channelCanAddressOneReaderInBand`).
739
+ * Posting a decision back in-band there would announce "your access request
740
+ * was declined" to a whole community channel, so Discord has one route to a
741
+ * requester and it is a DM.
701
742
  */
702
743
  async function deliverRequesterNotice(params: {
703
744
  channel: NotificationSourceChannel;
@@ -713,11 +754,19 @@ async function deliverRequesterNotice(params: {
713
754
  requesterChatId,
714
755
  requesterExternalUserId,
715
756
  assistantId,
716
- channelDeliveryContext,
717
757
  desktopDeliverUrl,
718
758
  text,
719
759
  } = params;
720
760
 
761
+ // Re-applied rather than assumed. The access-request resolver already drops
762
+ // the context for these channels, because its verification-code branch needs
763
+ // the same rule and never reaches this helper; repeating it here keeps the
764
+ // helper correct for any caller that has not, at the cost of one idempotent
765
+ // check.
766
+ const channelDeliveryContext = channelCanAddressOneReaderInBand(channel)
767
+ ? params.channelDeliveryContext
768
+ : undefined;
769
+
721
770
  if (channelDeliveryContext) {
722
771
  try {
723
772
  await deliverChannelReply(
@@ -740,10 +789,11 @@ async function deliverRequesterNotice(params: {
740
789
  }
741
790
 
742
791
  if (desktopDeliverUrl && requesterChatId) {
743
- const targetChatId =
744
- channel === "slack" && requesterExternalUserId
745
- ? requesterExternalUserId
746
- : requesterChatId;
792
+ const targetChatId = resolveRequesterDeliveryTarget({
793
+ channel,
794
+ requesterChatId,
795
+ requesterExternalUserId,
796
+ });
747
797
  try {
748
798
  await deliverChannelReply(desktopDeliverUrl, {
749
799
  chatId: targetChatId,
@@ -775,6 +825,13 @@ async function notifyRequesterOfDenial(params: {
775
825
  assistantId: string;
776
826
  channelDeliveryContext: ChannelDeliveryContext | undefined;
777
827
  desktopDeliverUrl: string | null;
828
+ /**
829
+ * Whether the guardian decided on a channel at all, as distinct from whether
830
+ * a route to the requester survived suppression. The lifecycle signal keys
831
+ * on the former; passing the suppressed context would drop the guardian's
832
+ * record of every Discord denial.
833
+ */
834
+ decidedOnChannel: boolean;
778
835
  deniedPayload: TrustedContactDecisionPayload;
779
836
  requestId: string;
780
837
  conversationId: string | null;
@@ -793,6 +850,7 @@ async function notifyRequesterOfDenial(params: {
793
850
  assistantId,
794
851
  channelDeliveryContext,
795
852
  desktopDeliverUrl,
853
+ decidedOnChannel,
796
854
  deniedPayload,
797
855
  requestId,
798
856
  conversationId,
@@ -820,7 +878,7 @@ async function notifyRequesterOfDenial(params: {
820
878
  // conversation for the same decision. The approve path holds the same
821
879
  // one-signal invariant via `verification_sent` standing in for
822
880
  // `guardian_decision`.
823
- if (channelDeliveryContext) {
881
+ if (decidedOnChannel) {
824
882
  void emitNotificationSignal({
825
883
  sourceEventName: "ingress.trusted_contact.guardian_decision",
826
884
  sourceChannel: channel,
@@ -981,6 +1039,17 @@ const accessRequestResolver: GuardianRequestResolver = {
981
1039
  // Non-voice approvals: mint an identity-bound verification session so the
982
1040
  // requester can verify their identity. The raw secret transits back on
983
1041
  // the decide response for daemon-owned delivery.
1042
+ //
1043
+ // The session is addressed to the requester, not to the room the request
1044
+ // arrived in. On the channels that reach a person by user id, that room is
1045
+ // one other people can read, so recording it as the destination describes
1046
+ // somewhere the code is never sent. `expectedChatId` keeps the room, which
1047
+ // is where the conversation is, and it is not a credential: whenever both
1048
+ // identity fields are set, `checkIdentityMatch` requires the user match.
1049
+ const destinationAddress = channelDeliversToUserId(channel)
1050
+ ? requesterExternalUserId || requesterChatId
1051
+ : requesterChatId;
1052
+
984
1053
  return {
985
1054
  ok: true,
986
1055
  aclOutcome: {
@@ -989,7 +1058,7 @@ const accessRequestResolver: GuardianRequestResolver = {
989
1058
  expectedExternalUserId: requesterExternalUserId,
990
1059
  expectedChatId: requesterChatId,
991
1060
  identityBindingStatus: "bound",
992
- destinationAddress: requesterChatId,
1061
+ destinationAddress,
993
1062
  verificationPurpose: "trusted_contact",
994
1063
  },
995
1064
  persistFailureReason: "verification_session_mint_failed",
@@ -997,7 +1066,7 @@ const accessRequestResolver: GuardianRequestResolver = {
997
1066
  },
998
1067
 
999
1068
  async resolve(ctx: ResolverContext): Promise<ResolverResult> {
1000
- const { request, decision, channelDeliveryContext } = ctx;
1069
+ const { request, decision } = ctx;
1001
1070
  const {
1002
1071
  channel,
1003
1072
  requesterExternalUserId,
@@ -1009,6 +1078,34 @@ const accessRequestResolver: GuardianRequestResolver = {
1009
1078
  const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
1010
1079
  const desktopDeliverUrl = resolveDeliverCallbackUrlForChannel(channel);
1011
1080
 
1081
+ // The reply context serves two readers on two different channels, so it is
1082
+ // suppressed per reader rather than wholesale.
1083
+ //
1084
+ // `requesterInBandContext` governs the requester-facing notice, which is
1085
+ // addressed into the REQUEST's channel: on Discord that is a guild channel
1086
+ // nobody can be singled out in, so it is dropped and the notice takes the
1087
+ // DM route the desktop path uses.
1088
+ //
1089
+ // `guardianInBandContext` governs the guardian's own copy of a verification
1090
+ // code, which is addressed into the channel the GUARDIAN replied on. A
1091
+ // Telegram guardian deciding a Discord request still has a private chat of
1092
+ // their own, and dropping their route because the requester's is public
1093
+ // would leave them with no confirmation and no code.
1094
+ const requesterInBandContext = channelCanAddressOneReaderInBand(channel)
1095
+ ? ctx.channelDeliveryContext
1096
+ : undefined;
1097
+ const guardianInBandContext = channelCanAddressOneReaderInBand(
1098
+ ctx.actor.channel,
1099
+ )
1100
+ ? ctx.channelDeliveryContext
1101
+ : undefined;
1102
+ // Where the guardian's own copy goes when their channel has no in-band
1103
+ // route. Resolved from THEIR channel, never the requester's, or the code
1104
+ // is handed to the wrong transport.
1105
+ const guardianDmUrl = resolveDeliverCallbackUrlForChannel(
1106
+ ctx.actor.channel,
1107
+ );
1108
+
1012
1109
  // Guardian-facing label prefers the contact display name over the raw ID.
1013
1110
  const requesterLabel =
1014
1111
  requesterDisplayName ||
@@ -1050,7 +1147,8 @@ const accessRequestResolver: GuardianRequestResolver = {
1050
1147
  requesterChatId,
1051
1148
  requesterExternalUserId,
1052
1149
  assistantId,
1053
- channelDeliveryContext,
1150
+ channelDeliveryContext: requesterInBandContext,
1151
+ decidedOnChannel: ctx.channelDeliveryContext !== undefined,
1054
1152
  desktopDeliverUrl,
1055
1153
  deniedPayload,
1056
1154
  requestId: request.id,
@@ -1091,7 +1189,8 @@ const accessRequestResolver: GuardianRequestResolver = {
1091
1189
  requesterChatId,
1092
1190
  requesterExternalUserId,
1093
1191
  assistantId,
1094
- channelDeliveryContext,
1192
+ channelDeliveryContext: requesterInBandContext,
1193
+ decidedOnChannel: ctx.channelDeliveryContext !== undefined,
1095
1194
  desktopDeliverUrl,
1096
1195
  deniedPayload,
1097
1196
  requestId: request.id,
@@ -1145,7 +1244,7 @@ const accessRequestResolver: GuardianRequestResolver = {
1145
1244
  requesterChatId,
1146
1245
  requesterExternalUserId,
1147
1246
  assistantId,
1148
- channelDeliveryContext,
1247
+ channelDeliveryContext: requesterInBandContext,
1149
1248
  desktopDeliverUrl,
1150
1249
  text: "Your access request has been approved. You can message the assistant here.",
1151
1250
  });
@@ -1187,39 +1286,46 @@ const accessRequestResolver: GuardianRequestResolver = {
1187
1286
  "Access request resolver: minted verification session",
1188
1287
  );
1189
1288
 
1190
- // Deliver the verification code to the guardian and notify the requester
1191
- // when channel delivery context is available (channel message path).
1289
+ // Two readers, decided separately. The guardian's copy of the code is
1290
+ // addressed on the channel THEY replied on; the requester's notice on the
1291
+ // channel the request came from. Those can differ (a Telegram guardian
1292
+ // deciding a Discord request), and the old single `if (channelDelivery
1293
+ // Context)` treated them as one, so suppressing a route for either reader
1294
+ // silently suppressed it for both.
1192
1295
  let requesterNotified = false;
1193
- if (channelDeliveryContext) {
1194
- let codeDelivered = true;
1195
-
1196
- // Deliver verification code to guardian
1197
- const codeText =
1198
- `You approved access for ${requesterLabel}. ` +
1199
- `Give them this verification code: \`${session.secret}\`. ` +
1200
- `The code expires in 10 minutes.`;
1296
+ let codeDelivered = false;
1297
+ const codeText = guardianVerificationCodeText(
1298
+ requesterLabel,
1299
+ session.secret,
1300
+ );
1301
+
1302
+ if (guardianInBandContext) {
1303
+ codeDelivered = true;
1201
1304
  try {
1202
1305
  const codePayload: Parameters<typeof deliverChannelReply>[1] = {
1203
- chatId: channelDeliveryContext.guardianChatId,
1306
+ chatId: guardianInBandContext.guardianChatId,
1204
1307
  text: codeText,
1205
1308
  assistantId,
1206
1309
  };
1207
1310
  // On Slack shared channels, deliver the verification code as ephemeral
1208
1311
  // so only the guardian sees the secret — not all channel members.
1209
1312
  if (
1210
- shouldUseEphemeral(channel, channelDeliveryContext.guardianChatId) &&
1313
+ shouldUseEphemeral(
1314
+ ctx.actor.channel,
1315
+ guardianInBandContext.guardianChatId,
1316
+ ) &&
1211
1317
  ctx.actor.actorExternalUserId
1212
1318
  ) {
1213
1319
  codePayload.ephemeral = true;
1214
1320
  codePayload.user = ctx.actor.actorExternalUserId;
1215
1321
  }
1216
1322
  await deliverChannelReply(
1217
- channelDeliveryContext.replyCallbackUrl,
1323
+ guardianInBandContext.replyCallbackUrl,
1218
1324
  codePayload,
1219
1325
  );
1220
1326
  } catch (err) {
1221
1327
  log.error(
1222
- { err, guardianChatId: channelDeliveryContext.guardianChatId },
1328
+ { err, guardianChatId: guardianInBandContext.guardianChatId },
1223
1329
  "Failed to deliver verification code to guardian",
1224
1330
  );
1225
1331
  codeDelivered = false;
@@ -1231,12 +1337,12 @@ const accessRequestResolver: GuardianRequestResolver = {
1231
1337
  const guardianUserId = ctx.actor.actorExternalUserId;
1232
1338
  if (
1233
1339
  codeDelivered &&
1234
- channel === "slack" &&
1340
+ ctx.actor.channel === "slack" &&
1235
1341
  guardianUserId &&
1236
- !channelDeliveryContext.guardianChatId.startsWith("D")
1342
+ !guardianInBandContext.guardianChatId.startsWith("D")
1237
1343
  ) {
1238
1344
  const dmCallbackUrl = stripThreadTsParam(
1239
- channelDeliveryContext.replyCallbackUrl,
1345
+ guardianInBandContext.replyCallbackUrl,
1240
1346
  );
1241
1347
 
1242
1348
  try {
@@ -1253,21 +1359,43 @@ const accessRequestResolver: GuardianRequestResolver = {
1253
1359
  );
1254
1360
  }
1255
1361
  }
1362
+ } else if (ctx.actor.channel === "vellum") {
1363
+ // A desktop guardian receives the code inline via `guardianReplyText`.
1364
+ codeDelivered = true;
1365
+ } else if (guardianDmUrl && decidedByExternalUserId) {
1366
+ // A channel guardian whose own channel has no in-band route. Without
1367
+ // this they approve and are never given the code they are meant to pass
1368
+ // on, because the inline reply text only reaches a desktop actor.
1369
+ try {
1370
+ await deliverChannelReply(guardianDmUrl, {
1371
+ chatId: decidedByExternalUserId,
1372
+ text: codeText,
1373
+ assistantId,
1374
+ });
1375
+ codeDelivered = true;
1376
+ } catch (err) {
1377
+ log.error(
1378
+ { err, decidedByExternalUserId },
1379
+ "Failed to deliver verification code to guardian DM",
1380
+ );
1381
+ }
1382
+ }
1256
1383
 
1257
- const requesterCallbackUrl =
1258
- channel === "slack" && requesterExternalUserId
1259
- ? stripThreadTsParam(channelDeliveryContext.replyCallbackUrl)
1260
- : channelDeliveryContext.replyCallbackUrl;
1384
+ // The requester. `requesterInBandContext` is the route back along their own
1385
+ // inbound message; `desktopDeliverUrl` is the callback-less route to them.
1386
+ const requesterReplyUrl =
1387
+ requesterInBandContext?.replyCallbackUrl ?? desktopDeliverUrl;
1261
1388
 
1389
+ if (requesterReplyUrl && requesterChatId) {
1262
1390
  if (codeDelivered) {
1263
- // On Slack, deliver the code straight to the requester's DM so the
1264
- // guardian doesn't have to relay it. Other channels (and a failed Slack
1265
- // delivery) fall back to the courier notice — there is no guaranteed
1266
- // private path to the requester elsewhere (e.g. group chats).
1391
+ // Where the channel can complete a code handshake in a DM, send the
1392
+ // code straight to the requester. Everywhere else the guardian relays
1393
+ // it and the requester gets a courier notice.
1267
1394
  const requesterCodeDelivered =
1268
- channel === "slack" && requesterExternalUserId
1269
- ? await deliverVerificationCodeToSlackRequester({
1270
- replyCallbackUrl: channelDeliveryContext.replyCallbackUrl,
1395
+ channelCanCompleteCodeHandshakeInDm(channel) &&
1396
+ requesterExternalUserId
1397
+ ? await deliverVerificationCodeToRequester({
1398
+ replyCallbackUrl: requesterReplyUrl,
1271
1399
  requesterExternalUserId,
1272
1400
  verificationCode: session.secret,
1273
1401
  assistantId,
@@ -1279,16 +1407,24 @@ const accessRequestResolver: GuardianRequestResolver = {
1279
1407
  } else {
1280
1408
  try {
1281
1409
  await deliverChannelReply(
1282
- requesterCallbackUrl,
1283
- buildRequesterChannelNotice({
1284
- channel,
1285
- requesterChatId,
1286
- requesterExternalUserId,
1287
- text:
1288
- "Your access request has been approved! " +
1289
- "Please enter the 6-digit verification code you receive from the guardian.",
1290
- assistantId,
1291
- }),
1410
+ requesterReplyUrl,
1411
+ requesterInBandContext
1412
+ ? buildRequesterChannelNotice({
1413
+ channel,
1414
+ requesterChatId,
1415
+ requesterExternalUserId,
1416
+ text: APPROVED_COURIER_NOTICE,
1417
+ assistantId,
1418
+ })
1419
+ : {
1420
+ chatId: resolveRequesterDeliveryTarget({
1421
+ channel,
1422
+ requesterChatId,
1423
+ requesterExternalUserId,
1424
+ }),
1425
+ text: APPROVED_COURIER_NOTICE,
1426
+ assistantId,
1427
+ },
1292
1428
  );
1293
1429
  requesterNotified = true;
1294
1430
  } catch (err) {
@@ -1301,16 +1437,24 @@ const accessRequestResolver: GuardianRequestResolver = {
1301
1437
  } else {
1302
1438
  try {
1303
1439
  await deliverChannelReply(
1304
- requesterCallbackUrl,
1305
- buildRequesterChannelNotice({
1306
- channel,
1307
- requesterChatId,
1308
- requesterExternalUserId,
1309
- text:
1310
- "Your access request was approved, but we were unable to " +
1311
- "deliver the verification code. Please try again later.",
1312
- assistantId,
1313
- }),
1440
+ requesterReplyUrl,
1441
+ requesterInBandContext
1442
+ ? buildRequesterChannelNotice({
1443
+ channel,
1444
+ requesterChatId,
1445
+ requesterExternalUserId,
1446
+ text: APPROVED_CODE_UNDELIVERED_NOTICE,
1447
+ assistantId,
1448
+ })
1449
+ : {
1450
+ chatId: resolveRequesterDeliveryTarget({
1451
+ channel,
1452
+ requesterChatId,
1453
+ requesterExternalUserId,
1454
+ }),
1455
+ text: APPROVED_CODE_UNDELIVERED_NOTICE,
1456
+ assistantId,
1457
+ },
1314
1458
  );
1315
1459
  } catch (err) {
1316
1460
  log.error(
@@ -1319,69 +1463,9 @@ const accessRequestResolver: GuardianRequestResolver = {
1319
1463
  );
1320
1464
  }
1321
1465
  }
1466
+ }
1322
1467
 
1323
- // Record the verification_sent lifecycle transition (delivery suppressed).
1324
- if (codeDelivered) {
1325
- emitVerificationSentSignal(
1326
- {
1327
- sourceChannel: channel,
1328
- requesterExternalUserId,
1329
- requesterChatId,
1330
- requesterDisplayName,
1331
- decidedByDisplayName,
1332
- verificationSessionId: session.sessionId,
1333
- },
1334
- request.sourceConversationId,
1335
- );
1336
- }
1337
- } else {
1338
- // Guardian decided off-channel (e.g. desktop). The guardian receives the
1339
- // verification code inline via `guardianReplyText` regardless of the
1340
- // requester's channel, so the lifecycle transition is recorded for every
1341
- // off-channel approve — including channels with no deliverable callback
1342
- // (e.g. email), where the requester cannot be auto-notified here.
1343
- if (desktopDeliverUrl && requesterChatId) {
1344
- // The requester is on a deliverable channel. On Slack, DM the code
1345
- // directly (parity with the on-channel path); otherwise fall back to
1346
- // the courier notice.
1347
- const requesterCodeDelivered =
1348
- channel === "slack" && requesterExternalUserId
1349
- ? await deliverVerificationCodeToSlackRequester({
1350
- replyCallbackUrl: desktopDeliverUrl,
1351
- requesterExternalUserId,
1352
- verificationCode: session.secret,
1353
- assistantId,
1354
- })
1355
- : false;
1356
-
1357
- if (requesterCodeDelivered) {
1358
- requesterNotified = true;
1359
- } else {
1360
- // For Slack, route to DM via requesterExternalUserId (user ID)
1361
- // instead of requesterChatId (channel ID) to avoid posting in public
1362
- // channels.
1363
- const targetChatId =
1364
- channel === "slack" && requesterExternalUserId
1365
- ? requesterExternalUserId
1366
- : requesterChatId;
1367
- try {
1368
- await deliverChannelReply(desktopDeliverUrl, {
1369
- chatId: targetChatId,
1370
- text:
1371
- "Your access request has been approved! " +
1372
- "Please enter the 6-digit verification code you receive from the guardian.",
1373
- assistantId,
1374
- });
1375
- requesterNotified = true;
1376
- } catch (err) {
1377
- log.error(
1378
- { err, requesterChatId },
1379
- "Failed to notify requester of access request approval (desktop decision path)",
1380
- );
1381
- }
1382
- }
1383
- }
1384
-
1468
+ if (codeDelivered) {
1385
1469
  // Record the verification_sent lifecycle transition for every off-channel
1386
1470
  // approve. The session is minted and the guardian has the code via
1387
1471
  // `guardianReplyText` regardless of whether (or how) the requester was
@@ -380,6 +380,56 @@ describe("startVoiceTurn escalation-continuation persistence", () => {
380
380
  voiceSessionTurn: true,
381
381
  });
382
382
  });
383
+
384
+ test("a live-voice turn persists its session attribution under metadata.client", async () => {
385
+ const fake = makeFakeConversation({ processing: false });
386
+ fakeConversation = fake.conversation;
387
+
388
+ await startVoiceTurn({
389
+ ...makeTurnOptions(),
390
+ voiceTelemetry: { sessionId: "session-123", client: "ios" },
391
+ });
392
+
393
+ // `turn-events-store` projects `$.client` onto `TurnTelemetryEvent.client`,
394
+ // so this bag is what makes a voice turn countable per client and joinable
395
+ // to its session's funnel rows. The platform goes in `os`, the same key
396
+ // the HTTP send path fills, so voice turns sit in the column existing turn
397
+ // analytics already read.
398
+ expect(fake.lastPersistOpts()?.metadata).toEqual({
399
+ voiceSessionTurn: true,
400
+ client: {
401
+ voice: true,
402
+ voice_session_id: "session-123",
403
+ os: "ios",
404
+ },
405
+ });
406
+ });
407
+
408
+ test("omits the client key when the session reported no originating client", async () => {
409
+ const fake = makeFakeConversation({ processing: false });
410
+ fakeConversation = fake.conversation;
411
+
412
+ await startVoiceTurn({
413
+ ...makeTurnOptions(),
414
+ voiceTelemetry: { sessionId: "session-123" },
415
+ });
416
+
417
+ expect(fake.lastPersistOpts()?.metadata).toEqual({
418
+ voiceSessionTurn: true,
419
+ client: { voice: true, voice_session_id: "session-123" },
420
+ });
421
+ });
422
+
423
+ test("a phone turn carries no client bag", async () => {
424
+ const fake = makeFakeConversation({ processing: false });
425
+ fakeConversation = fake.conversation;
426
+
427
+ await startVoiceTurn(makeTurnOptions());
428
+
429
+ // Only live-voice sessions pass `voiceTelemetry`; a phone call has no
430
+ // live-voice session id to attribute a turn to.
431
+ expect(fake.lastPersistOpts()?.metadata).not.toHaveProperty("client");
432
+ });
383
433
  });
384
434
 
385
435
  describe("startVoiceTurn hiddenSyntheticPrompt", () => {
@@ -38,6 +38,7 @@
38
38
  * - Media stream `stop` event / WebSocket close -> finalize call.
39
39
  */
40
40
 
41
+ import { GATEWAY_TUNNEL_LOST_WS_CLOSE_CODE } from "@vellumai/service-contracts/ingress";
41
42
  import type { ServerWebSocket } from "bun";
42
43
 
43
44
  import { revokeScopedApprovalGrantsForContext } from "../approvals/scoped-approval-grants.js";
@@ -344,9 +345,16 @@ export class MediaStreamCallSession {
344
345
  );
345
346
  }
346
347
  } else {
348
+ // The gateway's close reason does not survive the relay (Bun's
349
+ // WebSocket client drops it), so tunnel loss is decoded from the
350
+ // dedicated close code.
347
351
  const detail =
348
352
  reason ||
349
- (code ? `media_stream_closed_${code}` : "media_stream_closed_abnormal");
353
+ (code === GATEWAY_TUNNEL_LOST_WS_CLOSE_CODE
354
+ ? "public ingress tunnel disconnected"
355
+ : code
356
+ ? `media_stream_closed_${code}`
357
+ : "media_stream_closed_abnormal");
350
358
  updateCallSession(this.callSessionId, {
351
359
  status: "failed",
352
360
  endedAt: Date.now(),