@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
@@ -29,6 +29,7 @@ import {
29
29
  import type { AdmissionPolicy, TrustVerdict } from "@vellumai/gateway-client";
30
30
 
31
31
  import type { TrustClass } from "../../runtime/actor-trust-resolver.js";
32
+ import type { SetupOutcome } from "../call-setup-router.js";
32
33
  import type { CallSession } from "../types.js";
33
34
 
34
35
  // `resolveActorTrust` must never be consulted by the router — the gateway
@@ -376,47 +377,75 @@ describe("routeSetup — verdict session stamp gates getPendingSession", () => {
376
377
  });
377
378
 
378
379
  // ---------------------------------------------------------------------------
379
- // Floor table: trustClass × policy → admit/deny
380
+ // Floor table: trustClass × policy → setup action
381
+ //
382
+ // A caller at or above the floor connects directly (`normal_call`). A caller
383
+ // BELOW a floor that a guardian approval could clear is routed into the
384
+ // guardian access-request flow instead of hung up on: `name_capture` for an
385
+ // unrecognized number, `unverified_caller` for a known contact who still owes
386
+ // verification. `guardian_only` / `no_one` sit above the trusted
387
+ // contact rank an approval grants, so they stay a hard `deny`.
380
388
  // ---------------------------------------------------------------------------
381
389
 
382
390
  describe("routeSetup — admission floor table", () => {
383
391
  const cases: Array<{
384
392
  policy: AdmissionPolicy;
385
- admits: TrustClass[];
386
- denies: TrustClass[];
393
+ expected: Record<TrustClass, SetupOutcome["action"]>;
387
394
  }> = [
388
395
  {
389
396
  policy: "strangers",
390
- admits: ["unknown", "unverified_contact", "trusted_contact", "guardian"],
391
- denies: [],
397
+ expected: {
398
+ unknown: "normal_call",
399
+ unverified_contact: "normal_call",
400
+ trusted_contact: "normal_call",
401
+ guardian: "normal_call",
402
+ },
392
403
  },
393
404
  {
394
405
  policy: "any_contact",
395
- admits: ["unverified_contact", "trusted_contact", "guardian"],
396
- denies: ["unknown"],
406
+ expected: {
407
+ unknown: "name_capture",
408
+ unverified_contact: "normal_call",
409
+ trusted_contact: "normal_call",
410
+ guardian: "normal_call",
411
+ },
397
412
  },
398
413
  {
399
414
  policy: "trusted_contacts",
400
- admits: ["trusted_contact", "guardian"],
401
- denies: ["unknown", "unverified_contact"],
415
+ expected: {
416
+ unknown: "name_capture",
417
+ unverified_contact: "unverified_caller",
418
+ trusted_contact: "normal_call",
419
+ guardian: "normal_call",
420
+ },
402
421
  },
403
422
  {
404
423
  policy: "guardian_only",
405
- admits: ["guardian"],
406
- denies: ["unknown", "unverified_contact", "trusted_contact"],
424
+ expected: {
425
+ unknown: "deny",
426
+ unverified_contact: "deny",
427
+ trusted_contact: "deny",
428
+ guardian: "normal_call",
429
+ },
407
430
  },
408
431
  {
409
432
  policy: "no_one",
410
- admits: [],
411
- denies: ["unknown", "unverified_contact", "trusted_contact", "guardian"],
433
+ expected: {
434
+ unknown: "deny",
435
+ unverified_contact: "deny",
436
+ trusted_contact: "deny",
437
+ guardian: "deny",
438
+ },
412
439
  },
413
440
  ];
414
441
 
415
- for (const { policy, admits, denies } of cases) {
416
- for (const trustClass of denies) {
417
- test(`${policy} denies ${trustClass}`, async () => {
442
+ for (const { policy, expected } of cases) {
443
+ for (const [trustClass, action] of Object.entries(expected) as Array<
444
+ [TrustClass, SetupOutcome["action"]]
445
+ >) {
446
+ test(`${policy} routes ${trustClass} to ${action}`, async () => {
418
447
  const { outcome } = await route(policy, verdictFor(trustClass));
419
- expect(outcome.action).toBe("deny");
448
+ expect(outcome.action).toBe(action);
420
449
  if (outcome.action === "deny") {
421
450
  expect(outcome.logReason).toBe(
422
451
  `Inbound voice admission floor: ${policy}`,
@@ -424,15 +453,116 @@ describe("routeSetup — admission floor table", () => {
424
453
  }
425
454
  });
426
455
  }
427
- for (const trustClass of admits) {
428
- test(`${policy} admits ${trustClass}`, async () => {
429
- const { outcome } = await route(policy, verdictFor(trustClass));
430
- expect(outcome.action).not.toBe("deny");
431
- });
432
- }
433
456
  }
434
457
  });
435
458
 
459
+ // ---------------------------------------------------------------------------
460
+ // An unrecognized caller below a clearable floor reaches the guardian
461
+ // access-request flow instead of being hung up on. The default channel policy
462
+ // is `trusted_contacts`, so this is the out-of-the-box path for every inbound
463
+ // call from an unknown number.
464
+ // ---------------------------------------------------------------------------
465
+
466
+ describe("routeSetup: below-floor unknown caller reaches guardian approval", () => {
467
+ test("default trusted_contacts floor routes an unknown caller to name_capture", async () => {
468
+ const { outcome, resolved } = await route(
469
+ "trusted_contacts",
470
+ verdictFor("unknown"),
471
+ );
472
+ expect(outcome.action).toBe("name_capture");
473
+ if (outcome.action === "name_capture") {
474
+ expect(outcome.fromNumber).toBe("+12025550142");
475
+ }
476
+ expect(resolved.actorTrust.trustClass).toBe("unknown");
477
+ });
478
+
479
+ test("an active voice invite still takes precedence over the approval flow", async () => {
480
+ activeVoiceInvite = {
481
+ inviteId: "inv_1",
482
+ inviteeName: "Alice Example",
483
+ guardianName: "Sam",
484
+ codeDigits: 6,
485
+ };
486
+ const { outcome } = await route("trusted_contacts", verdictFor("unknown"));
487
+ expect(outcome.action).toBe("invite_redemption");
488
+ });
489
+
490
+ test("guardian_only keeps the hard deny (approval could not admit them)", async () => {
491
+ const { outcome } = await route("guardian_only", verdictFor("unknown"));
492
+ expect(outcome.action).toBe("deny");
493
+ });
494
+
495
+ test("a blocked caller below a clearable floor is still denied", async () => {
496
+ const { outcome } = await route(
497
+ "trusted_contacts",
498
+ makeMemberVerdict("unknown", { status: "blocked" }),
499
+ );
500
+ expect(outcome.action).toBe("deny");
501
+ });
502
+
503
+ test("a revoked caller below a clearable floor is still denied", async () => {
504
+ const { outcome } = await route(
505
+ "trusted_contacts",
506
+ makeMemberVerdict("unknown", { status: "revoked" }),
507
+ );
508
+ expect(outcome.action).toBe("deny");
509
+ });
510
+
511
+ // Status and policy are independent governance signals: the gateway derives
512
+ // trust class from status alone, so a channel the guardian set to
513
+ // `policy: "deny"` still resolves to `unverified_contact` while status is
514
+ // `unverified`/`pending`. The deny gate is trust-class independent and runs
515
+ // ahead of the floor, so it holds whether the floor admits or denies, and it
516
+ // outranks an active voice invite.
517
+ describe("member policy deny outranks every inbound flow", () => {
518
+ const denyVerdict = () =>
519
+ makeMemberVerdict("unverified_contact", {
520
+ status: "unverified",
521
+ policy: "deny",
522
+ });
523
+
524
+ test("denied below a clearable floor (not sent to verification guidance)", async () => {
525
+ const { outcome } = await route("trusted_contacts", denyVerdict());
526
+ expect(outcome.action).toBe("deny");
527
+ if (outcome.action === "deny") {
528
+ expect(outcome.logReason).toBe("Inbound voice ACL: member policy deny");
529
+ }
530
+ });
531
+
532
+ test("denied even when the floor would admit them", async () => {
533
+ const { outcome } = await route("strangers", denyVerdict());
534
+ expect(outcome.action).toBe("deny");
535
+ });
536
+
537
+ test("denied with no floor configured", async () => {
538
+ const { outcome } = await route(null, denyVerdict());
539
+ expect(outcome.action).toBe("deny");
540
+ });
541
+
542
+ test("denied even with an active voice invite", async () => {
543
+ activeVoiceInvite = {
544
+ inviteId: "inv_1",
545
+ inviteeName: "Alice Example",
546
+ guardianName: "Sam",
547
+ codeDigits: 6,
548
+ };
549
+ const { outcome } = await route("trusted_contacts", denyVerdict());
550
+ expect(outcome.action).toBe("deny");
551
+ });
552
+
553
+ test("an active-status member with policy deny is denied", async () => {
554
+ const { outcome } = await route(
555
+ "trusted_contacts",
556
+ makeMemberVerdict("trusted_contact", {
557
+ status: "active",
558
+ policy: "deny",
559
+ }),
560
+ );
561
+ expect(outcome.action).toBe("deny");
562
+ });
563
+ });
564
+ });
565
+
436
566
  // ---------------------------------------------------------------------------
437
567
  // Blocked / revoked always deny
438
568
  // ---------------------------------------------------------------------------
@@ -542,14 +672,14 @@ describe("routeSetup — permissive floor admits to normal_call", () => {
542
672
  ).toBe("unverified_caller");
543
673
  });
544
674
 
545
- test("trusted_contacts (default) still denies unknown and unverified", async () => {
675
+ test("trusted_contacts (default) keeps below-floor callers out of normal_call", async () => {
546
676
  expect(
547
677
  (await route("trusted_contacts", verdictFor("unknown"))).outcome.action,
548
- ).toBe("deny");
678
+ ).toBe("name_capture");
549
679
  expect(
550
680
  (await route("trusted_contacts", verdictFor("unverified_contact")))
551
681
  .outcome.action,
552
- ).toBe("deny");
682
+ ).toBe("unverified_caller");
553
683
  });
554
684
  });
555
685
 
@@ -78,6 +78,7 @@ import {
78
78
  cutFrontDoorContentAtVerdict,
79
79
  startVoiceTurn,
80
80
  TOOL_RESULT_PREVIEW_MAX_CHARS,
81
+ type VoiceTurnOptions,
81
82
  } from "../voice-session-bridge.js";
82
83
  import {
83
84
  escalatedContinuationRule,
@@ -118,7 +119,13 @@ interface FakeConversation {
118
119
  requestId: string;
119
120
  metadata?: Record<string, unknown>;
120
121
  }) => Promise<{ id: string }>;
122
+ workingDir: string;
121
123
  updateClient: (cb: unknown, reset?: boolean) => void;
124
+ handleConfirmationResponse: (
125
+ requestId: string,
126
+ decision: string,
127
+ opts?: { decisionContext?: string },
128
+ ) => void;
122
129
  runAgentLoop: (...args: unknown[]) => Promise<void>;
123
130
  abort: (reason?: unknown) => void;
124
131
  loadFromDb: () => Promise<void>;
@@ -134,14 +141,22 @@ function makeFakeConversation(opts: {
134
141
  hasQueuedMessages?: () => boolean;
135
142
  /** Runs before each persist resolves; throw to script a persist failure. */
136
143
  onPersist?: (attempt: number) => void;
144
+ /** Workspace root; pass empty to model a missing boundary. */
145
+ workingDir?: string;
137
146
  }) {
138
147
  const waitForIdleCalls: WaitForIdleCall[] = [];
148
+ const confirmationDecisions: Array<{ requestId: string; decision: string }> =
149
+ [];
150
+ let clientCallback: ((msg: unknown) => Promise<void>) | undefined;
139
151
  let persistCount = 0;
140
152
  let lastPersistOpts:
141
153
  | { content: string; requestId: string; metadata?: Record<string, unknown> }
142
154
  | undefined;
143
155
  const conversation: FakeConversation = {
144
156
  conversationId: "conv-voice-bridge-test",
157
+ // The workspace boundary the reach check compares paths against. A real
158
+ // conversation always has one; without it the approval gate fails closed.
159
+ workingDir: opts.workingDir ?? "/tmp/workspace-voice-bridge-test",
145
160
  callSessionId: undefined,
146
161
  forcePromptSideEffects: false,
147
162
  currentRequestId: undefined,
@@ -173,8 +188,14 @@ function makeFakeConversation(opts: {
173
188
  },
174
189
  // The install (reset falsy) / reset (reset true) pair marks a turn
175
190
  // taking ownership of the conversation vs a turn's cleanup releasing it.
176
- updateClient: (_cb, reset) => {
191
+ updateClient: (cb, reset) => {
177
192
  opts.events?.push(reset ? "client:reset" : "client:install");
193
+ if (reset !== true) {
194
+ clientCallback = cb as (msg: unknown) => Promise<void>;
195
+ }
196
+ },
197
+ handleConfirmationResponse: (requestId, decision) => {
198
+ confirmationDecisions.push({ requestId, decision });
178
199
  },
179
200
  runAgentLoop: () => (opts.runAgentLoop ?? (async () => {}))(),
180
201
  abort: () => {},
@@ -186,6 +207,11 @@ function makeFakeConversation(opts: {
186
207
  return {
187
208
  conversation,
188
209
  waitForIdleCalls,
210
+ confirmationDecisions,
211
+ /** Deliver an event the way the conversation would, to the installed handler. */
212
+ emitToClient: async (msg: unknown) => {
213
+ await clientCallback?.(msg);
214
+ },
189
215
  persistCount: () => persistCount,
190
216
  lastPersistOpts: () => lastPersistOpts,
191
217
  setProcessingFlag: (value: boolean) => {
@@ -338,7 +364,10 @@ describe("startVoiceTurn escalation-continuation persistence", () => {
338
364
  expect(fake.lastPersistOpts()?.content).toBe(
339
365
  ESCALATION_CONTINUATION_CONTENT,
340
366
  );
341
- expect(fake.lastPersistOpts()?.metadata).toEqual({ hidden: true });
367
+ expect(fake.lastPersistOpts()?.metadata).toEqual({
368
+ voiceSessionTurn: true,
369
+ hidden: true,
370
+ });
342
371
  });
343
372
 
344
373
  test("the opener prompt is persisted un-hidden (unchanged)", async () => {
@@ -347,7 +376,9 @@ describe("startVoiceTurn escalation-continuation persistence", () => {
347
376
 
348
377
  await startVoiceTurn(makeTurnOptions()); // content: CALL_OPENING_MARKER
349
378
 
350
- expect(fake.lastPersistOpts()?.metadata).toBeUndefined();
379
+ expect(fake.lastPersistOpts()?.metadata).toEqual({
380
+ voiceSessionTurn: true,
381
+ });
351
382
  });
352
383
  });
353
384
 
@@ -390,7 +421,10 @@ describe("startVoiceTurn hiddenSyntheticPrompt", () => {
390
421
  );
391
422
 
392
423
  expect(fake.lastPersistOpts()?.content).toBe(SYNTHETIC_CONTENT);
393
- expect(fake.lastPersistOpts()?.metadata).toEqual({ hidden: true });
424
+ expect(fake.lastPersistOpts()?.metadata).toEqual({
425
+ voiceSessionTurn: true,
426
+ hidden: true,
427
+ });
394
428
  expect(echoes).toHaveLength(0);
395
429
  });
396
430
 
@@ -405,7 +439,9 @@ describe("startVoiceTurn hiddenSyntheticPrompt", () => {
405
439
  }),
406
440
  );
407
441
 
408
- expect(fake.lastPersistOpts()?.metadata).toBeUndefined();
442
+ expect(fake.lastPersistOpts()?.metadata).toEqual({
443
+ voiceSessionTurn: true,
444
+ });
409
445
  expect(echoes).toEqual([
410
446
  expect.objectContaining({ text: SYNTHETIC_CONTENT }),
411
447
  ]);
@@ -475,9 +511,10 @@ describe("startVoiceTurn triage-and-escalate control prompt", () => {
475
511
  });
476
512
 
477
513
  describe("startVoiceTurn channel capabilities", () => {
478
- // Voice calls are non-interactive: the bridge forces supportsDynamicUi off
479
- // for every voice turn so ui-surface tools never reach the model mid-call,
480
- // while leaving the rest of the channel's resolved capabilities intact.
514
+ // Whether a call can show a surface is a property of its channel, not of
515
+ // calls in general, so the bridge applies no voice-specific override: a
516
+ // phone call has no screen and resolves false on its own, while a live-voice
517
+ // call is a screen the user is holding and resolves true.
481
518
 
482
519
  // The turn installs its capabilities, then cleanup resets them to null — so
483
520
  // capture every applied value and read the installed (non-null) one.
@@ -497,7 +534,7 @@ describe("startVoiceTurn channel capabilities", () => {
497
534
  );
498
535
  }
499
536
 
500
- test("a vellum/macos (live-voice) turn forces supportsDynamicUi off, other fields untouched", async () => {
537
+ test("a vellum/macos (live-voice) turn keeps its channel's dynamic UI", async () => {
501
538
  const installed = captureInstalledCapabilities();
502
539
  await startVoiceTurn({
503
540
  ...makeTurnOptions(),
@@ -505,14 +542,16 @@ describe("startVoiceTurn channel capabilities", () => {
505
542
  userMessageInterface: "macos",
506
543
  });
507
544
  const caps = installed();
508
- expect(caps?.supportsDynamicUi).toBe(false);
509
- // The override is surgical: live-voice keeps identifying as vellum/macos.
545
+ expect(caps?.supportsDynamicUi).toBe(true);
546
+ // Nothing else about the channel is rewritten either.
510
547
  expect(caps?.dashboardCapable).toBe(true);
511
548
  expect(caps?.supportsVoiceInput).toBe(true);
512
549
  expect(caps?.clientOS).toBe("macos");
513
550
  });
514
551
 
515
- test("phone defaults (no channel overrides) also yield supportsDynamicUi false", async () => {
552
+ // The case the removed override existed for, still covered: a phone call has
553
+ // no screen to show a surface on, and says so through its channel.
554
+ test("phone defaults yield supportsDynamicUi false", async () => {
516
555
  const installed = captureInstalledCapabilities();
517
556
  await startVoiceTurn(makeTurnOptions());
518
557
  const caps = installed();
@@ -521,6 +560,156 @@ describe("startVoiceTurn channel capabilities", () => {
521
560
  });
522
561
  });
523
562
 
563
+ describe("startVoiceTurn guardian approvals", () => {
564
+ function confirmationRequest(
565
+ toolName: string,
566
+ executionTarget?: "sandbox" | "host",
567
+ input: Record<string, unknown> = {},
568
+ ) {
569
+ return {
570
+ type: "confirmation_request",
571
+ requestId: "req-1",
572
+ toolName,
573
+ input,
574
+ riskLevel: "medium",
575
+ allowlistOptions: [],
576
+ scopeOptions: [],
577
+ ...(executionTarget !== undefined ? { executionTarget } : {}),
578
+ };
579
+ }
580
+
581
+ async function runVoiceTurn(
582
+ overrides: Partial<VoiceTurnOptions>,
583
+ conversationOpts: { workingDir?: string } = {},
584
+ ) {
585
+ const fake = makeFakeConversation({
586
+ processing: false,
587
+ ...conversationOpts,
588
+ });
589
+ fakeConversation = fake.conversation;
590
+ const pendingAnnounced: string[] = [];
591
+ await startVoiceTurn({
592
+ ...makeTurnOptions(),
593
+ trustContext: { trustClass: "guardian" },
594
+ onApprovalPending: (requestId: string) => {
595
+ pendingAnnounced.push(requestId);
596
+ },
597
+ ...overrides,
598
+ } as VoiceTurnOptions);
599
+ return { ...fake, pendingAnnounced };
600
+ }
601
+
602
+ // A live-voice call has a screen, so a tool that reaches the workspace or
603
+ // the host is put to the user rather than decided for them. This is the hole
604
+ // it closes: a guardian call used to allow every confirmation outright.
605
+ test("leaves a sensitive tool pending for the user to answer", async () => {
606
+ const fake = await runVoiceTurn({
607
+ userMessageChannel: "vellum",
608
+ userMessageInterface: "macos",
609
+ });
610
+
611
+ await fake.emitToClient(confirmationRequest("bash", "host"));
612
+
613
+ expect(fake.confirmationDecisions).toEqual([]);
614
+ // The card renders in the app, and the call covers the app, so a pending
615
+ // decision has to be announced or the turn just goes quiet.
616
+ expect(fake.pendingAnnounced).toEqual(["req-1"]);
617
+ });
618
+
619
+ // The tools that read or render were never the reason approval exists, and
620
+ // gating them would interrupt the conversation constantly.
621
+ test("still allows a tool with no sensitive reach", async () => {
622
+ const fake = await runVoiceTurn({
623
+ userMessageChannel: "vellum",
624
+ userMessageInterface: "macos",
625
+ });
626
+
627
+ await fake.emitToClient(confirmationRequest("ui_show", "sandbox"));
628
+
629
+ expect(fake.confirmationDecisions).toEqual([
630
+ { requestId: "req-1", decision: "allow" },
631
+ ]);
632
+ // Nothing is waiting, so nothing interrupts the call.
633
+ expect(fake.pendingAnnounced).toEqual([]);
634
+ });
635
+
636
+ // There is no screen on a phone call, so a prompt there is a question nobody
637
+ // can answer.
638
+ test("a phone call keeps allowing sensitive tools outright", async () => {
639
+ const fake = await runVoiceTurn({ userMessageChannel: "phone" });
640
+
641
+ await fake.emitToClient(confirmationRequest("bash", "host"));
642
+
643
+ expect(fake.confirmationDecisions).toEqual([
644
+ { requestId: "req-1", decision: "allow" },
645
+ ]);
646
+ });
647
+
648
+ // The escape this gate exists to catch: a *sandbox* file tool pointed
649
+ // outside the workspace reaches the host filesystem on a non-containerized
650
+ // install. The reach check can only see that when it is given the workspace
651
+ // boundary; without it this read of a host file classifies as `none` and
652
+ // lands on the auto-allow.
653
+ test("an out-of-workspace path prompts even on a sandbox target", async () => {
654
+ const fake = await runVoiceTurn(
655
+ { userMessageChannel: "vellum", userMessageInterface: "macos" },
656
+ { workingDir: "/tmp/workspace-voice-bridge-test" },
657
+ );
658
+
659
+ await fake.emitToClient(
660
+ confirmationRequest("file_read", "sandbox", { path: "/etc/hosts" }),
661
+ );
662
+
663
+ expect(fake.confirmationDecisions).toEqual([]);
664
+ });
665
+
666
+ // A path inside the workspace is the ordinary case, and gating it would
667
+ // interrupt the conversation for every file the assistant touches.
668
+ test("an in-workspace read is still allowed outright", async () => {
669
+ const fake = await runVoiceTurn(
670
+ { userMessageChannel: "vellum", userMessageInterface: "macos" },
671
+ { workingDir: "/tmp/workspace-voice-bridge-test" },
672
+ );
673
+
674
+ await fake.emitToClient(
675
+ confirmationRequest("file_read", "sandbox", {
676
+ path: "/tmp/workspace-voice-bridge-test/notes.md",
677
+ }),
678
+ );
679
+
680
+ expect(fake.confirmationDecisions).toEqual([
681
+ { requestId: "req-1", decision: "allow" },
682
+ ]);
683
+ });
684
+
685
+ // With no boundary there is no way to tell an ordinary write from an escape,
686
+ // and the safe reading of "cannot tell" is "ask".
687
+ test("a missing workspace boundary fails closed", async () => {
688
+ const fake = await runVoiceTurn(
689
+ { userMessageChannel: "vellum", userMessageInterface: "macos" },
690
+ { workingDir: "" },
691
+ );
692
+
693
+ await fake.emitToClient(confirmationRequest("ui_show", "sandbox"));
694
+
695
+ expect(fake.confirmationDecisions).toEqual([]);
696
+ });
697
+
698
+ // Requests from the proxy and network prompters carry no target. Unknown
699
+ // reads as the more consequential of the two: a prompt the user did not need
700
+ // costs less than an unreviewed action on their machine.
701
+ test("prompts when the execution target is unknown", async () => {
702
+ const fake = await runVoiceTurn({
703
+ userMessageChannel: "vellum",
704
+ userMessageInterface: "macos",
705
+ });
706
+
707
+ await fake.emitToClient(confirmationRequest("bash"));
708
+
709
+ expect(fake.confirmationDecisions).toEqual([]);
710
+ });
711
+ });
712
+
524
713
  describe("startVoiceTurn conversation-lock wait", () => {
525
714
  test("an idle conversation starts the turn without consulting waitForIdle", async () => {
526
715
  const fake = makeFakeConversation({ processing: false });