@vellumai/assistant 0.10.9 → 0.10.10-dev.202607162206.d08e98e

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 (456) hide show
  1. package/ARCHITECTURE.md +1 -1
  2. package/Dockerfile +8 -0
  3. package/docs/activation-funnel-telemetry.md +13 -7
  4. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/redacted-credential.test.ts +200 -0
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/redacted-credential.ts +226 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/redacted-credential.test.ts +200 -0
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/redacted-credential.ts +226 -0
  10. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/redacted-credential.test.ts +200 -0
  12. package/node_modules/@vellumai/service-contracts/src/redacted-credential.ts +226 -0
  13. package/openapi.yaml +530 -107
  14. package/package.json +1 -1
  15. package/scripts/generate-openapi.ts +8 -0
  16. package/src/__tests__/activation-early-marking.test.ts +6 -5
  17. package/src/__tests__/agent-loop-override-profile.test.ts +22 -25
  18. package/src/__tests__/agent-wake-override-profile.test.ts +21 -48
  19. package/src/__tests__/app-builder-tool-scripts.test.ts +0 -1
  20. package/src/__tests__/app-bundler.test.ts +4 -12
  21. package/src/__tests__/app-executors.test.ts +2 -49
  22. package/src/__tests__/app-routes-csp.test.ts +114 -146
  23. package/src/__tests__/auth-fallback-events-store.test.ts +8 -1
  24. package/src/__tests__/build-persisted-content.test.ts +96 -0
  25. package/src/__tests__/bundle-scanner.test.ts +27 -1
  26. package/src/__tests__/call-controller.test.ts +291 -0
  27. package/src/__tests__/call-site-routing-connection-auto-resolve.test.ts +165 -0
  28. package/src/__tests__/chat-credential-redaction.test.ts +1395 -0
  29. package/src/__tests__/chat-reveal-guard-priming.test.ts +791 -0
  30. package/src/__tests__/compaction.benchmark.test.ts +2 -1
  31. package/src/__tests__/compactor-image-manifest-trust.test.ts +50 -0
  32. package/src/__tests__/config-loader-backfill.test.ts +9 -4
  33. package/src/__tests__/config-schema-cmd.test.ts +10 -11
  34. package/src/__tests__/config-schema.test.ts +190 -257
  35. package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +208 -0
  36. package/src/__tests__/conversation-agent-loop-overflow.test.ts +5 -19
  37. package/src/__tests__/conversation-agent-loop.test.ts +22 -9
  38. package/src/__tests__/conversation-error.test.ts +31 -0
  39. package/src/__tests__/conversation-load-history-repair.test.ts +110 -0
  40. package/src/__tests__/conversation-process-callsite.test.ts +12 -19
  41. package/src/__tests__/conversation-routes-slash-commands.test.ts +7 -21
  42. package/src/__tests__/conversation-summarize-route.test.ts +36 -44
  43. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +1 -0
  44. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +7 -7
  45. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +0 -1
  46. package/src/__tests__/conversation-tool-setup-attribution.test.ts +0 -1
  47. package/src/__tests__/conversation-usage.test.ts +4 -12
  48. package/src/__tests__/credential-routes.test.ts +250 -0
  49. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  50. package/src/__tests__/db-migration-rollback.test.ts +22 -0
  51. package/src/__tests__/empty-response-hook.test.ts +186 -1
  52. package/src/__tests__/external-plugin-loader.test.ts +11 -5
  53. package/src/__tests__/heartbeat-service.test.ts +0 -28
  54. package/src/__tests__/host-shell-tool.test.ts +2 -0
  55. package/src/__tests__/inactive-tool-error-messages.test.ts +2 -2
  56. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +52 -8
  57. package/src/__tests__/internal-telemetry-routes.test.ts +23 -4
  58. package/src/__tests__/invite-routes-http.test.ts +12 -16
  59. package/src/__tests__/list-all-apps.test.ts +0 -4
  60. package/src/__tests__/llm-context-resolution.test.ts +32 -56
  61. package/src/__tests__/llm-request-log-turn-query.test.ts +109 -0
  62. package/src/__tests__/llm-resolver-override-or-default.test.ts +3 -52
  63. package/src/__tests__/llm-resolver.test.ts +342 -602
  64. package/src/__tests__/llm-schema.test.ts +79 -37
  65. package/src/__tests__/max-tokens-continue-hook.test.ts +19 -0
  66. package/src/__tests__/media-stream-output.test.ts +259 -3
  67. package/src/__tests__/media-stream-server-integration.test.ts +22 -1
  68. package/src/__tests__/media-stream-stt-session.test.ts +47 -0
  69. package/src/__tests__/memory-jobs-worker-cleanup-cadence.test.ts +33 -0
  70. package/src/__tests__/memory-recall-log-store.test.ts +47 -13
  71. package/src/__tests__/mock-gateway-ipc.ts +46 -1
  72. package/src/__tests__/mtime-cache.test.ts +61 -0
  73. package/src/__tests__/navigate-settings-tab.test.ts +2 -0
  74. package/src/__tests__/normalize-onboarding.test.ts +33 -0
  75. package/src/__tests__/onboarding-persona-write.test.ts +26 -0
  76. package/src/__tests__/plugin-api-resolve-credential.test.ts +140 -0
  77. package/src/__tests__/plugin-app-serve-routes.test.ts +166 -14
  78. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +2 -0
  79. package/src/__tests__/post-turn-tool-result-truncation.test.ts +38 -0
  80. package/src/__tests__/provider-commit-message-generator.test.ts +27 -20
  81. package/src/__tests__/provider-connections-backfill.test.ts +138 -0
  82. package/src/__tests__/provider-platform-proxy-integration.test.ts +10 -31
  83. package/src/__tests__/provider-registry-ollama.test.ts +8 -18
  84. package/src/__tests__/provider-send-message-override-profile.test.ts +23 -23
  85. package/src/__tests__/provider-usage-tracking.test.ts +9 -19
  86. package/src/__tests__/prune-old-conversations-job.test.ts +12 -0
  87. package/src/__tests__/published-app-updater.test.ts +22 -16
  88. package/src/__tests__/registry.test.ts +5 -16
  89. package/src/__tests__/retry-openrouter-only-normalization.test.ts +22 -23
  90. package/src/__tests__/retry-thinking-adaptive-only.test.ts +43 -48
  91. package/src/__tests__/retry-thinking-tool-choice.test.ts +57 -66
  92. package/src/__tests__/retry-verbosity-normalization.test.ts +24 -23
  93. package/src/__tests__/reveal-success-registry.test.ts +123 -0
  94. package/src/__tests__/run-conversation-turn-persistence.test.ts +130 -0
  95. package/src/__tests__/secret-fixtures.ts +9 -0
  96. package/src/__tests__/server-history-render.test.ts +28 -0
  97. package/src/__tests__/skills.test.ts +9 -4
  98. package/src/__tests__/slack-share-routes.test.ts +0 -1
  99. package/src/__tests__/stt-stream-session.test.ts +6 -5
  100. package/src/__tests__/subagent-call-site-routing.test.ts +69 -95
  101. package/src/__tests__/subagent-disposal.test.ts +2 -0
  102. package/src/__tests__/subagent-fork-notifications.test.ts +2 -0
  103. package/src/__tests__/subagent-fork-spawn.test.ts +2 -0
  104. package/src/__tests__/subagent-manager-notify.test.ts +2 -0
  105. package/src/__tests__/subagent-role-registry.test.ts +37 -0
  106. package/src/__tests__/subagent-spawn-and-await.test.ts +1 -0
  107. package/src/__tests__/subagent-terminal-message.test.ts +50 -0
  108. package/src/__tests__/subagent-tool-gate-mode.test.ts +78 -5
  109. package/src/__tests__/surface-completion-nudge-hook.test.ts +19 -0
  110. package/src/__tests__/telemetry-routes.test.ts +99 -19
  111. package/src/__tests__/tool-audit.test.ts +34 -4
  112. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
  113. package/src/__tests__/tool-profiler.test.ts +72 -1
  114. package/src/__tests__/tool-result-spool.test.ts +49 -4
  115. package/src/__tests__/tool-side-effects-slack-dm.test.ts +0 -1
  116. package/src/__tests__/ui-channel-variants.test.ts +108 -0
  117. package/src/__tests__/ui-shape-teaching.test.ts +255 -0
  118. package/src/__tests__/usage-attribution.test.ts +18 -41
  119. package/src/__tests__/user-plugin-loader.test.ts +4 -4
  120. package/src/__tests__/voice-config-update.test.ts +46 -0
  121. package/src/__tests__/voice-session-bridge.test.ts +216 -84
  122. package/src/__tests__/workspace-migration-131-drop-web-fetch-mode.test.ts +120 -0
  123. package/src/agent/loop.ts +4 -3
  124. package/src/api/events/open-conversation.test.ts +64 -0
  125. package/src/api/events/open-conversation.ts +33 -0
  126. package/src/api/index.ts +6 -0
  127. package/src/api/responses/conversation-message.ts +5 -0
  128. package/src/apps/app-store.ts +25 -35
  129. package/src/bundler/app-bundler.ts +34 -48
  130. package/src/bundler/app-compiler.ts +39 -4
  131. package/src/bundler/bundle-scanner.ts +13 -0
  132. package/src/bundler/manifest.ts +1 -1
  133. package/src/calls/__tests__/voice-session-bridge.test.ts +47 -0
  134. package/src/calls/call-constants.ts +5 -0
  135. package/src/calls/call-controller.ts +100 -32
  136. package/src/calls/call-transport.ts +9 -0
  137. package/src/calls/media-stream-output.ts +107 -4
  138. package/src/calls/media-stream-server.ts +29 -9
  139. package/src/calls/media-stream-stt-session.ts +7 -1
  140. package/src/calls/media-turn-detector.ts +11 -1
  141. package/src/calls/voice-session-bridge.ts +84 -63
  142. package/src/cli/commands/__tests__/inference-providers.test.ts +270 -33
  143. package/src/cli/commands/__tests__/notifications.test.ts +24 -3
  144. package/src/cli/commands/config.help.ts +6 -6
  145. package/src/cli/commands/credentials.help.ts +13 -0
  146. package/src/cli/commands/credentials.ts +6 -1
  147. package/src/cli/commands/email.help.ts +7 -0
  148. package/src/cli/commands/email.ts +35 -1
  149. package/src/cli/commands/inference-providers.ts +168 -107
  150. package/src/cli/commands/inference.help.ts +118 -46
  151. package/src/cli/commands/memory/index.help.ts +23 -0
  152. package/src/cli/commands/memory/nodes.ts +146 -0
  153. package/src/cli/commands/notifications.help.ts +10 -10
  154. package/src/cli/commands/oauth/connect-surface-guidance.test.ts +40 -0
  155. package/src/cli/commands/oauth/connect-surface-guidance.ts +54 -0
  156. package/src/cli/commands/oauth/connect.test.ts +126 -0
  157. package/src/cli/commands/oauth/connect.ts +29 -6
  158. package/src/cli/commands/oauth/index.help.ts +7 -1
  159. package/src/cli/commands/oauth/status.test.ts +69 -3
  160. package/src/cli/commands/oauth/status.ts +50 -12
  161. package/src/cli/commands/plugins.help.ts +13 -2
  162. package/src/cli/commands/plugins.ts +61 -7
  163. package/src/cli/commands/telemetry.help.ts +13 -0
  164. package/src/cli/commands/telemetry.ts +45 -5
  165. package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +8 -2
  166. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +213 -9
  167. package/src/cli/lib/bundled-marketplace.json +93 -36
  168. package/src/cli/lib/inspect-plugin.ts +5 -1
  169. package/src/cli/lib/install-from-github.ts +68 -4
  170. package/src/cli/lib/plugin-catalog-local.ts +6 -2
  171. package/src/cli/lib/plugin-fingerprint.ts +3 -3
  172. package/src/cli/lib/upgrade-plugin.ts +236 -35
  173. package/src/config/__tests__/default-profile-catalog.test.ts +8 -12
  174. package/src/config/__tests__/plugin-resident-skill-discovery.test.ts +137 -0
  175. package/src/config/__tests__/profile-materialization.test.ts +1 -88
  176. package/src/config/bundled-skills/AGENTS.md +3 -30
  177. package/src/config/bundled-skills/app-builder/SKILL.md +5 -3
  178. package/src/config/bundled-skills/app-builder/TOOLS.json +23 -0
  179. package/src/config/bundled-skills/app-builder/tools/app-open.ts +32 -0
  180. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +1 -1
  181. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +7 -7
  182. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  183. package/src/config/bundled-skills/schedule/TOOLS.json +1 -1
  184. package/src/config/bundled-skills/settings/TOOLS.json +3 -1
  185. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +2 -0
  186. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +18 -10
  187. package/src/config/bundled-skills/subagent/SKILL.md +2 -0
  188. package/src/config/bundled-skills/subagent/TOOLS.json +1 -1
  189. package/src/config/call-site-defaults.ts +3 -3
  190. package/src/config/feature-flag-registry.json +24 -39
  191. package/src/config/llm-resolver.ts +88 -475
  192. package/src/config/profile-materialization.ts +11 -11
  193. package/src/config/schema.ts +93 -127
  194. package/src/config/schemas/__tests__/live-voice.test.ts +24 -6
  195. package/src/config/schemas/__tests__/stt.test.ts +31 -3
  196. package/src/config/schemas/live-voice.ts +10 -4
  197. package/src/config/schemas/llm.ts +52 -15
  198. package/src/config/schemas/memory-lifecycle.ts +1 -1
  199. package/src/config/schemas/memory-retrospective.ts +8 -0
  200. package/src/config/schemas/memory-v2.ts +2 -2
  201. package/src/config/schemas/memory-v3.ts +1 -1
  202. package/src/config/schemas/services.ts +6 -3
  203. package/src/config/schemas/stt.ts +38 -21
  204. package/src/config/schemas/tts.ts +13 -18
  205. package/src/config/skills.ts +31 -21
  206. package/src/context/compactor.ts +44 -7
  207. package/src/context/post-turn-tool-result-truncation.ts +4 -2
  208. package/src/context/tool-result-spool.ts +26 -5
  209. package/src/conversations/__tests__/message-consolidation.test.ts +48 -0
  210. package/src/conversations/message-consolidation.ts +22 -2
  211. package/src/daemon/__tests__/conversation-tool-setup.test.ts +5 -12
  212. package/src/daemon/app-source-watcher.ts +17 -23
  213. package/src/daemon/chat-credential-redaction.ts +1365 -0
  214. package/src/daemon/conversation-agent-loop-handlers.ts +627 -34
  215. package/src/daemon/conversation-agent-loop.ts +71 -31
  216. package/src/daemon/conversation-error.ts +36 -14
  217. package/src/daemon/conversation-process.ts +22 -0
  218. package/src/daemon/conversation-store.ts +35 -0
  219. package/src/daemon/conversation-surfaces.ts +25 -25
  220. package/src/daemon/conversation-tool-setup.ts +33 -7
  221. package/src/daemon/conversation.ts +55 -5
  222. package/src/daemon/handlers/shared.ts +33 -2
  223. package/src/daemon/lifecycle.ts +4 -4
  224. package/src/daemon/message-types/conversations.ts +6 -17
  225. package/src/daemon/providers-setup.ts +8 -0
  226. package/src/daemon/tool-setup-types.ts +7 -1
  227. package/src/daemon/wake-conversation-ops.ts +10 -1
  228. package/src/hooks/types.ts +9 -0
  229. package/src/ipc/__tests__/email-ipc.test.ts +90 -0
  230. package/src/ipc/gateway-client.test.ts +59 -0
  231. package/src/ipc/gateway-client.ts +70 -27
  232. package/src/live-voice/__tests__/live-voice-events.test.ts +14 -2
  233. package/src/live-voice/__tests__/live-voice-integration.test.ts +116 -2
  234. package/src/live-voice/__tests__/live-voice-vad.test.ts +804 -13
  235. package/src/live-voice/__tests__/protocol.test.ts +122 -0
  236. package/src/live-voice/live-voice-session.ts +443 -40
  237. package/src/live-voice/protocol.ts +143 -1
  238. package/src/monitoring/__tests__/plugin-source-watch.test.ts +3 -1
  239. package/src/monitoring/plugin-source-watch.ts +3 -62
  240. package/src/notifications/README.md +1 -1
  241. package/src/permissions/checker.ts +8 -4
  242. package/src/persistence/__tests__/db-init-migrations-ok.test.ts +26 -0
  243. package/src/persistence/conversation-crud.ts +104 -2
  244. package/src/persistence/db-init.ts +14 -3
  245. package/src/persistence/job-handlers/cleanup.ts +15 -7
  246. package/src/persistence/llm-request-log-store.ts +88 -52
  247. package/src/persistence/migrations/298-move-memory-jobs-to-memory-db.ts +7 -31
  248. package/src/persistence/migrations/305-drop-contact-acl-columns.ts +3 -2
  249. package/src/persistence/migrations/326-move-injection-events-to-memory-db.ts +8 -34
  250. package/src/persistence/migrations/336-move-memory-v2-activation-logs-to-memory-db.ts +90 -0
  251. package/src/persistence/migrations/337-move-memory-recall-logs-to-memory-db.ts +114 -0
  252. package/src/persistence/migrations/338-move-memory-v3-selections-to-memory-db.ts +84 -0
  253. package/src/persistence/migrations/339-move-activation-sessions-to-memory-db.ts +52 -0
  254. package/src/persistence/migrations/__tests__/run-migrations.test.ts +155 -0
  255. package/src/persistence/migrations/helpers/relocation.ts +44 -1
  256. package/src/persistence/migrations/run-migrations.ts +25 -1
  257. package/src/persistence/schema/infrastructure.ts +9 -0
  258. package/src/persistence/schema/memory-core.ts +3 -0
  259. package/src/persistence/schema/memory-injection.ts +2 -0
  260. package/src/persistence/steps.ts +36 -0
  261. package/src/platform/client.test.ts +1 -44
  262. package/src/platform/client.ts +10 -20
  263. package/src/platform/consent-cache.test.ts +89 -23
  264. package/src/platform/consent-cache.ts +71 -33
  265. package/src/plugin-api/constants.ts +12 -0
  266. package/src/plugin-api/conversation-turn.ts +37 -14
  267. package/src/plugin-api/index.ts +11 -1
  268. package/src/plugin-api/resolve-credential.ts +75 -0
  269. package/src/plugin-api/vision-support.test.ts +8 -19
  270. package/src/plugin-api/vision-support.ts +26 -17
  271. package/src/plugins/collect-source-versions.ts +77 -0
  272. package/src/plugins/defaults/compaction/compact.ts +6 -0
  273. package/src/plugins/defaults/compaction/window-manager.ts +9 -0
  274. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +18 -24
  275. package/src/plugins/defaults/empty-response/hooks/user-prompt-submit.ts +38 -0
  276. package/src/plugins/defaults/empty-response/refusal-quarantine.ts +99 -0
  277. package/src/plugins/defaults/image-fallback/__tests__/caption-cache-persistence.test.ts +7 -3
  278. package/src/plugins/defaults/index.ts +8 -1
  279. package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +4 -1
  280. package/src/plugins/defaults/memory/__tests__/activation-session-store.test.ts +48 -6
  281. package/src/plugins/defaults/memory/__tests__/db-memory-attach.test.ts +28 -22
  282. package/src/plugins/defaults/memory/__tests__/memory-log-stores-degraded.test.ts +148 -0
  283. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +80 -8
  284. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +202 -0
  285. package/src/plugins/defaults/memory/__tests__/memory-v2-activation-log-store.test.ts +64 -25
  286. package/src/plugins/defaults/memory/__tests__/memory-v2-concept-frequency.test.ts +24 -11
  287. package/src/plugins/defaults/memory/__tests__/prompt-override.test.ts +70 -6
  288. package/src/plugins/defaults/memory/__tests__/table-relocation.test.ts +278 -0
  289. package/src/plugins/defaults/memory/activation-session-store.ts +27 -20
  290. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +2 -9
  291. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +1 -8
  292. package/src/plugins/defaults/memory/graph/retriever.test.ts +6 -6
  293. package/src/plugins/defaults/memory/graph/store.ts +114 -0
  294. package/src/plugins/defaults/memory/graph/tool-handlers.ts +36 -1
  295. package/src/plugins/defaults/memory/graph/tools.ts +47 -13
  296. package/src/plugins/defaults/memory/graph-topology/build-memory-graph.ts +16 -35
  297. package/src/plugins/defaults/memory/jobs-worker.ts +10 -1
  298. package/src/plugins/defaults/memory/memory-db.ts +3 -2
  299. package/src/plugins/defaults/memory/memory-recall-log-store.ts +129 -66
  300. package/src/plugins/defaults/memory/memory-retrospective-constants.ts +8 -0
  301. package/src/plugins/defaults/memory/memory-retrospective-job.ts +11 -122
  302. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +216 -0
  303. package/src/plugins/defaults/memory/memory-v2-activation-log-store.ts +84 -46
  304. package/src/plugins/defaults/memory/memory-v2-concept-frequency.ts +42 -32
  305. package/src/plugins/defaults/memory/path-containment.ts +21 -0
  306. package/src/plugins/defaults/memory/prompt-override.ts +51 -13
  307. package/src/plugins/defaults/memory/tools.test.ts +34 -0
  308. package/src/plugins/defaults/memory/tools.ts +12 -2
  309. package/src/plugins/defaults/memory/v2/__tests__/harness-compare.test.ts +19 -15
  310. package/src/plugins/defaults/memory/v2/__tests__/harness-oracle.test.ts +24 -19
  311. package/src/plugins/defaults/memory/v2/__tests__/harness-replay-input.test.ts +19 -15
  312. package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +22 -2
  313. package/src/plugins/defaults/memory/v2/__tests__/prompts-consolidation.test.ts +5 -3
  314. package/src/plugins/defaults/memory/v2/harness/oracle.ts +59 -41
  315. package/src/plugins/defaults/memory/v2/harness/replay-input.ts +29 -25
  316. package/src/plugins/defaults/memory/v2/migration.ts +46 -16
  317. package/src/plugins/defaults/memory/v2/prompts/consolidation.ts +4 -0
  318. package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +17 -15
  319. package/src/plugins/defaults/memory/v3/__tests__/gate.test.ts +24 -22
  320. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +9 -5
  321. package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +277 -10
  322. package/src/plugins/defaults/memory/v3/__tests__/selection-log-store.test.ts +57 -5
  323. package/src/plugins/defaults/memory/v3/__tests__/shadow-integration.test.ts +9 -7
  324. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +38 -7
  325. package/src/plugins/defaults/memory/v3/hot-set.test.ts +36 -18
  326. package/src/plugins/defaults/memory/v3/hot-set.ts +12 -14
  327. package/src/plugins/defaults/memory/v3/learned-edges.test.ts +47 -27
  328. package/src/plugins/defaults/memory/v3/learned-edges.ts +12 -14
  329. package/src/plugins/defaults/memory/v3/orchestrate.ts +139 -22
  330. package/src/plugins/defaults/memory/v3/prune.test.ts +10 -3
  331. package/src/plugins/defaults/memory/v3/prune.ts +17 -11
  332. package/src/plugins/defaults/memory/v3/selection-log-store.ts +23 -11
  333. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +70 -53
  334. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +9 -6
  335. package/src/plugins/external-plugin-loader.ts +15 -15
  336. package/src/plugins/mtime-cache.ts +47 -0
  337. package/src/plugins/pipeline.ts +9 -1
  338. package/src/plugins/plugin-execution-context.ts +44 -0
  339. package/src/plugins/plugin-tree-walk.ts +31 -24
  340. package/src/plugins/source-fingerprint.ts +3 -4
  341. package/src/prompts/normalize-onboarding.ts +12 -0
  342. package/src/prompts/persona-resolver.ts +8 -0
  343. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +1 -1
  344. package/src/providers/__tests__/dispatch-connection-routing.test.ts +6 -9
  345. package/src/providers/__tests__/registry-native-web-search.test.ts +4 -10
  346. package/src/providers/__tests__/retry-callsite.test.ts +235 -215
  347. package/src/providers/__tests__/satellite-connection-routing.test.ts +8 -16
  348. package/src/providers/atlascloud/client.ts +10 -49
  349. package/src/providers/baseten/client.ts +43 -0
  350. package/src/providers/call-site-routing.ts +26 -13
  351. package/src/providers/connection-resolution.ts +9 -11
  352. package/src/providers/fetch-provider-catalog.ts +4 -2
  353. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +29 -4
  354. package/src/providers/inference/__tests__/connection-availability-keyless.test.ts +78 -0
  355. package/src/providers/inference/adapter-factory.ts +27 -6
  356. package/src/providers/inference/auth.ts +29 -1
  357. package/src/providers/inference/backfill.ts +61 -10
  358. package/src/providers/inference/connection-availability.ts +3 -2
  359. package/src/providers/inference/resolve-auth.ts +9 -1
  360. package/src/providers/model-catalog.ts +37 -0
  361. package/src/providers/openai/__tests__/api-error-normalization.test.ts +24 -2
  362. package/src/providers/openai/api-key-validation.ts +70 -0
  363. package/src/providers/retry.ts +2 -0
  364. package/src/providers/types.ts +11 -4
  365. package/src/providers/vellum-model-routing.test.ts +26 -0
  366. package/src/providers/vellum-model-routing.ts +30 -0
  367. package/src/providers/voice-error-copy.ts +47 -0
  368. package/src/runtime/agent-wake.ts +40 -24
  369. package/src/runtime/for-chat-mint-registry.ts +118 -0
  370. package/src/runtime/reveal-nonce.ts +49 -0
  371. package/src/runtime/reveal-success-registry.ts +306 -0
  372. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +88 -3
  373. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +161 -30
  374. package/src/runtime/routes/__tests__/migration-vellum-metadata-reconcile.test.ts +7 -0
  375. package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +15 -0
  376. package/src/runtime/routes/app-management-routes.ts +152 -45
  377. package/src/runtime/routes/app-routes.ts +16 -77
  378. package/src/runtime/routes/canned-message-complete.ts +16 -16
  379. package/src/runtime/routes/conversation-management-routes.ts +17 -14
  380. package/src/runtime/routes/conversation-query-routes.ts +54 -6
  381. package/src/runtime/routes/conversation-routes.ts +48 -2
  382. package/src/runtime/routes/credential-routes.ts +116 -3
  383. package/src/runtime/routes/email-routes.ts +17 -1
  384. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +33 -5
  385. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +6 -5
  386. package/src/runtime/routes/inference-provider-connection-routes.ts +77 -28
  387. package/src/runtime/routes/inference-send-routes.ts +1 -1
  388. package/src/runtime/routes/internal-telemetry-routes.ts +7 -6
  389. package/src/runtime/routes/plugins-routes.ts +37 -6
  390. package/src/runtime/routes/publish-routes.ts +15 -18
  391. package/src/runtime/routes/schedule-worker-routes.ts +9 -0
  392. package/src/runtime/routes/secret-routes.ts +10 -0
  393. package/src/runtime/routes/telemetry-routes.ts +149 -45
  394. package/src/schedule/__tests__/schedule-timezone.test.ts +101 -0
  395. package/src/schedule/__tests__/worker-watchdog.test.ts +209 -0
  396. package/src/schedule/schedule-store.ts +12 -1
  397. package/src/schedule/schedule-timezone.ts +63 -0
  398. package/src/schedule/scheduler.ts +100 -6
  399. package/src/schedule/worker-control.ts +19 -0
  400. package/src/security/auth-fallback-events-store.ts +7 -6
  401. package/src/security/secret-scanner.ts +26 -1
  402. package/src/services/published-app-updater.ts +6 -11
  403. package/src/stt/stt-stream-session.ts +36 -16
  404. package/src/subagent/manager.ts +34 -8
  405. package/src/telemetry/AGENTS.md +30 -1
  406. package/src/telemetry/__tests__/config-setting-snapshot.test.ts +18 -0
  407. package/src/telemetry/__tests__/outbox-test-harness.ts +5 -3
  408. package/src/telemetry/config-setting-snapshot.ts +44 -10
  409. package/src/telemetry/telemetry-event-sources.test.ts +124 -30
  410. package/src/telemetry/telemetry-event-sources.ts +137 -83
  411. package/src/telemetry/telemetry-events-outbox.test.ts +46 -1
  412. package/src/telemetry/telemetry-events-outbox.ts +60 -9
  413. package/src/telemetry/telemetry-wire-source.json +1 -1
  414. package/src/telemetry/telemetry-wire-validation.ts +39 -2
  415. package/src/telemetry/telemetry-wire.generated.ts +8 -0
  416. package/src/telemetry/tool-audit.ts +15 -9
  417. package/src/telemetry/tool-executed-events-store.test.ts +1 -1
  418. package/src/telemetry/turn-events-store.ts +20 -0
  419. package/src/telemetry/types.ts +45 -12
  420. package/src/telemetry/usage-telemetry-reporter.test.ts +294 -25
  421. package/src/telemetry/usage-telemetry-reporter.ts +117 -18
  422. package/src/telemetry/watchdog-direct-emit.test.ts +11 -3
  423. package/src/telemetry/watchdog-direct-emit.ts +11 -6
  424. package/src/tools/apps/executors.ts +11 -36
  425. package/src/tools/executor.ts +14 -1
  426. package/src/tools/host-terminal/host-shell.ts +6 -2
  427. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +1 -1
  428. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +25 -0
  429. package/src/tools/network/web-fetch.ts +6 -2
  430. package/src/tools/skills/sandbox-runner.ts +5 -2
  431. package/src/tools/subagent/spawn.ts +7 -11
  432. package/src/tools/terminal/shell.ts +5 -2
  433. package/src/tools/tool-manifest.ts +0 -2
  434. package/src/tools/tool-profiler.ts +37 -6
  435. package/src/tools/ui-surface/channel-variants.ts +101 -0
  436. package/src/tools/ui-surface/definitions.ts +23 -67
  437. package/src/tools/ui-surface/surface-shape-docs.ts +229 -0
  438. package/src/tts/__tests__/provider-adapters.test.ts +18 -0
  439. package/src/tts/provider-catalog.ts +2 -4
  440. package/src/tts/providers/deepgram-provider.ts +10 -1
  441. package/src/types/onboarding-context.ts +8 -0
  442. package/src/usage/attribution.ts +18 -112
  443. package/src/{config/bundled-skills/messaging/tools/gmail-mime-helpers.ts → util/mime-type.ts} +4 -1
  444. package/src/util/provider-error-patterns.ts +7 -1
  445. package/src/util/worker-process.ts +105 -0
  446. package/src/watcher/__tests__/telemetry.test.ts +17 -4
  447. package/src/watcher/telemetry.ts +5 -5
  448. package/src/workspace/migrations/131-drop-web-fetch-mode.ts +61 -0
  449. package/src/workspace/migrations/registry.ts +2 -0
  450. package/src/workspace/provider-commit-message-generator.ts +6 -4
  451. package/src/__tests__/app-open-proxy.test.ts +0 -67
  452. package/src/onboarding/onboarding-research-events-store.test.ts +0 -230
  453. package/src/onboarding/onboarding-research-events-store.ts +0 -104
  454. package/src/runtime/routes/assets/vellum-design-system.css +0 -2236
  455. package/src/tools/apps/definitions.ts +0 -73
  456. package/src/tools/apps/open-proxy.ts +0 -43
