@vellumai/assistant 0.11.1 → 0.11.2-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (318) hide show
  1. package/Dockerfile +1 -3
  2. package/README.md +1 -1
  3. package/eslint.config.mjs +28 -8
  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__/stripe-currency.test.ts +37 -0
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -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__/stripe-currency.test.ts +37 -0
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  10. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  12. package/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  13. package/node_modules/@vellumai/slack-text/src/index.test.ts +48 -0
  14. package/node_modules/@vellumai/slack-text/src/index.ts +25 -6
  15. package/openapi.yaml +292 -5
  16. package/package.json +1 -1
  17. package/src/__tests__/agent-loop-resume-interrupted.test.ts +223 -0
  18. package/src/__tests__/byok-default-profile-ensure.test.ts +43 -0
  19. package/src/__tests__/cli-logger-boundary-guard.test.ts +87 -0
  20. package/src/__tests__/compaction-events.test.ts +139 -2
  21. package/src/__tests__/config-loader-backfill.test.ts +35 -3
  22. package/src/__tests__/config-schema.test.ts +1 -0
  23. package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +30 -0
  24. package/src/__tests__/conversation-agent-loop.test.ts +180 -7
  25. package/src/__tests__/conversation-error.test.ts +211 -4
  26. package/src/__tests__/conversation-options-turn-scoped-transport.test.ts +64 -0
  27. package/src/__tests__/conversation-queue.test.ts +126 -0
  28. package/src/__tests__/conversation-retry-route.test.ts +46 -7
  29. package/src/__tests__/conversation-slash.test.ts +4 -2
  30. package/src/__tests__/conversation-summarize-route.test.ts +7 -1
  31. package/src/__tests__/conversation-summarize-up-to.test.ts +71 -5
  32. package/src/__tests__/document-create-dedupe.test.ts +2 -1
  33. package/src/__tests__/document-find-replace.test.ts +2 -1
  34. package/src/__tests__/document-tool-security.test.ts +2 -1
  35. package/src/__tests__/document-update-default-surface.test.ts +2 -1
  36. package/src/__tests__/document-workspace-file.test.ts +467 -0
  37. package/src/__tests__/emit-signal-routing-intent.test.ts +290 -49
  38. package/src/__tests__/guardian-card-withdrawal.test.ts +91 -3
  39. package/src/__tests__/http-user-message-parity.test.ts +66 -0
  40. package/src/__tests__/list-messages-attachments.test.ts +87 -0
  41. package/src/__tests__/list-messages-provider-error.test.ts +143 -0
  42. package/src/__tests__/list-messages-system-card.test.ts +100 -0
  43. package/src/__tests__/llm-resolver.test.ts +19 -9
  44. package/src/__tests__/managed-profile-guard.test.ts +23 -0
  45. package/src/__tests__/notification-platform-adapter.test.ts +130 -2
  46. package/src/__tests__/notification-telegram-adapter.test.ts +6 -0
  47. package/src/__tests__/notification-vellum-adapter.test.ts +45 -0
  48. package/src/__tests__/plugin-api-model-profiles.test.ts +10 -1
  49. package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
  50. package/src/__tests__/provider-error-scenarios.test.ts +140 -0
  51. package/src/__tests__/provider-send-message-override-profile.test.ts +95 -0
  52. package/src/__tests__/run-conversation-turn-persistence.test.ts +66 -3
  53. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +209 -0
  54. package/src/__tests__/skills.test.ts +27 -0
  55. package/src/__tests__/slack-channels-routes.test.ts +0 -2
  56. package/src/__tests__/slack-share-routes.test.ts +0 -3
  57. package/src/__tests__/slack-users-routes.test.ts +0 -2
  58. package/src/__tests__/subagent-tools.test.ts +11 -0
  59. package/src/__tests__/tool-preview-lifecycle.test.ts +58 -0
  60. package/src/__tests__/tool-result-spool.test.ts +5 -4
  61. package/src/__tests__/turn-boundary-resolution.test.ts +53 -0
  62. package/src/__tests__/turn-events-store.test.ts +26 -0
  63. package/src/__tests__/ui-visual-surface.test.ts +695 -0
  64. package/src/__tests__/unified-turn-context-visible-app.test.ts +99 -0
  65. package/src/__tests__/visible-app-context.test.ts +189 -0
  66. package/src/__tests__/workspace-git-service.test.ts +173 -33
  67. package/src/__tests__/workspace-migration-137-repair-retired-fireworks-minimax-model-id.test.ts +157 -0
  68. package/src/__tests__/workspace-migration-138-backfill-home-feed-titles.test.ts +373 -0
  69. package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +137 -0
  70. package/src/agent/loop.ts +36 -7
  71. package/src/api/events/context-window-usage.ts +31 -0
  72. package/src/api/events/notification-intent.ts +8 -0
  73. package/src/api/events/ui-surface-pending.ts +35 -0
  74. package/src/api/index.ts +14 -0
  75. package/src/api/responses/conversation-message.ts +25 -4
  76. package/src/api/surfaces.ts +90 -1
  77. package/src/approvals/guardian-card-withdrawal.ts +66 -31
  78. package/src/approvals/guardian-decision-primitive.ts +4 -0
  79. package/src/calls/__tests__/call-setup-router.test.ts +156 -26
  80. package/src/calls/__tests__/voice-session-bridge.test.ts +201 -12
  81. package/src/calls/call-setup-router.ts +80 -36
  82. package/src/calls/voice-session-bridge.ts +173 -26
  83. package/src/cli/commands/inference.help.ts +3 -3
  84. package/src/cli/commands/notifications.help.ts +3 -2
  85. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +42 -128
  86. package/src/cli/commands/platform/__tests__/credits.test.ts +9 -78
  87. package/src/cli/commands/platform/__tests__/helpers.ts +90 -0
  88. package/src/cli/commands/platform/__tests__/invoices.test.ts +238 -0
  89. package/src/cli/commands/platform/__tests__/plans.test.ts +9 -88
  90. package/src/cli/commands/platform/__tests__/status.test.ts +12 -87
  91. package/src/cli/commands/platform/__tests__/subscription.test.ts +9 -86
  92. package/src/cli/commands/platform/index.help.ts +92 -0
  93. package/src/cli/commands/platform/index.ts +7 -0
  94. package/src/cli/commands/platform/invoices.ts +132 -0
  95. package/src/cli/commands/usage.help.ts +1 -1
  96. package/src/cli/lib/list-installed-plugins.ts +2 -1
  97. package/src/config/__tests__/default-profile-catalog.test.ts +7 -10
  98. package/src/config/__tests__/deployment-context-defaults.test.ts +27 -5
  99. package/src/config/assistant-feature-flags.ts +7 -2
  100. package/src/config/bundled-skills/app-builder/SKILL.md +3 -2
  101. package/src/config/bundled-skills/subagent/SKILL.md +3 -1
  102. package/src/config/bundled-skills/subagent/TOOLS.json +3 -3
  103. package/src/config/bundled-skills/visualize/SKILL.md +163 -0
  104. package/src/config/call-site-defaults.ts +3 -2
  105. package/src/config/default-profile-catalog.ts +75 -70
  106. package/src/config/default-profile-names.ts +13 -28
  107. package/src/config/env-registry.ts +1 -0
  108. package/src/config/feature-flag-registry.json +16 -0
  109. package/src/config/llm-resolver.ts +21 -5
  110. package/src/config/loader.ts +18 -11
  111. package/src/config/schemas/llm.ts +1 -9
  112. package/src/config/schemas/memory-retrospective.ts +9 -0
  113. package/src/config/schemas/monitoring.ts +28 -2
  114. package/src/config/schemas/workspace-git.ts +15 -0
  115. package/src/config/seed-inference-profiles.ts +19 -5
  116. package/src/context/post-turn-tool-result-truncation.ts +2 -2
  117. package/src/conversations/__tests__/message-consolidation.test.ts +54 -0
  118. package/src/conversations/message-consolidation.ts +17 -15
  119. package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +182 -0
  120. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +181 -0
  121. package/src/daemon/conversation-agent-loop-handlers.ts +90 -7
  122. package/src/daemon/conversation-agent-loop.ts +107 -22
  123. package/src/daemon/conversation-error.ts +169 -48
  124. package/src/daemon/conversation-messaging.ts +87 -4
  125. package/src/daemon/conversation-process.ts +57 -45
  126. package/src/daemon/conversation-runtime-assembly.ts +57 -0
  127. package/src/daemon/conversation-store.ts +36 -1
  128. package/src/daemon/conversation-surfaces.ts +68 -6
  129. package/src/daemon/conversation-turn-finalize.ts +76 -18
  130. package/src/daemon/conversation.ts +122 -27
  131. package/src/daemon/lifecycle.ts +9 -0
  132. package/src/daemon/message-types/conversations.ts +8 -0
  133. package/src/daemon/message-types/surfaces.ts +3 -0
  134. package/src/documents/document-store.ts +247 -10
  135. package/src/home/__tests__/feed-types.test.ts +43 -8
  136. package/src/home/__tests__/feed-writer.test.ts +59 -0
  137. package/src/home/feed-types.ts +34 -7
  138. package/src/home/feed-writer.ts +15 -6
  139. package/src/live-voice/__tests__/activity-label.test.ts +95 -0
  140. package/src/live-voice/__tests__/live-activity-reporter.test.ts +86 -5
  141. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +310 -25
  142. package/src/live-voice/__tests__/live-voice-events.test.ts +4 -4
  143. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +8 -4
  144. package/src/live-voice/__tests__/live-voice-vad.test.ts +9 -1
  145. package/src/live-voice/activity-label.ts +169 -0
  146. package/src/live-voice/live-activity-reporter.ts +35 -6
  147. package/src/live-voice/live-voice-session.ts +282 -41
  148. package/src/live-voice/protocol.ts +35 -0
  149. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +84 -2
  150. package/src/messaging/providers/slack/adapter.ts +86 -19
  151. package/src/messaging/providers/slack/api.test.ts +85 -1
  152. package/src/messaging/providers/slack/api.ts +121 -288
  153. package/src/messaging/providers/slack/client.ts +20 -251
  154. package/src/messaging/providers/slack/send.test.ts +4 -9
  155. package/src/messaging/providers/slack/send.ts +1 -1
  156. package/src/messaging/providers/slack/types.ts +5 -0
  157. package/src/messaging/providers/slack/web-api-transport.test.ts +181 -0
  158. package/src/messaging/providers/slack/web-api-transport.ts +367 -0
  159. package/src/messaging/providers/slack/withdraw.ts +6 -14
  160. package/src/messaging/providers/telegram-bot/send.test.ts +31 -1
  161. package/src/messaging/providers/telegram-bot/send.ts +23 -2
  162. package/src/messaging/providers/telegram-bot/withdraw.test.ts +152 -0
  163. package/src/messaging/providers/telegram-bot/withdraw.ts +166 -0
  164. package/src/monitoring/__tests__/db-integrity-sample.test.ts +18 -4
  165. package/src/monitoring/__tests__/file-descriptors.test.ts +144 -0
  166. package/src/monitoring/file-descriptors.ts +262 -0
  167. package/src/monitoring/process-memory.ts +4 -11
  168. package/src/monitoring/resource-sampler.ts +5 -0
  169. package/src/monitoring/worker.ts +12 -0
  170. package/src/notifications/__tests__/assistant-reply-producer.test.ts +740 -0
  171. package/src/notifications/__tests__/broadcaster.test.ts +347 -7
  172. package/src/notifications/__tests__/copy-composer.test.ts +53 -3
  173. package/src/notifications/__tests__/decision-engine.test.ts +349 -29
  174. package/src/notifications/__tests__/deterministic-checks.test.ts +21 -0
  175. package/src/notifications/__tests__/edit-notification.test.ts +330 -0
  176. package/src/notifications/__tests__/guardian-delivery-recorder.test.ts +71 -0
  177. package/src/notifications/__tests__/home-feed-side-effect.test.ts +170 -12
  178. package/src/notifications/adapters/macos.ts +3 -0
  179. package/src/notifications/adapters/platform.ts +90 -14
  180. package/src/notifications/adapters/telegram.ts +6 -4
  181. package/src/notifications/assistant-reply-producer.ts +219 -0
  182. package/src/notifications/broadcaster.ts +523 -317
  183. package/src/notifications/copy-composer.ts +27 -6
  184. package/src/notifications/decision-engine.ts +154 -77
  185. package/src/notifications/deterministic-checks.ts +6 -7
  186. package/src/notifications/edit-notification.ts +8 -4
  187. package/src/notifications/emit-signal.ts +49 -16
  188. package/src/notifications/guardian-delivery-recorder.ts +8 -2
  189. package/src/notifications/home-feed-side-effect.ts +43 -6
  190. package/src/notifications/notification-utils.ts +39 -4
  191. package/src/notifications/signal.ts +10 -0
  192. package/src/notifications/types.ts +17 -0
  193. package/src/persistence/bookmark-crud.ts +18 -9
  194. package/src/persistence/conversation-attention-store.ts +26 -0
  195. package/src/persistence/conversation-crud.ts +79 -32
  196. package/src/persistence/conversation-queries.ts +30 -15
  197. package/src/persistence/conversation-title-service.ts +5 -170
  198. package/src/persistence/conversation-types.ts +192 -1
  199. package/src/persistence/db-init.ts +14 -1
  200. package/src/persistence/db-maintenance.ts +5 -4
  201. package/src/persistence/embeddings/__tests__/plugin-index-qdrant-init.test.ts +219 -0
  202. package/src/persistence/embeddings/__tests__/plugin-index.test.ts +22 -0
  203. package/src/persistence/embeddings/__tests__/worker-script-version.test.ts +76 -0
  204. package/src/persistence/embeddings/embedding-local.ts +2 -1
  205. package/src/persistence/embeddings/embedding-runtime-manager.ts +29 -5
  206. package/src/persistence/embeddings/plugin-index.ts +84 -12
  207. package/src/persistence/migrations/360-add-document-workspace-path.test.ts +110 -0
  208. package/src/persistence/migrations/360-add-document-workspace-path.ts +39 -0
  209. package/src/persistence/planner-statistics.ts +14 -0
  210. package/src/persistence/schema/documents.ts +26 -11
  211. package/src/persistence/steps.ts +2 -0
  212. package/src/platform/client.test.ts +172 -2
  213. package/src/platform/client.ts +178 -55
  214. package/src/plugin-api/conversation-turn.ts +13 -0
  215. package/src/plugin-api/model-profiles.test.ts +6 -2
  216. package/src/plugins/defaults/memory/__tests__/bookmark-crud.test.ts +34 -0
  217. package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +22 -0
  218. package/src/plugins/defaults/memory/__tests__/jobs-store-enqueue-gate.test.ts +11 -0
  219. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +199 -0
  220. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +98 -1
  221. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +105 -1
  222. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +24 -2
  223. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +524 -0
  224. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +1 -1
  225. package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +9 -4
  226. package/src/plugins/defaults/memory/host-utils.ts +5 -0
  227. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +105 -1
  228. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +64 -5
  229. package/src/plugins/defaults/memory/memory-retrospective-job.ts +36 -1
  230. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +12 -3
  231. package/src/plugins/defaults/memory/src/__tests__/memory-v2-simulate-route.test.ts +1 -0
  232. package/src/plugins/defaults/memory/substrate/__tests__/skill-store.test.ts +195 -1
  233. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +1 -1
  234. package/src/plugins/defaults/memory/substrate/skill-store.ts +167 -29
  235. package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +131 -0
  236. package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +1 -0
  237. package/src/plugins/defaults/memory/v2/activation-log-store.ts +4 -0
  238. package/src/plugins/defaults/memory/v2/injection.ts +48 -3
  239. package/src/plugins/defaults/memory/v2/rerank-local.ts +6 -2
  240. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -1
  241. package/src/plugins/defaults/platform-hosted/routes/reengage.ts +1 -1
  242. package/src/plugins/defaults/turn-context/injectors.ts +1 -0
  243. package/src/plugins/defaults/turn-context/unified-turn-context.ts +26 -0
  244. package/src/plugins/types.ts +17 -0
  245. package/src/prompts/templates/system-sections.ts +2 -2
  246. package/src/providers/__tests__/dispatch-connection-routing.test.ts +43 -2
  247. package/src/providers/__tests__/vellum-mismatch-routing.test.ts +8 -0
  248. package/src/providers/call-site-routing.ts +77 -14
  249. package/src/providers/connection-resolution.ts +42 -2
  250. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +127 -1
  251. package/src/providers/inference/adapter-factory.ts +70 -12
  252. package/src/providers/model-catalog.ts +0 -11
  253. package/src/providers/model-intents.ts +11 -0
  254. package/src/providers/registry.ts +8 -0
  255. package/src/providers/retry.ts +85 -17
  256. package/src/providers/types.ts +8 -1
  257. package/src/runtime/__tests__/agent-wake.test.ts +181 -0
  258. package/src/runtime/agent-wake.ts +132 -9
  259. package/src/runtime/channel-approval-types.ts +25 -0
  260. package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +2 -2
  261. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +11 -0
  262. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +121 -9
  263. package/src/runtime/routes/__tests__/platform-invoice-routes.test.ts +392 -0
  264. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +7 -0
  265. package/src/runtime/routes/__tests__/workspace-commit-routes.test.ts +63 -0
  266. package/src/runtime/routes/consolidation-routes.ts +1 -1
  267. package/src/runtime/routes/conversation-management-routes.ts +18 -6
  268. package/src/runtime/routes/conversation-query-routes.ts +14 -23
  269. package/src/runtime/routes/conversation-routes.ts +113 -8
  270. package/src/runtime/routes/credential-routes.ts +1 -5
  271. package/src/runtime/routes/documents-routes.ts +207 -12
  272. package/src/runtime/routes/identity-routes.ts +3 -93
  273. package/src/runtime/routes/inbound-stages/admission-policy.test.ts +31 -1
  274. package/src/runtime/routes/inbound-stages/admission-policy.ts +18 -0
  275. package/src/runtime/routes/inference-provider-connection-routes.ts +52 -3
  276. package/src/runtime/routes/notification-routes.ts +4 -1
  277. package/src/runtime/routes/platform-routes.ts +238 -3
  278. package/src/runtime/routes/playground/guard.ts +1 -2
  279. package/src/runtime/routes/slack-channel-routes.ts +2 -4
  280. package/src/runtime/routes/workspace-commit-routes.ts +49 -2
  281. package/src/runtime/routes/workspace-routes.ts +11 -12
  282. package/src/runtime/routes/workspace-utils.ts +49 -2
  283. package/src/runtime/services/conversation-serializer.ts +2 -4
  284. package/src/subagent/__tests__/consult-context-gating.test.ts +98 -0
  285. package/src/subagent/__tests__/consult-context-skills.test.ts +82 -0
  286. package/src/subagent/__tests__/consult-context.test.ts +84 -0
  287. package/src/subagent/__tests__/consult-prompt.test.ts +36 -0
  288. package/src/subagent/consult-context.ts +410 -0
  289. package/src/subagent/consult-prompt.ts +38 -5
  290. package/src/subagent/manager.ts +9 -4
  291. package/src/subagent/types.ts +8 -0
  292. package/src/telemetry/telemetry-event-sources.ts +7 -0
  293. package/src/telemetry/telemetry-wire-source.json +1 -1
  294. package/src/telemetry/telemetry-wire.generated.ts +1 -0
  295. package/src/telemetry/turn-events-store.ts +30 -1
  296. package/src/telemetry/types.ts +24 -0
  297. package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
  298. package/src/tools/browser/pinned-tabs.ts +3 -1
  299. package/src/tools/subagent/spawn.ts +23 -1
  300. package/src/tools/terminal/safe-env.ts +1 -0
  301. package/src/tools/ui-surface/definitions.ts +39 -1
  302. package/src/tools/ui-surface/surface-shape-docs.ts +38 -4
  303. package/src/tools/ui-surface/visual-validation.ts +787 -0
  304. package/src/tools/workflows/run-workflow.test.ts +1 -0
  305. package/src/util/__tests__/short-title.test.ts +229 -0
  306. package/src/util/__tests__/worker-compute.test.ts +65 -0
  307. package/src/util/cgroup-cpu.ts +93 -0
  308. package/src/util/errors.ts +27 -0
  309. package/src/util/process-tree.ts +19 -0
  310. package/src/util/short-title.ts +189 -0
  311. package/src/util/worker-compute.ts +83 -0
  312. package/src/workspace/byok-default-profile-ensure.ts +25 -15
  313. package/src/workspace/git-service.ts +79 -30
  314. package/src/workspace/migrations/137-repair-retired-fireworks-minimax-model-id.ts +131 -0
  315. package/src/workspace/migrations/138-backfill-home-feed-titles.ts +179 -0
  316. package/src/workspace/migrations/139-clear-renamed-cost-profile-label.ts +92 -0
  317. package/src/workspace/migrations/registry.ts +6 -0
  318. package/src/plugins/defaults/memory/substrate/constants.ts +0 -8