@@ -1,3 +1,7 @@
1
+ import {
2
+ neutralizeRedactedSentinels,
3
+ SENTINEL_REDACTION_VERSION,
4
+ } from "@vellumai/service-contracts/redacted-credential";
1
5
  import { v4 as uuid } from "uuid";
2
6
 
3
7
  import type {
@@ -11,6 +15,7 @@ import { getConfig } from "../../config/loader.js";
11
15
  import type { LLMCallSite, Speed } from "../../config/schemas/llm.js";
12
16
  import { ipcCall as gatewayIpcCall } from "../../ipc/gateway-client.js";
13
17
  import type { SecretPromptResult } from "../../permissions/secret-prompt-types.js";
18
+ import type { ConversationCreateType } from "../../persistence/conversation-types.js";
14
19
  import { resolveMediaSourceData } from "../../providers/media-resolve.js";
15
20
  import { isPlaceholderSentinelText } from "../../providers/placeholder-sentinels.js";
16
21
  import type { MediaSource } from "../../providers/types.js";
@@ -180,6 +185,13 @@ export interface ConversationCreateOptions {
180
185
  * chronological renderer to consume.
181
186
  */
182
187
  slackInbound?: SlackInboundMessageMetadata;
188
+ /**
189
+ * Conversation type for newly created conversations. When omitted,
190
+ * defaults to `"standard"` (visible in the sidebar). Set to
191
+ * `"background"` for plugin-driven conversations that should not
192
+ * appear in the sidebar's Recents grouping.
193
+ */
194
+ conversationType?: ConversationCreateType;
183
195
  }
184
196
 
185
197
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -315,6 +327,10 @@ export function renderHistoryContent(
315
327
  } else {
316
328
  text = unwrapExternalContentForDisplay(String(content));
317
329
  }
330
+ // Raw-string rows predate the block-shaped persist path entirely, so
331
+ // they can never carry the `_redactionVersion` rider — always run the
332
+ // sentinel forgery guard (see the rider check in the block walk below).
333
+ text = neutralizeRedactedSentinels(text);
318
334
  return {
319
335
  text,
320
336
  toolCalls: [],
@@ -445,7 +461,22 @@ export function renderHistoryContent(
445
461
  }
446
462
 
447
463
  if (block.type === "text" && typeof block.text === "string") {
448
- const displayText = unwrapExternalContentForDisplay(block.text);
464
+ // Forgery guard for pre-feature history: blocks persisted before the
465
+ // chat-credential sentinel work never had sentinel-shaped strings
466
+ // neutralized at write, so a forged `〔redacted:…〕` in an old row could
467
+ // manufacture a reveal chip on a chip-enabled client surface. Blocks
468
+ // written by the neutralization-aware persist path carry the
469
+ // `_redactionVersion` rider (internal, never shipped on the wire) and
470
+ // pass through verbatim — their surviving sentinels are
471
+ // redactor-authored. Everything else is neutralized here at the read
472
+ // boundary, the same belt-and-suspenders posture as the
473
+ // `<no_response/>` and placeholder-sentinel strips below.
474
+ const rawText =
475
+ typeof block._redactionVersion === "number" &&
476
+ block._redactionVersion >= SENTINEL_REDACTION_VERSION
477
+ ? block.text
478
+ : neutralizeRedactedSentinels(block.text);
479
+ const displayText = unwrapExternalContentForDisplay(rawText);
449
480
  // Skip empty/whitespace-only text blocks. During streaming the client
450
481
  // discards empty text deltas (guard !text.isEmpty), so including them
451
482
  // here produces a contentOrder that differs from the live streaming
@@ -673,7 +704,7 @@ export function renderHistoryContent(
673
704
  continue;
674
705
  }
675
706
  const resolved = resolveMediaSourceData(source);
676
- if (resolved) imageDataList.push(resolved.data);
707
+ if (resolved) {imageDataList.push(resolved.data);}
677
708
  }
678
709
  }
679
710
  }