@@ -20,6 +20,7 @@ import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
20
20
  import {
21
21
  type AdmissionPolicyResult,
22
22
  enforceAdmissionPolicy,
23
+ trustedContactPromotionClearsFloor,
23
24
  } from "../runtime/routes/inbound-stages/admission-policy.js";
24
25
  import {
25
26
  actorTrustContextFromVerdict,
@@ -260,13 +261,20 @@ export async function routeSetup(ctx: SetupContext): Promise<{
260
261
  : await getPendingSession("phone");
261
262
 
262
263
  // An admission floor is "active" only when a policy applies and no pending
263
- // verification challenge is in flight. While active, the floor IS the access
264
- // decision: an admitted caller bypasses the legacy identity flows
265
- // (unverified_caller / name_capture) and connects directly. When inactive
266
- // (null policy, flag off, exempt channel, or a pending challenge), those
267
- // legacy flows are preserved unchanged.
264
+ // verification challenge is in flight. While active, an admitted caller
265
+ // bypasses the legacy identity flows (unverified_caller / name_capture) and
266
+ // connects directly. When inactive (null policy, flag off, exempt channel,
267
+ // or a pending challenge), those legacy flows are preserved unchanged.
268
268
  const floorActive = ctx.admissionPolicy != null && !pendingChallenge;
269
269
 
270
+ // Whether a guardian approval could lift a below-floor caller past this
271
+ // floor. On floors it can clear, a floor deny routes the caller into the
272
+ // guardian access-request flow instead of hanging up on them (see the
273
+ // unknown-caller branch below).
274
+ const approvalCouldClearFloor =
275
+ ctx.admissionPolicy != null &&
276
+ trustedContactPromotionClearsFloor(ctx.admissionPolicy);
277
+
270
278
  // Inbound admission floor verdict; defaults to admitted when inactive.
271
279
  const floorVerdict = floorActive
272
280
  ? enforceAdmissionPolicy({
@@ -303,6 +311,36 @@ export async function routeSetup(ctx: SetupContext): Promise<{
303
311
  };
304
312
  };
305
313
 
314
+ // Members whose channel the guardian set to `policy: 'deny'`. This gate is
315
+ // trust-class independent and runs ahead of every inbound flow below: the
316
+ // gateway derives trust class from channel STATUS alone, so a denied channel
317
+ // still resolves to `unverified_contact` (status `unverified`/`pending`) or
318
+ // `trusted_contact` (status `active`). Gating per class would leave the deny
319
+ // unenforced for whichever classes the branch order happened to exclude.
320
+ //
321
+ // It also outranks an active voice invite: an explicit channel-level deny is
322
+ // the guardian's standing ruling, and an invite must not redeem past it into
323
+ // a trusted contact.
324
+ if (actorTrust.memberRecord?.policy === "deny") {
325
+ log.info(
326
+ {
327
+ callSessionId: ctx.callSessionId,
328
+ from: ctx.from,
329
+ channelId: actorTrust.memberRecord.channel.id,
330
+ trustClass: actorTrust.trustClass,
331
+ },
332
+ "Inbound voice ACL: member policy deny",
333
+ );
334
+ return {
335
+ outcome: {
336
+ action: "deny",
337
+ message: "This number is not authorized to use this assistant.",
338
+ logReason: "Inbound voice ACL: member policy deny",
339
+ },
340
+ resolved,
341
+ };
342
+ }
343
+
306
344
  if (
307
345
  (actorTrust.trustClass === "unknown" ||
308
346
  actorTrust.trustClass === "unverified_contact") &&
@@ -350,19 +388,46 @@ export async function routeSetup(ctx: SetupContext): Promise<{
350
388
  };
351
389
  }
352
390
 
353
- // When a floor is active it is the access decision: an admitted caller
354
- // connects directly (skipping unverified_caller / name_capture), and a
355
- // below-floor caller is denied. Invites (handled above) bypass the floor
356
- // as an explicit grant. When the floor is inactive (null policy), fall
357
- // through to the legacy identity flows below.
391
+ // A caller who clears the floor connects directly, skipping
392
+ // unverified_caller / name_capture. Invites (handled above) bypass the
393
+ // floor as an explicit grant.
394
+ //
395
+ // A caller BELOW the floor is not simply hung up on: when a guardian
396
+ // approval could admit them, fall through to the legacy identity flows
397
+ // below, which capture the caller's name, raise a guardian access request,
398
+ // and hold the line for the decision. The floor still governs the outcome,
399
+ // because the flow only ever admits a caller the guardian approved into a
400
+ // trusted contact.
401
+ //
402
+ // Two cases keep the hard deny. Floors a trusted-contact promotion could
403
+ // not clear (`guardian_only`, `no_one`), where an approval flow would
404
+ // promise a decision that cannot admit the caller. And `blocked` /
405
+ // `revoked` members, who should hear the denial rather than an invitation
406
+ // to identify themselves. (The third governance signal, a channel
407
+ // `policy` of `deny`, is gated ahead of this whole branch.)
358
408
  if (floorActive) {
359
- if (!floorVerdict.admitted) {
409
+ if (floorVerdict.admitted) {
410
+ return {
411
+ outcome: { action: "normal_call" as const, isInbound: true },
412
+ resolved,
413
+ };
414
+ }
415
+ if (
416
+ !approvalCouldClearFloor ||
417
+ floorVerdict.reason === "member_blocked" ||
418
+ floorVerdict.reason === "member_revoked"
419
+ ) {
360
420
  return floorDeny(floorVerdict);
361
421
  }
362
- return {
363
- outcome: { action: "normal_call" as const, isInbound: true },
364
- resolved,
365
- };
422
+ log.info(
423
+ {
424
+ callSessionId: ctx.callSessionId,
425
+ from: ctx.from,
426
+ trustClass: actorTrust.trustClass,
427
+ effectivePolicy: floorVerdict.effectivePolicy,
428
+ },
429
+ "Inbound voice ACL: caller below admission floor, routing to guardian access request",
430
+ );
366
431
  }
367
432
 
368
433
  // Known caller whose channel hasn't passed verification yet —
@@ -413,27 +478,6 @@ export async function routeSetup(ctx: SetupContext): Promise<{
413
478
  };
414
479
  }
415
480
 
416
- // Members with policy: 'deny'
417
- if (actorTrust.memberRecord?.policy === "deny") {
418
- log.info(
419
- {
420
- callSessionId: ctx.callSessionId,
421
- from: ctx.from,
422
- channelId: actorTrust.memberRecord.channel.id,
423
- trustClass: actorTrust.trustClass,
424
- },
425
- "Inbound voice ACL: member policy deny",
426
- );
427
- return {
428
- outcome: {
429
- action: "deny",
430
- message: "This number is not authorized to use this assistant.",
431
- logReason: "Inbound voice ACL: member policy deny",
432
- },
433
- resolved,
434
- };
435
- }
436
-
437
481
  // Guardian verification challenge
438
482
  if (pendingChallenge) {
439
483
  return {
@@ -40,6 +40,7 @@ import { getCurrentSeq } from "../runtime/assistant-stream-state.js";
40
40
  import { publishConversationMessagesChanged } from "../runtime/sync/resource-sync-events.js";
41
41
  import { computeToolApprovalDigest } from "../security/tool-approval-digest.js";
42
42
  import { getAllTools } from "../tools/registry.js";
43
+ import { sensitiveToolReach } from "../tools/tool-approval-handler.js";
43
44
  import { createAbortReason } from "../util/abort-reasons.js";
44
45
  import { getLogger } from "../util/logger.js";
45
46
  import { truncate } from "../util/truncate.js";
@@ -286,6 +287,28 @@ export interface VoiceTurnOptions {
286
287
  onError?: (message: string) => void;
287
288
  /** Event-name callbacks used by non-phone voice clients. */
288
289
  callbacks?: VoiceTurnCallbacks;
290
+ /**
291
+ * Called when this turn leaves a confirmation for the user to answer instead
292
+ * of deciding it, so the client can put the prompt where they can see it.
293
+ *
294
+ * A voice client renders its call as something that covers the app (the
295
+ * live-voice room is a full-screen overlay), which is fine while the call is
296
+ * the only thing happening and wrong the moment the turn is waiting on a
297
+ * decision: the card is on screen, behind the call. The bridge cannot reach
298
+ * a client's own surfaces, so it says *that a decision is waiting* and the
299
+ * client decides what to do about it.
300
+ */
301
+ onApprovalPending?: (requestId: string) => void;
302
+ /**
303
+ * Called when the last pending confirmation this turn was waiting on is
304
+ * decided, however it was decided (the user answered, it timed out, a newer
305
+ * message superseded it). Paired with {@link onApprovalPending} so a client
306
+ * that changed its presentation for the wait can change it back.
307
+ *
308
+ * Fires on the *last* one, not each: a turn waiting on two decisions is
309
+ * still waiting after the first is answered.
310
+ */
311
+ onApprovalsResolved?: () => void;
289
312
  /** Optional AbortSignal for external cancellation (e.g. barge-in). */
290
313
  signal?: AbortSignal;
291
314
  /**
@@ -367,12 +390,27 @@ export interface VoiceTurnHandle {
367
390
  * provides assistant identity) and guardian context (injected separately).
368
391
  */
369
392
  /**
370
- * Steering shared by every voice channel. Voice turns exclude the ui-surface
371
- * tools, but the model can still reach OAuth/sign-in flows through shell or
372
- * CLI tools (e.g. `assistant oauth connect`), which open a browser window
373
- * mid-call that the caller may be unable to see or complete. Tell it to speak
374
- * the limitation and defer the flow to text chat instead.
393
+ * Steering shared by every voice channel. A sign-in flow opens a browser
394
+ * window mid-call that the caller may be unable to see or complete, whether it
395
+ * is reached through a ui-surface tool or through shell and CLI tools (e.g.
396
+ * `assistant oauth connect`). Tell the model to speak the limitation and defer
397
+ * the flow to text chat instead.
398
+ *
399
+ * This outlives the ui-surface restriction it was written alongside: a
400
+ * live-voice call can now show surfaces, but a browser window handing control
401
+ * to a third party mid-call is a different problem, and one a minimized room
402
+ * does not solve.
375
403
  */
404
+ /**
405
+ * How long a live-voice call waits on an approval before deciding it itself.
406
+ *
407
+ * Long enough to pick the phone up and read the card, short enough that a turn
408
+ * cannot sit blocked for the rest of the call. The fallback is the decision
409
+ * this channel made before it prompted at all, so the worst case of nobody
410
+ * answering is exactly the old behavior, arrived at late.
411
+ */
412
+ const VOICE_APPROVAL_TIMEOUT_MS = 45_000;
413
+
376
414
  export const VOICE_NO_SETUP_FLOWS_RULE =
377
415
  "Never start account connections, OAuth or sign-in flows, or any other action that opens a browser window or needs the user's screen during this call — not even through shell or CLI tools. If the task needs one, say so briefly and offer to finish it in text chat after the call.";
378
416
 
@@ -851,7 +889,37 @@ export async function startVoiceTurn(
851
889
  // (tracked via `clientCallbackInstalled`); otherwise cleanup would detach
852
890
  // an active sender installed by a prior turn.
853
891
  let clientCallbackInstalled = false;
892
+ /**
893
+ * Confirmations this voice turn left pending for the user to answer, and the
894
+ * timers that stop them waiting forever.
895
+ *
896
+ * A call is a poor place to block: the user may have put the phone in a
897
+ * pocket, and a turn that waits indefinitely is a session that looks wedged.
898
+ * Each pending request therefore carries a deadline, after which it resolves
899
+ * the way this channel resolved everything before it prompted at all.
900
+ */
901
+ const pendingVoiceApprovals = new Map<
902
+ string,
903
+ ReturnType<typeof setTimeout>
904
+ >();
905
+ const settleVoiceApproval = (requestId: string): void => {
906
+ const timer = pendingVoiceApprovals.get(requestId);
907
+ if (timer === undefined) {
908
+ // Not one of ours: other interactions (secrets, host-proxy requests)
909
+ // resolve through the same event.
910
+ return;
911
+ }
912
+ clearTimeout(timer);
913
+ pendingVoiceApprovals.delete(requestId);
914
+ if (pendingVoiceApprovals.size === 0) {
915
+ opts.onApprovalsResolved?.();
916
+ }
917
+ };
854
918
  const cleanup = () => {
919
+ for (const timer of pendingVoiceApprovals.values()) {
920
+ clearTimeout(timer);
921
+ }
922
+ pendingVoiceApprovals.clear();
855
923
  conversation.setChannelCapabilities(null);
856
924
  conversation.setTrustContext(null);
857
925
  conversation.setCommandIntent(null);
@@ -872,7 +940,12 @@ export async function startVoiceTurn(
872
940
  const persistResult = await conversation.persistUserMessage({
873
941
  content: persistedContent,
874
942
  requestId,
875
- ...(isHiddenSyntheticPrompt ? { metadata: { hidden: true } } : {}),
943
+ metadata: {
944
+ // Durable "this turn came from an open voice session" marker; see
945
+ // `isVoiceSessionUserMessage` for why the channel fields cannot carry it.
946
+ voiceSessionTurn: true,
947
+ ...(isHiddenSyntheticPrompt ? { hidden: true } : {}),
948
+ },
876
949
  });
877
950
  return persistResult.id;
878
951
  };
@@ -892,21 +965,22 @@ export async function startVoiceTurn(
892
965
  trustContext: opts.trustContext ?? null,
893
966
  turnChannelContext,
894
967
  turnInterfaceContext,
895
- channelCapabilities: {
896
- ...resolveChannelCapabilities(
897
- turnChannelContext.userMessageChannel,
898
- turnInterfaceContext.userMessageInterface,
899
- ),
900
- // Voice calls are non-interactive: no surface can be shown, read, or
901
- // clicked mid-call, so `ui_show`/`ui_update`/`ui_dismiss` (and thus
902
- // `oauth_connect`, a ui_show surface_type) must never reach the model.
903
- // Phone already resolves to false via its channel; live-voice resolves
904
- // vellum/macos → true, so force it off here for every voice turn. This
905
- // also flips the runtime-context `supports_dynamic_ui` line the prompt
906
- // advertises, the secret-prompter's dynamic-UI branch, and the
907
- // task-progress-nudge hook — all correctly non-UI during a call.
908
- supportsDynamicUi: false,
909
- },
968
+ // Resolved from the channel, with no voice-specific override.
969
+ //
970
+ // Whether a call can show a surface is a property of the call's channel,
971
+ // not of calls in general. A phone call has no screen at all and resolves
972
+ // to `supportsDynamicUi: false` on its own; a live-voice call is a screen
973
+ // the user is holding, temporarily covered by the room overlay, and the
974
+ // session minimizes that overlay when a surface is shown (see the ui-tool
975
+ // branch of the live-voice session's `tool_use_start`).
976
+ //
977
+ // This one flag also drives the runtime-context `supports_dynamic_ui`
978
+ // line, the secret-prompter's dynamic-UI branch, and the
979
+ // task-progress-nudge hook, so a live-voice turn now reaches all of them.
980
+ channelCapabilities: resolveChannelCapabilities(
981
+ turnChannelContext.userMessageChannel,
982
+ turnInterfaceContext.userMessageInterface,
983
+ ),
910
984
  voiceCallControlPrompt,
911
985
  };
912
986
  const installVoiceTurnState = () => {
@@ -1105,6 +1179,11 @@ export async function startVoiceTurn(
1105
1179
  // Voice auto-denies/auto-allows/auto-resolves these since there's no interactive UI.
1106
1180
  let lastError: string | null = null;
1107
1181
  conversation.updateClient(async (msg: AssistantEvent) => {
1182
+ // The user (or anything else) answered: stop the fallback from firing on a
1183
+ // request that is already decided.
1184
+ if (msg.type === "interaction_resolved") {
1185
+ settleVoiceApproval(msg.requestId);
1186
+ }
1108
1187
  if (msg.type === "confirmation_request") {
1109
1188
  // Broadcast the request BEFORE resolving it: resolution synchronously
1110
1189
  // broadcasts `interaction_resolved` (handleConfirmationResponse →
@@ -1181,6 +1260,71 @@ export async function startVoiceTurn(
1181
1260
  });
1182
1261
  return;
1183
1262
  }
1263
+ // A live-voice call has a screen, so a consequential action can be put
1264
+ // to the user instead of decided for them.
1265
+ //
1266
+ // Everything used to be allowed here on the strength of the caller being
1267
+ // a guardian, which made a voice call the one surface where a tool that
1268
+ // writes to the workspace or reaches the host never had to ask. Only
1269
+ // *sensitive* reach prompts: gating every confirmation would interrupt a
1270
+ // conversation constantly, and the tools that read or render were never
1271
+ // the reason approval exists.
1272
+ //
1273
+ // Phone keeps the old behavior in full. There is no screen on that
1274
+ // channel, so a prompt there is a question nobody can answer.
1275
+ // The workspace root is what makes an escape visible. A sandbox file
1276
+ // tool pointed outside the workspace reaches the host filesystem on a
1277
+ // non-containerized install, and `sensitiveToolReach` can only see that
1278
+ // when it is given the boundary to compare against: without it,
1279
+ // `file_read { path: "/etc/hosts" }` classifies as `none` and would fall
1280
+ // through to the auto-allow this branch exists to prevent.
1281
+ const workspaceRoot = conversation.workingDir;
1282
+ const reach = sensitiveToolReach(
1283
+ msg.toolName,
1284
+ // Absent on requests that do not come from the tool pipeline (proxy
1285
+ // and network prompters). Unknown reads as the more consequential of
1286
+ // the two: the cost of being wrong is a prompt the user did not need,
1287
+ // against an unreviewed action on their machine.
1288
+ msg.executionTarget ?? "host",
1289
+ msg.input,
1290
+ workspaceRoot,
1291
+ );
1292
+ const canPrompt = turnChannelContext.userMessageChannel === "vellum";
1293
+ // Fail closed on a missing boundary for the same reason: with no
1294
+ // workspace root there is no way to tell an ordinary write from an
1295
+ // escape, and the safe reading of "cannot tell" is "ask". A real
1296
+ // conversation always has one, so this is a guard rather than a path.
1297
+ if (canPrompt && (reach !== "none" || !workspaceRoot)) {
1298
+ log.info(
1299
+ { turnId, toolName: msg.toolName, reach },
1300
+ "Prompting guardian voice caller for a sensitive tool",
1301
+ );
1302
+ // Left pending: the request is already broadcast, so the approval card
1303
+ // an attached client renders is now answerable rather than cleared a
1304
+ // moment later by this handler.
1305
+ //
1306
+ // Announced separately, because "a card exists" and "the user can see
1307
+ // it" are different things on a channel whose call covers the app.
1308
+ opts.onApprovalPending?.(msg.requestId);
1309
+ const timer = setTimeout(() => {
1310
+ pendingVoiceApprovals.delete(msg.requestId);
1311
+ if (pendingVoiceApprovals.size === 0) {
1312
+ opts.onApprovalsResolved?.();
1313
+ }
1314
+ log.info(
1315
+ { turnId, toolName: msg.toolName },
1316
+ "Voice approval timed out — falling back to the guardian allow",
1317
+ );
1318
+ conversation.handleConfirmationResponse(msg.requestId, "allow", {
1319
+ decisionContext: `Permission approved for "${msg.toolName}": this is a verified guardian voice call and the approval prompt went unanswered.`,
1320
+ });
1321
+ }, VOICE_APPROVAL_TIMEOUT_MS);
1322
+ // Never hold the process open for a prompt nobody is looking at.
1323
+ timer.unref?.();
1324
+ pendingVoiceApprovals.set(msg.requestId, timer);
1325
+ return;
1326
+ }
1327
+
1184
1328
  log.info(
1185
1329
  { turnId, toolName: msg.toolName },
1186
1330
  "Auto-approving confirmation request for guardian voice turn",
@@ -1191,11 +1335,14 @@ export async function startVoiceTurn(
1191
1335
  return;
1192
1336
  }
1193
1337
  if (msg.type === "secret_request") {
1194
- // Defense-in-depth: SecretPrompter.prompt fails fast with
1195
- // `unsupported_channel` on voice turns (supportsDynamicUi is forced
1196
- // off above), so a secret_request should never reach this handler.
1197
- // Resolve immediately anyway in case an emitter bypasses the
1198
- // prompter's channel check or races a capability install.
1338
+ // Auto-resolved rather than prompted, on every voice channel.
1339
+ //
1340
+ // A phone call cannot render a secret field at all. A live-voice call
1341
+ // now can (its channel resolves `supportsDynamicUi` true), so this is
1342
+ // where the prompt would surface, and it deliberately does not yet:
1343
+ // typing a credential into a screen you reached by minimizing a call is
1344
+ // a flow that needs designing, not a flag flip. Resolving with no secret
1345
+ // leaves the tool to fail the way it does today.
1199
1346
  log.info(
1200
1347
  { turnId, service: msg.service, field: msg.field },
1201
1348
  "Auto-resolving secret request for voice turn (no secret-entry UI)",
@@ -81,7 +81,7 @@ Examples:
81
81
  $ echo "Summarize this" | assistant inference send
82
82
  $ assistant llm send --system-prompt "You are a poet" "Write a haiku"
83
83
  $ assistant inference send --timeout-seconds 300 "Draft a long memo"
84
- $ assistant inference send --model claude-sonnet-4-20250514 --json "Hello"
84
+ $ assistant inference send --model claude-sonnet-5 --json "Hello"
85
85
  $ assistant inference send --profile balanced "Explain RFC 1149"`,
86
86
  };
87
87
 
@@ -96,7 +96,7 @@ Examples:
96
96
  $ assistant inference send "What is the capital of France?"
97
97
  $ echo "Explain quantum computing" | assistant inference send
98
98
  $ assistant llm send --system-prompt "Be concise" "What is TCP?"
99
- $ assistant inference send --model claude-sonnet-4-20250514 --json "Hello"
99
+ $ assistant inference send --model claude-sonnet-5 --json "Hello"
100
100
  $ assistant inference send --profile balanced "Explain RFC 1149"`,
101
101
  subcommands: [
102
102
  sendSubcommandHelp,
@@ -551,7 +551,7 @@ Examples:
551
551
  $ assistant llm send "What is the capital of France?"
552
552
  $ echo "Explain quantum computing" | assistant llm send
553
553
  $ assistant llm send --system-prompt "Be concise" "What is TCP?"
554
- $ assistant llm send --model claude-sonnet-4-20250514 --json "Hello"
554
+ $ assistant llm send --model claude-sonnet-5 --json "Hello"
555
555
  $ assistant llm send --profile balanced "Explain RFC 1149"`,
556
556
  subcommands: [sendSubcommandHelp],
557
557
  };
@@ -86,7 +86,7 @@ Examples:
86
86
  {
87
87
  flags: "--title <title>",
88
88
  description:
89
- "Short headline (≤ 8 words). Always provide one — the auto-derived fallback just truncates --message.",
89
+ "Short headline. Titles over 40 characters are trimmed to fit, and one over 40 characters that also runs past 7 words is cut to its first 5. A title that reads like prose (a reasoning opener, or a full sentence ending in terminal punctuation) is discarded in favor of a headline derived from --message.",
90
90
  },
91
91
  {
92
92
  flags: "--urgency <urgency>",
@@ -263,7 +263,8 @@ Examples:
263
263
  },
264
264
  {
265
265
  flags: "--title <title>",
266
- description: "New short headline (≤ 8 words).",
266
+ description:
267
+ "New short headline (≤ 8 words). An empty value is ignored, so the existing title stays put.",
267
268
  },
268
269
  {
269
270
  flags: "--urgency <urgency>",