@@ -156,8 +156,8 @@ export async function runDaemon(): Promise<void> {
156
156
  // so a fast-reconnecting client could otherwise resolve a turn against a
157
157
  // partial profile in the window between readiness and the post-overlay
158
158
  // ensure call below. Sync, DB-free, and idempotent. Skipped when an
159
- // unconsumed onboarding overlay is pending: the overlay can rewrite
160
- // llm.default later this boot, and baking against the pre-overlay default
159
+ // unconsumed onboarding overlay is pending: the overlay can rewrite the
160
+ // llm config later this boot, and baking against the pre-overlay default
161
161
  // would pin the wrong baseline — on that single boot (a fresh hatch, with
162
162
  // no established clients to race the window) the post-overlay pass owns
163
163
  // materialization; the overlay file is consumed on merge, so every
@@ -235,7 +235,7 @@ export async function runDaemon(): Promise<void> {
235
235
  } else {
236
236
  setDbMigrationFailed();
237
237
  log.error(
238
- "Daemon startup: DB opened but one or more migrations failed — /readyz will remain unready",
238
+ "Daemon startup: DB opened but one or more migrations failed or were deferred — /readyz will remain unready",
239
239
  );
240
240
  }
241
241
  // Migrations have settled (successfully or in the failed degraded mode),
@@ -553,7 +553,7 @@ export async function runDaemon(): Promise<void> {
553
553
  }
554
554
 
555
555
  // Refresh the consent cache regardless of dev mode so record-time telemetry
556
- // writes (gated on getCachedShareAnalytics()) work in dev too. The usage
556
+ // writes (which drop on a confirmed opt-out) work in dev too. The usage
557
557
  // telemetry reporter re-checks share_analytics on every flush, so dev still
558
558
  // never sends telemetry to the platform. Fire-and-forget: startConsentRefresh()
559
559
  // runs an immediate non-blocking refresh, so the startup hot path is never
@@ -8,6 +8,7 @@ import type { ConversationNoticeEvent } from "../../api/events/conversation-noti
8
8
  import type { ConversationTitleUpdatedEvent } from "../../api/events/conversation-title-updated.js";
9
9
  import type { GenerationCancelledEvent } from "../../api/events/generation-cancelled.js";
10
10
  import type { GenerationHandoffEvent } from "../../api/events/generation-handoff.js";
11
+ import type { OpenConversationEvent } from "../../api/events/open-conversation.js";
11
12
  import type { UsageProgressEvent } from "../../api/events/usage-progress.js";
12
13
  import type { UsageUpdateEvent } from "../../api/events/usage-update.js";
13
14
  import type {
@@ -505,22 +506,10 @@ export interface ScheduleConversationCreated {
505
506
  title: string;
506
507
  }
507
508
 
508
- /**
509
- * Server push — instructs the client to open and focus a conversation. If
510
- * the conversation isn't already present in the client's sidebar list (e.g.
511
- * it was just created via `POST /v1/conversations`), the client should stub
512
- * a sidebar entry using the provided `title` before navigating.
513
- */
514
- export interface OpenConversation {
515
- type: "open_conversation";
516
- conversationId: string;
517
- /** Optional conversation title; supplied when the client may not yet have the conversation in its list. */
518
- title?: string;
519
- /** Optional message ID to scroll to after focus. */
520
- anchorMessageId?: string;
521
- /** When `false`, the client should register the conversation in its sidebar (so it's visible and navigable) but must NOT switch focus to it. Omitting the field defaults to `true` for backward compatibility with existing single-target 'jump to conversation' callers. */
522
- focus?: boolean;
523
- }
509
+ // `open_conversation` is a migrated event: its canonical wire contract lives
510
+ // in `../../api/events/open-conversation.ts` (imported as
511
+ // `OpenConversationEvent`). Instructs the client to open and, by default,
512
+ // focus a conversation see that file for the full field docs.
524
513
 
525
514
  // --- Domain-level union aliases (consumed by the barrel file) ---
526
515
 
@@ -565,4 +554,4 @@ export type _ConversationsServerMessages =
565
554
  | MessageContentResponse
566
555
  | ConversationListInvalidatedEvent
567
556
  | ScheduleConversationCreated
568
- | OpenConversation;
557
+ | OpenConversationEvent;
@@ -1,3 +1,4 @@
1
+ import { maybeDefaultSpeechToManaged } from "../config/managed-speech-defaults.js";
1
2
  import { rehydratePlatformCredentials } from "../config/platform-rehydration.js";
2
3
  import type { AssistantConfig } from "../config/types.js";
3
4
  import { getMcpServerManager } from "../mcp/manager.js";
@@ -32,6 +33,13 @@ export async function initializeProvidersAndTools(
32
33
  // overrides are normally only set by the secret-routes handlers at runtime.
33
34
  await rehydratePlatformCredentials();
34
35
 
36
+ // Speech defaulting normally fires when the platform credentials land
37
+ // (secret-routes); evaluating it once per boot also covers assistants whose
38
+ // connection predates that trigger, so voice works for every connected
39
+ // assistant with no configured speech credentials. Idempotent, and detached
40
+ // per the startup philosophy — must not block boot.
41
+ void maybeDefaultSpeechToManaged();
42
+
35
43
  await initializeProviders(config);
36
44
  // initializeTools() also loads workspace tool overrides from
37
45
  // `<workspaceDir>/tools/` once core tools have settled, so they own
@@ -33,7 +33,7 @@ export type SubagentToolGateMode = "wire" | "execution";
33
33
  * but the definitions themselves are resolved from the live context: a
34
34
  * fork-retrospective wake hydrates clientless (`hasNoClient = true`, no
35
35
  * transport interface, no channel capabilities), which drops client-gated
36
- * tools (`host_*`, `ui_*`, `app_open`, `request_system_permission`) from
36
+ * tools (`host_*`, `ui_*`, `ask_question`, `request_system_permission`) from
37
37
  * the wire definitions and breaks the cache prefix anyway. When this pin is
38
38
  * set on the conversation, `isToolActiveForContext` reads `hasNoClient` and
39
39
  * `transportInterface` exclusively from the pin and treats channel
@@ -80,6 +80,12 @@ export interface ToolSetupContext extends SurfaceConversationContext {
80
80
  allowedToolNames?: Set<string>;
81
81
  /** When set, the subagent/wake tool allowlist (see {@link subagentToolGateMode}). */
82
82
  subagentAllowedTools?: Set<string>;
83
+ /**
84
+ * Collects tool names the subagent attempted but that
85
+ * {@link subagentAllowedTools} denied, for parent-visible reporting. The
86
+ * executor records into this Set (shared by reference with the Conversation).
87
+ */
88
+ subagentDeniedToolNames?: Set<string>;
83
89
  /**
84
90
  * How {@link subagentAllowedTools} is enforced. Absent or `"wire"` keeps
85
91
  * the historical behavior (definitions filtered before the provider
@@ -342,28 +342,37 @@ export async function persistWakeTriggerMessage(
342
342
  * inputs for tool-definition resolution — see {@link WakeToolContextPin}.
343
343
  * Its `requestOrigin`, when set, is stamped onto the conversation's per-turn
344
344
  * origin so the permission checker's origin-scoped auto-grants fire for the
345
- * wake. All are set and restored alongside the allowlist.
345
+ * wake. `preactivateSkillIds`, when non-empty, activates those skills' bundled
346
+ * tools in the turn's active set (`allowedToolNames`) for the wake so they are
347
+ * callable without a prior `skill_load`. All are set and restored alongside the
348
+ * allowlist.
346
349
  */
347
350
  export function scopeWakeAllowedTools(
348
351
  conversation: Conversation,
349
352
  tools: ReadonlySet<string>,
350
353
  gateMode: SubagentToolGateMode = "wire",
351
354
  toolContextPin?: WakeToolContextPin,
355
+ preactivateSkillIds?: readonly string[],
352
356
  ): () => void {
353
357
  const previous = conversation.subagentAllowedTools;
354
358
  const previousGateMode = conversation.subagentToolGateMode;
355
359
  const previousToolContextPin = conversation.toolContextPin;
356
360
  const previousRequestOrigin = conversation.currentTurnRequestOrigin;
361
+ const previousPreactivated = conversation.preactivatedSkillIds;
357
362
  conversation.setSubagentAllowedTools(new Set(tools));
358
363
  conversation.subagentToolGateMode = gateMode;
359
364
  conversation.toolContextPin = toolContextPin;
360
365
  if (toolContextPin?.requestOrigin !== undefined) {
361
366
  conversation.currentTurnRequestOrigin = toolContextPin.requestOrigin;
362
367
  }
368
+ if (preactivateSkillIds && preactivateSkillIds.length > 0) {
369
+ conversation.setPreactivatedSkillIds([...preactivateSkillIds]);
370
+ }
363
371
  return () => {
364
372
  conversation.setSubagentAllowedTools(previous);
365
373
  conversation.subagentToolGateMode = previousGateMode;
366
374
  conversation.toolContextPin = previousToolContextPin;
367
375
  conversation.currentTurnRequestOrigin = previousRequestOrigin;
376
+ conversation.setPreactivatedSkillIds(previousPreactivated);
368
377
  };
369
378
  }
@@ -173,6 +173,15 @@ export interface UserPromptSubmitInputContext {
173
173
  * user speech (e.g. title generation) should skip these turns.
174
174
  */
175
175
  readonly isHiddenPrompt?: boolean;
176
+ /**
177
+ * How the triggering message was initiated when it was sent on the
178
+ * user's behalf by the UI rather than typed by hand (`body.source` on
179
+ * `POST /v1/messages`, persisted as `metadata.userMessageSource`).
180
+ * Current value: `"nav_redirect"` — a navigation shortcut redirected the
181
+ * user into chat with a pre-filled message. Unset for hand-typed
182
+ * messages.
183
+ */
184
+ readonly messageSource?: string;
176
185
  /**
177
186
  * The user's original message list, immutable for the hook. Plugins
178
187
  * may snapshot or compare against this but MUST NOT mutate it.
@@ -11,6 +11,9 @@
11
11
  // Must be first — before any imports that resolve socket paths
12
12
  delete process.env.ASSISTANT_IPC_SOCKET_DIR;
13
13
 
14
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
15
+ import { tmpdir } from "node:os";
16
+ import { join } from "node:path";
14
17
  import { afterEach, beforeEach, expect, mock, test } from "bun:test";
15
18
 
16
19
  import { runAssistantCommandFull } from "../../cli/__tests__/run-assistant-command.js";
@@ -358,6 +361,93 @@ test("send --json returns delivery_id", async () => {
358
361
  expect(process.exitCode).toBe(0);
359
362
  });
360
363
 
364
+ test("send --attach reads the file and forwards base64 attachments to the platform", async () => {
365
+ const dir = mkdtempSync(join(tmpdir(), "email-attach-"));
366
+ const filePath = join(dir, "report.pdf");
367
+ const fileBytes = "pdf-bytes-here";
368
+ writeFileSync(filePath, fileBytes);
369
+
370
+ let sendBody: Record<string, unknown> | null = null;
371
+ mockFetchFn = async (path, init) => {
372
+ if (path.includes("/email-addresses/")) {
373
+ return new Response(
374
+ JSON.stringify({
375
+ results: [{ id: "addr-1", address: "mybot@example.com" }],
376
+ }),
377
+ { status: 200 },
378
+ );
379
+ }
380
+ // send endpoint — capture the forwarded payload
381
+ sendBody = JSON.parse(init?.body as string);
382
+ return new Response(
383
+ JSON.stringify({ delivery_id: "del_attach", status: "accepted" }),
384
+ { status: 200 },
385
+ );
386
+ };
387
+
388
+ try {
389
+ await runAssistantCommandFull(
390
+ "email",
391
+ "--json",
392
+ "send",
393
+ "user@example.com",
394
+ "-s",
395
+ "Report",
396
+ "-b",
397
+ "Attached",
398
+ "--attach",
399
+ filePath,
400
+ );
401
+
402
+ expect(sendBody).not.toBeNull();
403
+ expect(sendBody!.attachments).toEqual([
404
+ {
405
+ filename: "report.pdf",
406
+ content_type: "application/pdf",
407
+ content: Buffer.from(fileBytes).toString("base64"),
408
+ },
409
+ ]);
410
+ expect(process.exitCode).toBe(0);
411
+ } finally {
412
+ rmSync(dir, { recursive: true, force: true });
413
+ }
414
+ });
415
+
416
+ test("send with a missing --attach path fails without calling the platform", async () => {
417
+ let sendCalled = false;
418
+ mockFetchFn = async (path) => {
419
+ if (path.includes("/email-addresses/")) {
420
+ return new Response(
421
+ JSON.stringify({
422
+ results: [{ id: "addr-1", address: "mybot@example.com" }],
423
+ }),
424
+ { status: 200 },
425
+ );
426
+ }
427
+ sendCalled = true;
428
+ return new Response(
429
+ JSON.stringify({ delivery_id: "x", status: "accepted" }),
430
+ { status: 200 },
431
+ );
432
+ };
433
+
434
+ await runAssistantCommandFull(
435
+ "email",
436
+ "--json",
437
+ "send",
438
+ "user@example.com",
439
+ "-s",
440
+ "Report",
441
+ "-b",
442
+ "Attached",
443
+ "--attach",
444
+ join(tmpdir(), "does-not-exist-xyz.pdf"),
445
+ );
446
+
447
+ expect(process.exitCode).not.toBe(0);
448
+ expect(sendCalled).toBe(false);
449
+ });
450
+
361
451
  test("send: 402 billing error surfaces", async () => {
362
452
  mockFetchFn = async (path) => {
363
453
  if (path.includes("/email-addresses/")) {
@@ -9,12 +9,14 @@
9
9
  import { afterEach, describe, expect, test } from "bun:test";
10
10
 
11
11
  import {
12
+ getMockPersistentCallCount,
12
13
  mockGatewayIpc,
13
14
  resetMockGatewayIpc,
14
15
  } from "../__tests__/mock-gateway-ipc.js";
15
16
  import {
16
17
  ipcCall,
17
18
  ipcCallPersistent,
19
+ ipcClassifyRisk,
18
20
  ipcGetFeatureFlags,
19
21
  resetPersistentClient,
20
22
  } from "./gateway-client.js";
@@ -132,3 +134,60 @@ describe("ipcGetFeatureFlags", () => {
132
134
  expect(flags).toEqual({});
133
135
  });
134
136
  });
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // ipcClassifyRisk retry semantics
140
+ // ---------------------------------------------------------------------------
141
+
142
+ describe("ipcClassifyRisk retry", () => {
143
+ const CLASSIFY_OK = {
144
+ risk: "low",
145
+ reason: "safe",
146
+ matchType: "none",
147
+ scopeOptions: [],
148
+ };
149
+ const emptyParams = {} as unknown as Parameters<typeof ipcClassifyRisk>[0];
150
+
151
+ test("retries a transient failure and then succeeds", async () => {
152
+ mockGatewayIpc(null, {
153
+ failFirstN: 1,
154
+ results: { classify_risk: CLASSIFY_OK },
155
+ });
156
+
157
+ const result = await ipcClassifyRisk(emptyParams);
158
+ expect(result?.risk).toBe("low");
159
+ expect(getMockPersistentCallCount("classify_risk")).toBe(2);
160
+ });
161
+
162
+ test("returns undefined after exhausting retries (3 attempts)", async () => {
163
+ mockGatewayIpc(null, { error: true });
164
+
165
+ const result = await ipcClassifyRisk(emptyParams);
166
+ expect(result).toBeUndefined();
167
+ expect(getMockPersistentCallCount("classify_risk")).toBe(3);
168
+ });
169
+
170
+ test("does NOT retry a structured IpcCallError (fails after 1 attempt)", async () => {
171
+ mockGatewayIpc(null, {
172
+ failFirstN: 3,
173
+ ipcCallError: true,
174
+ results: { classify_risk: CLASSIFY_OK },
175
+ });
176
+
177
+ const result = await ipcClassifyRisk(emptyParams);
178
+ expect(result).toBeUndefined();
179
+ expect(getMockPersistentCallCount("classify_risk")).toBe(1);
180
+ });
181
+
182
+ test("stops retrying once the signal is aborted", async () => {
183
+ mockGatewayIpc(null, { error: true });
184
+ const controller = new AbortController();
185
+ controller.abort();
186
+
187
+ const result = await ipcClassifyRisk(emptyParams, controller.signal);
188
+ expect(result).toBeUndefined();
189
+ // An already-aborted signal short-circuits the retry loop after the first
190
+ // failed attempt rather than exhausting all three.
191
+ expect(getMockPersistentCallCount("classify_risk")).toBe(1);
192
+ });
193
+ });
@@ -12,6 +12,7 @@
12
12
 
13
13
  import {
14
14
  ipcCall as packageIpcCall,
15
+ IpcCallError,
15
16
  PersistentIpcClient as PackagePersistentIpcClient,
16
17
  } from "@vellumai/gateway-client/ipc-client";
17
18
 
@@ -20,6 +21,7 @@ import type {
20
21
  ClassifyRiskParams,
21
22
  } from "../permissions/ipc-risk-types.js";
22
23
  import { getLogger } from "../util/logger.js";
24
+ import { abortableSleep, computeRetryDelay } from "../util/retry.js";
23
25
  import { resolveIpcSocketPath } from "./socket-path.js";
24
26
 
25
27
  const log = getLogger("gateway-ipc-client");
@@ -138,6 +140,19 @@ export async function ipcGetVelayStatus(): Promise<VelayTunnelStatus | null> {
138
140
  };
139
141
  }
140
142
 
143
+ // classify_risk is an idempotent, side-effect-free read, so a transient gateway
144
+ // blip (socket dropped between calls, momentary unreachability) is safe to
145
+ // retry — the persistent client re-establishes its socket on the next call. The
146
+ // budget is deliberately sized for SUB-SECOND blips, not restart-class outages:
147
+ // each attempt uses a tighter timeout than the transport default so a wedged-
148
+ // but-reachable gateway cannot multiply the worst-case wait, and total latency
149
+ // against a hard-down gateway stays close to the single-attempt ceiling.
150
+ // Deterministic failures — a structured IpcCallError, or a returned-but-
151
+ // malformed payload — are NOT retried; they fail closed immediately.
152
+ const CLASSIFY_RISK_MAX_RETRIES = 2; // 3 attempts total
153
+ const CLASSIFY_RISK_RETRY_BASE_MS = 100; // ~[50,100] then ~[100,200] ms backoff
154
+ const CLASSIFY_RISK_ATTEMPT_TIMEOUT_MS = 1_500; // per-attempt cap (< 5s default)
155
+
141
156
  /**
142
157
  * Classify risk for a tool invocation via the gateway's persistent IPC
143
158
  * connection.
@@ -146,41 +161,69 @@ export async function ipcGetVelayStatus(): Promise<VelayTunnelStatus | null> {
146
161
  * classification is on the hot path for every tool invocation and the
147
162
  * persistent connection avoids per-call connect overhead.
148
163
  *
149
- * Returns `undefined` when the gateway is unreachable, the response is
150
- * malformed, or the call fails for any reason callers should throw
151
- * since there is no local fallback (gateway is a hard dependency).
164
+ * A transient transport failure is retried with bounded backoff (see the
165
+ * constants above) before giving up; a structured `IpcCallError` or a
166
+ * malformed payload fails fast without retrying. Returns `undefined` when the
167
+ * gateway is unreachable or the response is unusable after retries — callers
168
+ * should throw since there is no local fallback (gateway is a hard dependency).
169
+ * When a `signal` is supplied, retries stop as soon as it aborts.
152
170
  */
153
171
  export async function ipcClassifyRisk(
154
172
  params: ClassifyRiskParams,
173
+ signal?: AbortSignal,
155
174
  ): Promise<ClassificationResult | undefined> {
156
- try {
157
- const result = await ipcCallPersistent(
158
- "classify_risk",
159
- params as unknown as Record<string, unknown>,
160
- );
161
-
162
- // Validate the response has at minimum a `risk` field
163
- if (!result || typeof result !== "object" || Array.isArray(result)) {
164
- log.warn(
165
- { result },
166
- "ipcClassifyRisk: gateway returned non-object response",
175
+ for (let attempt = 0; ; attempt++) {
176
+ try {
177
+ const result = await ipcCallPersistent(
178
+ "classify_risk",
179
+ params as unknown as Record<string, unknown>,
180
+ CLASSIFY_RISK_ATTEMPT_TIMEOUT_MS,
167
181
  );
168
- return undefined;
169
- }
170
182
 
171
- const obj = result as Record<string, unknown>;
172
- if (typeof obj.risk !== "string") {
173
- log.warn(
174
- { result },
175
- "ipcClassifyRisk: gateway response missing 'risk' field",
176
- );
183
+ // Returned-but-malformed responses are deterministic, not transient:
184
+ // fail closed immediately (no retry).
185
+ if (!result || typeof result !== "object" || Array.isArray(result)) {
186
+ log.warn(
187
+ { result },
188
+ "ipcClassifyRisk: gateway returned non-object response",
189
+ );
190
+ return undefined;
191
+ }
192
+
193
+ const obj = result as Record<string, unknown>;
194
+ if (typeof obj.risk !== "string") {
195
+ log.warn(
196
+ { result },
197
+ "ipcClassifyRisk: gateway response missing 'risk' field",
198
+ );
199
+ return undefined;
200
+ }
201
+
202
+ return result as ClassificationResult;
203
+ } catch (err) {
204
+ // A structured gateway error means the gateway was reachable and
205
+ // deterministically rejected the request — not a transient blip.
206
+ const retryable = !(err instanceof IpcCallError);
207
+ if (
208
+ retryable &&
209
+ attempt < CLASSIFY_RISK_MAX_RETRIES &&
210
+ !signal?.aborted
211
+ ) {
212
+ log.warn(
213
+ { err, attempt },
214
+ "ipcClassifyRisk: transient IPC failure, retrying after backoff",
215
+ );
216
+ await abortableSleep(
217
+ computeRetryDelay(attempt, CLASSIFY_RISK_RETRY_BASE_MS),
218
+ signal,
219
+ );
220
+ if (!signal?.aborted) {
221
+ continue;
222
+ }
223
+ }
224
+ log.warn({ err }, "ipcClassifyRisk: persistent IPC call failed");
177
225
  return undefined;
178
226
  }
179
-
180
- return result as ClassificationResult;
181
- } catch (err) {
182
- log.warn({ err }, "ipcClassifyRisk: persistent IPC call failed");
183
- return undefined;
184
227
  }
185
228
  }
186
229
 
@@ -4,6 +4,7 @@ import type {
4
4
  VoiceTurnCallbacks,
5
5
  VoiceTurnOptions,
6
6
  } from "../../calls/voice-session-bridge.js";
7
+ import { VOICE_NO_SETUP_FLOWS_RULE } from "../../calls/voice-session-bridge.js";
7
8
  import type {
8
9
  StreamingTranscriber,
9
10
  SttStreamServerEvent,
@@ -129,7 +130,9 @@ async function waitFor(
129
130
  message = "Timed out waiting for live voice event test condition",
130
131
  ): Promise<void> {
131
132
  for (let attempt = 0; attempt < 60; attempt += 1) {
132
- if (predicate()) return;
133
+ if (predicate()) {
134
+ return;
135
+ }
133
136
  await new Promise((resolve) => setTimeout(resolve, 5));
134
137
  }
135
138
  throw new Error(message);
@@ -246,7 +249,16 @@ describe("LiveVoiceSession archive and metrics events", () => {
246
249
 
247
250
  await startReleasedTurn(session);
248
251
  expect(startVoiceTurn.mock.calls[0]?.[0]).toMatchObject({
249
- approvalMode: "local-live-voice",
252
+ userMessageChannel: "vellum",
253
+ assistantMessageChannel: "vellum",
254
+ userMessageInterface: "macos",
255
+ assistantMessageInterface: "macos",
256
+ // Pins the full production control prompt, including the no-UI rule
257
+ // (voice turns are non-interactive — everything is conveyed in speech)
258
+ // and the shared no-setup-flows rule (no OAuth/browser flows mid-call).
259
+ voiceControlPrompt:
260
+ "You are speaking in a local live voice session. Keep replies brief and conversational. You cannot display cards, forms, or any on-screen UI during the call — convey everything in speech. " +
261
+ VOICE_NO_SETUP_FLOWS_RULE,
250
262
  });
251
263
  callbacks?.assistant_text_delta?.(makeTextDelta("Hello there."));
252
264
  callbacks?.message_complete?.(makeMessageComplete());