@vellumai/assistant 0.10.1 → 0.10.2-dev.202606241651.2d2b40d

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 (367) hide show
  1. package/docs/workspace-tools.md +42 -33
  2. package/eslint-rules/cli-no-daemon-internals.js +6 -0
  3. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-delivery-contract.test.ts +91 -0
  4. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +31 -0
  5. package/node_modules/@vellumai/gateway-client/src/guardian-delivery-contract.ts +48 -0
  6. package/node_modules/@vellumai/gateway-client/src/index.ts +14 -0
  7. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +17 -0
  8. package/openapi.yaml +74 -1
  9. package/package.json +1 -1
  10. package/scripts/test.sh +36 -15
  11. package/src/__tests__/actor-token-service.test.ts +36 -14
  12. package/src/__tests__/agent-loop-override-profile.test.ts +1 -0
  13. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  14. package/src/__tests__/agent-wake-override-profile.test.ts +2 -0
  15. package/src/__tests__/annotate-activity-metadata.test.ts +2 -0
  16. package/src/__tests__/annotate-risk-options.test.ts +2 -0
  17. package/src/__tests__/approval-cascade.test.ts +2 -0
  18. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -0
  19. package/src/__tests__/btw-routes.test.ts +2 -0
  20. package/src/__tests__/build-persisted-content.test.ts +2 -0
  21. package/src/__tests__/call-controller.test.ts +19 -0
  22. package/src/__tests__/channel-guardian.test.ts +94 -58
  23. package/src/__tests__/channel-reply-delivery.test.ts +2 -0
  24. package/src/__tests__/compaction-events.test.ts +2 -0
  25. package/src/__tests__/compaction.benchmark.test.ts +2 -0
  26. package/src/__tests__/compactor-call-site-logging.test.ts +2 -0
  27. package/src/__tests__/compactor-low-watermark-cut.test.ts +2 -0
  28. package/src/__tests__/compactor-preserved-tail-count.test.ts +2 -0
  29. package/src/__tests__/compactor-summary-call-truncation.test.ts +2 -0
  30. package/src/__tests__/compactor-web-search-strip.test.ts +2 -0
  31. package/src/__tests__/computer-use-tools.test.ts +13 -0
  32. package/src/__tests__/config-loader-backfill.test.ts +5 -1
  33. package/src/__tests__/config-schema.test.ts +1 -0
  34. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +31 -29
  35. package/src/__tests__/contacts-relay-reads.test.ts +13 -15
  36. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  37. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +2 -0
  38. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  39. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
  40. package/src/__tests__/conversation-agent-loop.test.ts +7 -0
  41. package/src/__tests__/conversation-analysis-routes.test.ts +2 -0
  42. package/src/__tests__/conversation-app-control-lifecycle.test.ts +2 -0
  43. package/src/__tests__/conversation-confirmation-signals.test.ts +2 -0
  44. package/src/__tests__/conversation-history-web-search.test.ts +2 -0
  45. package/src/__tests__/conversation-load-history-repair.test.ts +2 -0
  46. package/src/__tests__/conversation-load-history-stripped.test.ts +2 -0
  47. package/src/__tests__/conversation-pairing.test.ts +2 -0
  48. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
  49. package/src/__tests__/conversation-process-callsite.test.ts +2 -0
  50. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  51. package/src/__tests__/conversation-queue.test.ts +91 -0
  52. package/src/__tests__/conversation-routes-guardian-reply.test.ts +14 -0
  53. package/src/__tests__/conversation-routes-slash-commands.test.ts +14 -0
  54. package/src/__tests__/conversation-slash-queue.test.ts +2 -0
  55. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  56. package/src/__tests__/conversation-speed-override.test.ts +2 -0
  57. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +65 -0
  58. package/src/__tests__/conversation-title-service.test.ts +2 -0
  59. package/src/__tests__/conversation-tool-setup-attribution.test.ts +47 -0
  60. package/src/__tests__/conversation-usage.test.ts +2 -0
  61. package/src/__tests__/conversation-workspace-cache-state.test.ts +2 -0
  62. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  63. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  64. package/src/__tests__/credential-security-invariants.test.ts +0 -1
  65. package/src/__tests__/db-migration-rollback.test.ts +205 -171
  66. package/src/__tests__/db-test-helpers.ts +5 -4
  67. package/src/__tests__/deterministic-verification-control-plane.test.ts +4 -2
  68. package/src/__tests__/disk-pressure-guard.test.ts +41 -0
  69. package/src/__tests__/dm-persistence.test.ts +2 -0
  70. package/src/__tests__/emit-signal-routing-intent.test.ts +10 -5
  71. package/src/__tests__/events-dev-bypass-actor.test.ts +7 -1
  72. package/src/__tests__/filing-service.test.ts +2 -0
  73. package/src/__tests__/guardian-binding-drift-heal.test.ts +75 -10
  74. package/src/__tests__/guardian-dispatch.test.ts +95 -1
  75. package/src/__tests__/guardian-outbound-http.test.ts +13 -0
  76. package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
  77. package/src/__tests__/heartbeat-service.test.ts +2 -0
  78. package/src/__tests__/helpers/channel-test-adapter.ts +1 -7
  79. package/src/__tests__/host-app-control-routes.test.ts +24 -30
  80. package/src/__tests__/host-bash-routes.test.ts +31 -41
  81. package/src/__tests__/host-browser-routes.test.ts +26 -32
  82. package/src/__tests__/host-cu-proxy.test.ts +299 -0
  83. package/src/__tests__/host-cu-routes-targeted.test.ts +25 -33
  84. package/src/__tests__/host-file-routes-targeted.test.ts +40 -52
  85. package/src/__tests__/host-transfer-routes-targeted.test.ts +31 -43
  86. package/src/__tests__/http-user-message-parity.test.ts +167 -8
  87. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  88. package/src/__tests__/invite-redemption-service.test.ts +43 -0
  89. package/src/__tests__/llm-context-normalization.test.ts +105 -0
  90. package/src/__tests__/llm-usage-store.test.ts +25 -0
  91. package/src/__tests__/media-stream-server-integration.test.ts +127 -0
  92. package/src/__tests__/memory-retrieval-hook.test.ts +2 -0
  93. package/src/__tests__/messaging-send-tool.test.ts +2 -0
  94. package/src/__tests__/migration-import-from-url.test.ts +2 -2
  95. package/src/__tests__/native-web-search.test.ts +2 -0
  96. package/src/__tests__/non-member-access-request.test.ts +189 -17
  97. package/src/__tests__/notification-broadcaster.test.ts +4 -0
  98. package/src/__tests__/notification-decision-recipient-context.test.ts +33 -32
  99. package/src/__tests__/notification-deep-link.test.ts +6 -0
  100. package/src/__tests__/notification-guardian-path.test.ts +19 -0
  101. package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
  102. package/src/__tests__/pending-interactions-resolved-event.test.ts +7 -4
  103. package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
  104. package/src/__tests__/plugin-bootstrap.test.ts +3 -73
  105. package/src/__tests__/plugin-route-contribution.test.ts +4 -17
  106. package/src/__tests__/plugin-tool-contribution.test.ts +3 -18
  107. package/src/__tests__/plugin-types.test.ts +0 -2
  108. package/src/__tests__/process-message-background-slack.test.ts +2 -0
  109. package/src/__tests__/process-message-display-content.test.ts +2 -0
  110. package/src/__tests__/provider-usage-tracking.test.ts +39 -0
  111. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +2 -0
  112. package/src/__tests__/registry.test.ts +3 -0
  113. package/src/__tests__/relay-server.test.ts +694 -25
  114. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  115. package/src/__tests__/secret-ingress-http.test.ts +14 -0
  116. package/src/__tests__/send-endpoint-busy.test.ts +30 -8
  117. package/src/__tests__/skills.test.ts +44 -0
  118. package/src/__tests__/slack-inbound-verification.test.ts +47 -2
  119. package/src/__tests__/sse-actor-principal-guardian-source.test.ts +102 -0
  120. package/src/__tests__/steer-on-enqueue-question.test.ts +181 -0
  121. package/src/__tests__/stt-hints.test.ts +44 -13
  122. package/src/__tests__/subagent-detail.test.ts +27 -0
  123. package/src/__tests__/subagent-disposal.test.ts +65 -0
  124. package/src/__tests__/subagent-notify-parent.test.ts +2 -0
  125. package/src/__tests__/subagent-spawn-tool-fork.test.ts +2 -0
  126. package/src/__tests__/subagent-tools.test.ts +2 -0
  127. package/src/__tests__/suggestion-routes.test.ts +2 -0
  128. package/src/__tests__/title-generate-hook.test.ts +2 -0
  129. package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
  130. package/src/__tests__/tool-executor.test.ts +16 -11
  131. package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
  132. package/src/__tests__/tool-result-metadata-plumbing.test.ts +2 -0
  133. package/src/__tests__/tool-start-timestamp.test.ts +2 -0
  134. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +10 -10
  135. package/src/__tests__/twilio-routes.test.ts +96 -0
  136. package/src/__tests__/verification-control-plane-policy.test.ts +2 -0
  137. package/src/__tests__/web-search-backend-failure.test.ts +2 -0
  138. package/src/__tests__/workspace-tool-loader.test.ts +195 -2
  139. package/src/agent/loop-exclusive-tool.test.ts +150 -0
  140. package/src/agent/loop.ts +56 -0
  141. package/src/api/constants/sse-replay.ts +41 -0
  142. package/src/api/index.ts +6 -0
  143. package/src/api/responses/llm-request-log-entry.ts +25 -0
  144. package/src/api/responses/subagent-detail.ts +17 -0
  145. package/src/calls/__tests__/relay-setup-router.test.ts +262 -4
  146. package/src/calls/call-domain.ts +3 -3
  147. package/src/calls/guardian-dispatch.ts +10 -8
  148. package/src/calls/inbound-trust-reader.ts +17 -1
  149. package/src/calls/media-stream-server.ts +21 -0
  150. package/src/calls/relay-server.ts +167 -50
  151. package/src/calls/relay-setup-router.ts +37 -7
  152. package/src/calls/relay-verification.ts +4 -4
  153. package/src/calls/stt-hints.ts +9 -12
  154. package/src/calls/twilio-routes.ts +14 -4
  155. package/src/cli/commands/__tests__/cache.test.ts +8 -1
  156. package/src/cli/commands/cache.ts +194 -181
  157. package/src/cli/commands/db/__tests__/repair.test.ts +6 -5
  158. package/src/cli/commands/db/status.ts +37 -1
  159. package/src/cli/commands/mcp.ts +252 -218
  160. package/src/cli/commands/memory/__tests__/worker.test.ts +302 -0
  161. package/src/cli/commands/memory/index.ts +2 -0
  162. package/src/cli/commands/memory/worker.ts +175 -0
  163. package/src/cli/commands/plugins.ts +75 -3
  164. package/src/cli/lib/__tests__/install-from-github.test.ts +102 -0
  165. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +160 -1
  166. package/src/cli/lib/list-installed-plugins.ts +179 -1
  167. package/src/config/__tests__/loader-callsite-strip-fallback.test.ts +143 -0
  168. package/src/config/bundled-skills/computer-use/TOOLS.json +6 -1
  169. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +27 -17
  170. package/src/config/bundled-skills/contacts/tools/contact-search.ts +13 -3
  171. package/src/config/feature-flag-registry.json +0 -8
  172. package/src/config/loader.ts +36 -5
  173. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  174. package/src/config/schemas/memory-lifecycle.ts +12 -0
  175. package/src/config/schemas/memory-v3.ts +7 -0
  176. package/src/config/schemas/memory.ts +4 -0
  177. package/src/config/schemas/timeouts.ts +8 -0
  178. package/src/config/seed-inference-profiles.ts +14 -5
  179. package/src/config/skills.ts +27 -5
  180. package/src/contacts/__tests__/guardian-delivery-reader.test.ts +312 -0
  181. package/src/contacts/contacts-write.ts +3 -0
  182. package/src/contacts/guardian-delivery-reader.ts +223 -0
  183. package/src/daemon/conversation-agent-loop.ts +9 -0
  184. package/src/daemon/conversation-process.ts +39 -17
  185. package/src/daemon/conversation-surfaces.ts +8 -0
  186. package/src/daemon/conversation-tool-setup.ts +49 -16
  187. package/src/daemon/conversation.ts +21 -2
  188. package/src/daemon/disk-pressure-guard.ts +12 -2
  189. package/src/daemon/event-loop-watchdog.ts +28 -1
  190. package/src/daemon/external-plugins-bootstrap.ts +4 -34
  191. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +25 -0
  192. package/src/daemon/handlers/__tests__/config-channels.test.ts +225 -0
  193. package/src/daemon/handlers/config-a2a.ts +6 -14
  194. package/src/daemon/handlers/config-channels.ts +78 -22
  195. package/src/daemon/handlers/conversations.ts +77 -0
  196. package/src/daemon/host-cu-proxy.ts +102 -11
  197. package/src/daemon/lifecycle.ts +4 -0
  198. package/src/daemon/memory-v2-startup.test.ts +72 -0
  199. package/src/daemon/memory-v2-startup.ts +87 -19
  200. package/src/daemon/server.ts +0 -4
  201. package/src/daemon/shutdown-handlers.ts +20 -0
  202. package/src/daemon/tool-setup-types.ts +9 -0
  203. package/src/ipc/__tests__/clients-list-ipc.test.ts +1 -1
  204. package/src/ipc/assistant-server.ts +2 -2
  205. package/src/memory/__tests__/301-create-watchdog-events.test.ts +110 -0
  206. package/src/memory/__tests__/memory-retrospective-job.test.ts +8 -0
  207. package/src/memory/__tests__/prompt-override.test.ts +192 -0
  208. package/src/memory/__tests__/watchdog-events-store.test.ts +161 -0
  209. package/src/memory/conversation-crud.ts +38 -0
  210. package/src/memory/db-connection.ts +22 -3
  211. package/src/memory/db-init.ts +36 -502
  212. package/src/memory/db-singleton.ts +6 -4
  213. package/src/memory/jobs-worker.ts +58 -0
  214. package/src/memory/llm-usage-store.ts +48 -20
  215. package/src/memory/memory-retrospective-job.ts +9 -8
  216. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +13 -3
  217. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -27
  218. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +130 -56
  219. package/src/memory/migrations/300-add-processing-started-at.ts +30 -0
  220. package/src/memory/migrations/301-create-watchdog-events.ts +45 -0
  221. package/src/memory/migrations/__tests__/014-backfill-inbox-thread-state.test.ts +108 -0
  222. package/src/memory/migrations/__tests__/136-drop-assistant-id-columns.test.ts +82 -0
  223. package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +224 -0
  224. package/src/memory/migrations/__tests__/run-migrations.test.ts +2 -2
  225. package/src/memory/migrations/run-migrations.ts +90 -6
  226. package/src/memory/migrations/schema-introspection.ts +14 -0
  227. package/src/memory/migrations/validate-migration-state.ts +101 -66
  228. package/src/memory/prompt-override.ts +129 -0
  229. package/src/memory/schema/conversations.ts +9 -0
  230. package/src/memory/schema/infrastructure.ts +20 -0
  231. package/src/memory/steps.ts +573 -0
  232. package/src/memory/v2/__tests__/cli-command-store.test.ts +25 -0
  233. package/src/memory/v2/__tests__/skill-store.test.ts +80 -0
  234. package/src/memory/v2/cli-command-store.ts +75 -38
  235. package/src/memory/v2/prompts/consolidation.ts +13 -82
  236. package/src/memory/v2/prompts/router.ts +21 -93
  237. package/src/memory/v2/skill-store.ts +68 -31
  238. package/src/memory/watchdog-events-store.ts +87 -0
  239. package/src/memory/worker-control.ts +118 -0
  240. package/src/memory/worker-process.ts +72 -0
  241. package/src/notifications/__tests__/broadcaster.test.ts +16 -8
  242. package/src/notifications/__tests__/connected-channels.test.ts +114 -0
  243. package/src/notifications/__tests__/decision-engine.test.ts +78 -9
  244. package/src/notifications/__tests__/destination-resolver.test.ts +256 -0
  245. package/src/notifications/broadcaster.ts +8 -1
  246. package/src/notifications/decision-engine.ts +15 -7
  247. package/src/notifications/destination-resolver.ts +68 -24
  248. package/src/notifications/emit-signal.ts +39 -14
  249. package/src/onboarding/checkin-event.test.ts +220 -0
  250. package/src/onboarding/checkin-event.ts +321 -0
  251. package/src/onboarding/schedule-checkin.ts +190 -0
  252. package/src/permissions/question-prompter.test.ts +1 -1
  253. package/src/permissions/question-prompter.ts +7 -4
  254. package/src/plugin-api/index.ts +6 -6
  255. package/src/plugin-api/types.ts +3 -5
  256. package/src/plugin-api/vision-support.test.ts +28 -4
  257. package/src/plugin-api/vision-support.ts +66 -31
  258. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +161 -0
  259. package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +106 -0
  260. package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +60 -0
  261. package/src/plugins/defaults/advisor/consult.ts +110 -6
  262. package/src/plugins/defaults/advisor/context-pack.ts +288 -0
  263. package/src/plugins/defaults/advisor/steering.ts +14 -2
  264. package/src/plugins/defaults/advisor/tools/advisor.ts +32 -5
  265. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +47 -7
  266. package/src/plugins/defaults/image-fallback/hooks/post-tool-use.ts +10 -11
  267. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +12 -20
  268. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +42 -11
  269. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +11 -2
  270. package/src/plugins/defaults/memory-v3-shadow/pool-select.test.ts +146 -0
  271. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +29 -1
  272. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +8 -1
  273. package/src/plugins/mtime-cache.ts +7 -2
  274. package/src/plugins/types.ts +0 -2
  275. package/src/providers/anthropic/client.ts +5 -0
  276. package/src/providers/call-site-routing.ts +4 -0
  277. package/src/providers/model-catalog.ts +16 -0
  278. package/src/providers/openai/responses-provider.ts +5 -0
  279. package/src/providers/openrouter/client.ts +5 -0
  280. package/src/providers/provider-send-message.ts +4 -0
  281. package/src/providers/ratelimit.ts +4 -0
  282. package/src/providers/retry.ts +4 -0
  283. package/src/providers/types.ts +9 -0
  284. package/src/providers/usage-tracking.ts +4 -0
  285. package/src/runtime/__tests__/channel-verification-service.test.ts +133 -0
  286. package/src/runtime/__tests__/guardian-vellum-migration.test.ts +181 -0
  287. package/src/runtime/__tests__/is-guardian-bound-for-channel.test.ts +66 -0
  288. package/src/runtime/__tests__/local-principal-trust.test.ts +164 -0
  289. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +335 -3
  290. package/src/runtime/access-request-helper.ts +19 -39
  291. package/src/runtime/actor-trust-resolver.ts +2 -2
  292. package/src/runtime/anchored-guardian.test.ts +156 -0
  293. package/src/runtime/anchored-guardian.ts +135 -0
  294. package/src/runtime/assistant-event-hub.ts +1 -1
  295. package/src/runtime/assistant-stream-state.ts +9 -2
  296. package/src/runtime/auth/__tests__/require-bound-guardian.test.ts +99 -0
  297. package/src/runtime/auth/require-bound-guardian.ts +21 -11
  298. package/src/runtime/channel-verification-service.ts +56 -31
  299. package/src/runtime/confirmation-request-guardian-bridge.ts +3 -3
  300. package/src/runtime/guardian-vellum-migration.ts +66 -7
  301. package/src/runtime/invite-redemption-service.ts +50 -18
  302. package/src/runtime/local-actor-identity.ts +76 -11
  303. package/src/runtime/local-principal-trust.ts +52 -0
  304. package/src/runtime/pending-interactions.ts +11 -1
  305. package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +56 -5
  306. package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
  307. package/src/runtime/routes/__tests__/contact-routes.test.ts +212 -0
  308. package/src/runtime/routes/__tests__/global-search-routes.test.ts +93 -0
  309. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +215 -1
  310. package/src/runtime/routes/browser-routes.ts +1 -1
  311. package/src/runtime/routes/channel-verification-routes.ts +3 -3
  312. package/src/runtime/routes/contact-routes.ts +8 -32
  313. package/src/runtime/routes/conversation-cli-routes.ts +4 -5
  314. package/src/runtime/routes/conversation-list-routes.ts +4 -7
  315. package/src/runtime/routes/conversation-routes.ts +74 -81
  316. package/src/runtime/routes/events-routes.ts +2 -2
  317. package/src/runtime/routes/global-search-routes.ts +3 -1
  318. package/src/runtime/routes/guardian-action-routes.ts +4 -5
  319. package/src/runtime/routes/host-app-control-routes.ts +5 -4
  320. package/src/runtime/routes/host-bash-routes.ts +5 -4
  321. package/src/runtime/routes/host-browser-routes.ts +9 -11
  322. package/src/runtime/routes/host-cu-routes.ts +5 -4
  323. package/src/runtime/routes/host-file-routes.ts +5 -4
  324. package/src/runtime/routes/host-transfer-routes.ts +6 -6
  325. package/src/runtime/routes/http-adapter.ts +1 -1
  326. package/src/runtime/routes/identity-routes.ts +3 -2
  327. package/src/runtime/routes/inbound-message-handler.ts +5 -5
  328. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +97 -5
  329. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +61 -49
  330. package/src/runtime/routes/inbound-stages/background-dispatch.ts +16 -4
  331. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +7 -7
  332. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +21 -8
  333. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -3
  334. package/src/runtime/routes/index.ts +2 -0
  335. package/src/runtime/routes/llm-context-normalization.ts +71 -0
  336. package/src/runtime/routes/mcp-auth-routes.ts +38 -15
  337. package/src/runtime/routes/migration-rollback-routes.ts +4 -3
  338. package/src/runtime/routes/migration-routes.ts +4 -1
  339. package/src/runtime/routes/onboarding-checkin-routes.ts +86 -0
  340. package/src/runtime/routes/subagents-routes.ts +5 -0
  341. package/src/runtime/routes/surface-action-routes.ts +51 -55
  342. package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
  343. package/src/runtime/services/conversation-serializer.ts +7 -9
  344. package/src/runtime/tool-grant-request-helper.ts +3 -3
  345. package/src/runtime/trust-verdict-consumer.ts +85 -9
  346. package/src/runtime/verification-outbound-actions.ts +18 -18
  347. package/src/signals/user-message.ts +16 -0
  348. package/src/subagent/manager.ts +9 -0
  349. package/src/telemetry/types.ts +34 -1
  350. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  351. package/src/telemetry/usage-telemetry-reporter.ts +87 -3
  352. package/src/tools/ask-question/ask-question-tool.test.ts +29 -0
  353. package/src/tools/ask-question/ask-question-tool.ts +13 -0
  354. package/src/tools/computer-use/definitions.ts +8 -2
  355. package/src/tools/executor.ts +4 -4
  356. package/src/tools/registry.ts +18 -0
  357. package/src/tools/tool-approval-handler.ts +1 -1
  358. package/src/tools/tool-defaults.ts +9 -2
  359. package/src/tools/types.ts +17 -2
  360. package/src/tools/workspace-tools/loader.ts +348 -244
  361. package/src/util/platform.ts +5 -0
  362. package/src/util/telemetry-db-path.ts +24 -0
  363. package/src/workspace/migrations/017-seed-persona-dirs.ts +3 -34
  364. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +3 -24
  365. package/src/__tests__/workspace-tools-watcher-flag.test.ts +0 -70
  366. package/src/daemon/workspace-tools-watcher.ts +0 -328
  367. package/src/memory/migrations/registry.ts +0 -573
@@ -87,7 +87,7 @@ afterAll(() => {
87
87
 
88
88
  const handleHostFileResult = ROUTES.find(
89
89
  (r) => r.endpoint === "host-file-result",
90
- )!.handler;
90
+ )!.handler as (args: Record<string, unknown>) => Promise<unknown>;
91
91
 
92
92
  // ── Helpers ─────────────────────────────────────────────────────────────────
93
93
 
@@ -189,15 +189,13 @@ describe("handleHostFileResult — targetClientId guard", () => {
189
189
  ).toThrow(BadRequestError);
190
190
  });
191
191
 
192
- test("interaction is NOT resolved on 400 (still pending)", () => {
192
+ test("interaction is NOT resolved on 400 (still pending)", async () => {
193
193
  const requestId = "req-file-targeted-no-header-stays";
194
194
  registerPending(requestId, { targetClientId: "client-A" });
195
195
 
196
- try {
197
- handleHostFileResult({ body: fileBody(requestId) });
198
- } catch {
196
+ await handleHostFileResult({ body: fileBody(requestId) }).catch(() => {
199
197
  // expected
200
- }
198
+ });
201
199
 
202
200
  expect(resolvedIds).not.toContain(requestId);
203
201
  expect(pendingStore.has(requestId)).toBe(true);
@@ -219,19 +217,17 @@ describe("handleHostFileResult — targetClientId guard", () => {
219
217
  ).toThrow(ForbiddenError);
220
218
  });
221
219
 
222
- test("ForbiddenError message names both submitting and expected client", () => {
220
+ test("ForbiddenError message names both submitting and expected client", async () => {
223
221
  const requestId = "req-file-targeted-mismatch-msg";
224
222
  registerPending(requestId, { targetClientId: "client-A" });
225
223
 
226
224
  let caught: unknown;
227
- try {
228
- handleHostFileResult({
229
- body: fileBody(requestId),
230
- headers: { "x-vellum-client-id": "client-B" },
231
- });
232
- } catch (e) {
225
+ await handleHostFileResult({
226
+ body: fileBody(requestId),
227
+ headers: { "x-vellum-client-id": "client-B" },
228
+ }).catch((e: unknown) => {
233
229
  caught = e;
234
- }
230
+ });
235
231
 
236
232
  expect(caught).toBeInstanceOf(ForbiddenError);
237
233
  const msg = (caught as ForbiddenError).message;
@@ -239,18 +235,16 @@ describe("handleHostFileResult — targetClientId guard", () => {
239
235
  expect(msg).toContain("client-A");
240
236
  });
241
237
 
242
- test("interaction is NOT consumed on 403 (pendingInteractions.get still returns it)", () => {
238
+ test("interaction is NOT consumed on 403 (pendingInteractions.get still returns it)", async () => {
243
239
  const requestId = "req-file-targeted-mismatch-stays";
244
240
  registerPending(requestId, { targetClientId: "client-A" });
245
241
 
246
- try {
247
- handleHostFileResult({
248
- body: fileBody(requestId),
249
- headers: { "x-vellum-client-id": "client-B" },
250
- });
251
- } catch {
242
+ await handleHostFileResult({
243
+ body: fileBody(requestId),
244
+ headers: { "x-vellum-client-id": "client-B" },
245
+ }).catch(() => {
252
246
  // expected
253
- }
247
+ });
254
248
 
255
249
  expect(resolvedIds).not.toContain(requestId);
256
250
  expect(pendingStore.has(requestId)).toBe(true);
@@ -306,22 +300,20 @@ describe("handleHostFileResult — targetClientId guard", () => {
306
300
  ).toThrow(ForbiddenError);
307
301
  });
308
302
 
309
- test("interaction is NOT consumed on actor-mismatch 403", () => {
303
+ test("interaction is NOT consumed on actor-mismatch 403", async () => {
310
304
  const requestId = "req-file-actor-mismatch-stays";
311
305
  clientActors.set("client-A", "actor-1");
312
306
  registerPending(requestId, { targetClientId: "client-A" });
313
307
 
314
- try {
315
- handleHostFileResult({
316
- body: fileBody(requestId),
317
- headers: {
318
- "x-vellum-client-id": "client-A",
319
- "x-vellum-actor-principal-id": "actor-2",
320
- },
321
- });
322
- } catch {
308
+ await handleHostFileResult({
309
+ body: fileBody(requestId),
310
+ headers: {
311
+ "x-vellum-client-id": "client-A",
312
+ "x-vellum-actor-principal-id": "actor-2",
313
+ },
314
+ }).catch(() => {
323
315
  // expected
324
- }
316
+ });
325
317
 
326
318
  expect(resolvedIds).not.toContain(requestId);
327
319
  expect(pendingStore.has(requestId)).toBe(true);
@@ -360,19 +352,17 @@ describe("handleHostFileResult — targetClientId guard", () => {
360
352
  ).toThrow(ForbiddenError);
361
353
  });
362
354
 
363
- test("interaction is NOT consumed when submitting actor is missing", () => {
355
+ test("interaction is NOT consumed when submitting actor is missing", async () => {
364
356
  const requestId = "req-file-actor-missing-stays";
365
357
  clientActors.set("client-A", "actor-1");
366
358
  registerPending(requestId, { targetClientId: "client-A" });
367
359
 
368
- try {
369
- handleHostFileResult({
370
- body: fileBody(requestId),
371
- headers: { "x-vellum-client-id": "client-A" },
372
- });
373
- } catch {
360
+ await handleHostFileResult({
361
+ body: fileBody(requestId),
362
+ headers: { "x-vellum-client-id": "client-A" },
363
+ }).catch(() => {
374
364
  // expected
375
- }
365
+ });
376
366
 
377
367
  expect(resolvedIds).not.toContain(requestId);
378
368
  expect(pendingStore.has(requestId)).toBe(true);
@@ -398,21 +388,19 @@ describe("handleHostFileResult — targetClientId guard", () => {
398
388
  ).toThrow(ForbiddenError);
399
389
  });
400
390
 
401
- test("interaction is NOT consumed when target client has no stored actor", () => {
391
+ test("interaction is NOT consumed when target client has no stored actor", async () => {
402
392
  const requestId = "req-file-target-no-actor-stays";
403
393
  registerPending(requestId, { targetClientId: "client-A" });
404
394
 
405
- try {
406
- handleHostFileResult({
407
- body: fileBody(requestId),
408
- headers: {
409
- "x-vellum-client-id": "client-A",
410
- "x-vellum-actor-principal-id": "actor-1",
411
- },
412
- });
413
- } catch {
395
+ await handleHostFileResult({
396
+ body: fileBody(requestId),
397
+ headers: {
398
+ "x-vellum-client-id": "client-A",
399
+ "x-vellum-actor-principal-id": "actor-1",
400
+ },
401
+ }).catch(() => {
414
402
  // expected
415
- }
403
+ });
416
404
 
417
405
  expect(resolvedIds).not.toContain(requestId);
418
406
  expect(pendingStore.has(requestId)).toBe(true);
@@ -99,7 +99,7 @@ afterAll(() => {
99
99
 
100
100
  const handleTransferContentGet = ROUTES.find(
101
101
  (r) => r.endpoint === "transfers/:transferId/content" && r.method === "GET",
102
- )!.handler;
102
+ )!.handler as (args: Record<string, unknown>) => Promise<unknown>;
103
103
 
104
104
  const handleTransferContentPut = ROUTES.find(
105
105
  (r) => r.endpoint === "transfers/:transferId/content" && r.method === "PUT",
@@ -107,7 +107,7 @@ const handleTransferContentPut = ROUTES.find(
107
107
 
108
108
  const handleTransferResult = ROUTES.find(
109
109
  (r) => r.endpoint === "host-transfer-result",
110
- )!.handler;
110
+ )!.handler as (args: Record<string, unknown>) => Promise<unknown>;
111
111
 
112
112
  // ── Helpers ─────────────────────────────────────────────────────────────────
113
113
 
@@ -186,15 +186,13 @@ describe("handleTransferContentGet — Phase 3 targetClientId guard", () => {
186
186
  ).toThrow(BadRequestError);
187
187
  });
188
188
 
189
- test("getTransferContent NOT called on 400", () => {
189
+ test("getTransferContent NOT called on 400", async () => {
190
190
  stubTargetClientId = "client-A";
191
- try {
192
- handleTransferContentGet({
193
- pathParams: { transferId: TEST_TRANSFER_ID },
194
- });
195
- } catch {
191
+ await handleTransferContentGet({
192
+ pathParams: { transferId: TEST_TRANSFER_ID },
193
+ }).catch(() => {
196
194
  // expected
197
- }
195
+ });
198
196
  expect(getTransferContentCalls).toHaveLength(0);
199
197
  });
200
198
  });
@@ -212,16 +210,14 @@ describe("handleTransferContentGet — Phase 3 targetClientId guard", () => {
212
210
  ).toThrow(ForbiddenError);
213
211
  });
214
212
 
215
- test("getTransferContent NOT called on 403", () => {
213
+ test("getTransferContent NOT called on 403", async () => {
216
214
  stubTargetClientId = "client-A";
217
- try {
218
- handleTransferContentGet({
219
- pathParams: { transferId: TEST_TRANSFER_ID },
220
- headers: { "x-vellum-client-id": "client-B" },
221
- });
222
- } catch {
215
+ await handleTransferContentGet({
216
+ pathParams: { transferId: TEST_TRANSFER_ID },
217
+ headers: { "x-vellum-client-id": "client-B" },
218
+ }).catch(() => {
223
219
  // expected
224
- }
220
+ });
225
221
  expect(getTransferContentCalls).toHaveLength(0);
226
222
  });
227
223
  });
@@ -526,23 +522,19 @@ describe("handleTransferResult — Phase 3 targetClientId guard", () => {
526
522
  );
527
523
  });
528
524
 
529
- test("resolveTransferResult NOT called on 400", () => {
525
+ test("resolveTransferResult NOT called on 400", async () => {
530
526
  registerHostTransferPending("client-A");
531
- try {
532
- handleTransferResult({ body: resultBody() });
533
- } catch {
527
+ await handleTransferResult({ body: resultBody() }).catch(() => {
534
528
  // expected
535
- }
529
+ });
536
530
  expect(resolveTransferResultCalls).toHaveLength(0);
537
531
  });
538
532
 
539
- test("pending interaction still present after 400", () => {
533
+ test("pending interaction still present after 400", async () => {
540
534
  registerHostTransferPending("client-A");
541
- try {
542
- handleTransferResult({ body: resultBody() });
543
- } catch {
535
+ await handleTransferResult({ body: resultBody() }).catch(() => {
544
536
  // expected
545
- }
537
+ });
546
538
  expect(pendingStore.has(TEST_REQUEST_ID)).toBe(true);
547
539
  });
548
540
  });
@@ -560,29 +552,25 @@ describe("handleTransferResult — Phase 3 targetClientId guard", () => {
560
552
  ).toThrow(ForbiddenError);
561
553
  });
562
554
 
563
- test("resolveTransferResult NOT called on 403", () => {
555
+ test("resolveTransferResult NOT called on 403", async () => {
564
556
  registerHostTransferPending("client-A");
565
- try {
566
- handleTransferResult({
567
- body: resultBody(),
568
- headers: { "x-vellum-client-id": "client-B" },
569
- });
570
- } catch {
557
+ await handleTransferResult({
558
+ body: resultBody(),
559
+ headers: { "x-vellum-client-id": "client-B" },
560
+ }).catch(() => {
571
561
  // expected
572
- }
562
+ });
573
563
  expect(resolveTransferResultCalls).toHaveLength(0);
574
564
  });
575
565
 
576
- test("pending interaction still present after 403", () => {
566
+ test("pending interaction still present after 403", async () => {
577
567
  registerHostTransferPending("client-A");
578
- try {
579
- handleTransferResult({
580
- body: resultBody(),
581
- headers: { "x-vellum-client-id": "client-B" },
582
- });
583
- } catch {
568
+ await handleTransferResult({
569
+ body: resultBody(),
570
+ headers: { "x-vellum-client-id": "client-B" },
571
+ }).catch(() => {
584
572
  // expected
585
- }
573
+ });
586
574
  expect(pendingStore.has(TEST_REQUEST_ID)).toBe(true);
587
575
  });
588
576
  });
@@ -62,6 +62,21 @@ mock.module("../runtime/guardian-reply-router.js", () => ({
62
62
  routeGuardianReply: routeGuardianReplyMock,
63
63
  }));
64
64
 
65
+ // Stub for the shared reset-drift helper. handleSendMessage only consumes its
66
+ // result (a guardian TrustContext or null) on a first-pass-unknown actor; the
67
+ // gate itself is covered in runtime/__tests__/guardian-vellum-migration.test.ts.
68
+ const reResolveCalls: string[] = [];
69
+ let mockReResolve: { trustClass: string; sourceChannel: string } | null = null;
70
+ mock.module("../runtime/guardian-vellum-migration.js", () => ({
71
+ reResolveTrustOnResetDrift: async (
72
+ incomingPrincipalId: string,
73
+ _sourceChannel: string,
74
+ ) => {
75
+ reResolveCalls.push(incomingPrincipalId);
76
+ return mockReResolve;
77
+ },
78
+ }));
79
+
65
80
  mock.module("../memory/canonical-guardian-store.js", () => ({
66
81
  createCanonicalGuardianRequest: () => ({
67
82
  id: "canonical-id",
@@ -94,6 +109,8 @@ mock.module("../runtime/confirmation-request-guardian-bridge.js", () => ({
94
109
  }));
95
110
 
96
111
  mock.module("../memory/conversation-crud.js", () => ({
112
+ setConversationProcessingStartedAt: () => {},
113
+ isConversationProcessing: () => false,
97
114
  addMessage: (
98
115
  conversationId: string,
99
116
  role: string,
@@ -104,17 +121,32 @@ mock.module("../memory/conversation-crud.js", () => ({
104
121
  }));
105
122
 
106
123
  mock.module("../runtime/local-actor-identity.js", () => ({
107
- resolveLocalTrustContext: () => ({
108
- trustClass: "guardian",
109
- sourceChannel: "vellum",
110
- }),
124
+ findLocalGuardianPrincipalId: async () =>
125
+ mockGuardians?.find(
126
+ (g) => g.channelType === "vellum" && g.status === "active",
127
+ )?.principalId as string | undefined,
128
+ }));
129
+
130
+ let mockGuardians: Array<Record<string, unknown>> | null = [
131
+ {
132
+ channelType: "vellum",
133
+ contactId: "guardian-contact",
134
+ principalId: "test-user",
135
+ address: "test-user",
136
+ status: "active",
137
+ },
138
+ ];
139
+
140
+ mock.module("../contacts/guardian-delivery-reader.js", () => ({
141
+ getGuardianDelivery: async () => mockGuardians,
142
+ guardianForChannel: (
143
+ list: Array<Record<string, unknown>>,
144
+ channelType: string,
145
+ ) => list.find((g) => g.channelType === channelType && g.status === "active"),
111
146
  }));
112
147
 
148
+ // handleSendMessage wraps the first-pass resolve with withSourceChannel.
113
149
  mock.module("../runtime/trust-context-resolver.js", () => ({
114
- resolveTrustContext: () => ({
115
- trustClass: "guardian",
116
- sourceChannel: "vellum",
117
- }),
118
150
  withSourceChannel: (sourceChannel: unknown, ctx: unknown) => ({
119
151
  ...(ctx as Record<string, unknown>),
120
152
  sourceChannel,
@@ -454,3 +486,130 @@ describe("HTTP POST /v1/messages clientTimezone transport metadata", () => {
454
486
  expect(runAgentLoop).toHaveBeenCalledTimes(1);
455
487
  });
456
488
  });
489
+
490
+ // ============================================================================
491
+ // TRUST CONTEXT — derived from the gateway guardian binding
492
+ // ============================================================================
493
+ describe("HTTP POST /v1/messages trust context from the gateway binding", () => {
494
+ beforeEach(() => {
495
+ mockGuardians = [
496
+ {
497
+ channelType: "vellum",
498
+ contactId: "guardian-contact",
499
+ principalId: "test-user",
500
+ address: "test-user",
501
+ status: "active",
502
+ },
503
+ ];
504
+ reResolveCalls.length = 0;
505
+ mockReResolve = null;
506
+ });
507
+
508
+ function requestAs(principalId: string, sourceChannel = "vellum") {
509
+ return new Request("http://localhost/v1/messages", {
510
+ method: "POST",
511
+ headers: {
512
+ "Content-Type": "application/json",
513
+ "x-vellum-actor-principal-id": principalId,
514
+ "x-vellum-principal-type": "actor",
515
+ },
516
+ body: JSON.stringify({
517
+ conversationKey: "trust-test-key",
518
+ content: "hi",
519
+ sourceChannel,
520
+ interface: "macos",
521
+ }),
522
+ });
523
+ }
524
+
525
+ async function trustContextFor(
526
+ principalId: string,
527
+ sourceChannel = "vellum",
528
+ ): Promise<Record<string, unknown>> {
529
+ let captured: Record<string, unknown> | undefined;
530
+ const conversation = makeConversation({
531
+ setTrustContext: (ctx: Record<string, unknown>) => {
532
+ captured = ctx;
533
+ },
534
+ });
535
+ const res = await callHandler(
536
+ (args) =>
537
+ handleSendMessage(args, {
538
+ sendMessageDeps: {
539
+ getOrCreateConversation: async () => conversation,
540
+ assistantEventHub: { publish: async () => {} } as any,
541
+ resolveAttachments: () => [],
542
+ },
543
+ }),
544
+ requestAs(principalId, sourceChannel),
545
+ undefined,
546
+ 202,
547
+ );
548
+ expect(res.status).toBe(202);
549
+ return captured ?? {};
550
+ }
551
+
552
+ async function trustClassFor(principalId: string): Promise<string> {
553
+ return (await trustContextFor(principalId)).trustClass as string;
554
+ }
555
+
556
+ test("guardian principal resolves to guardian context, helper not called", async () => {
557
+ expect(await trustClassFor("test-user")).toBe("guardian");
558
+ expect(reResolveCalls).toEqual([]);
559
+ });
560
+
561
+ test("non-guardian principal: helper consulted, null result stays unknown", async () => {
562
+ mockReResolve = null;
563
+ expect(await trustClassFor("vellum-principal-stranger")).toBe("unknown");
564
+ expect(reResolveCalls).toEqual(["vellum-principal-stranger"]);
565
+ });
566
+
567
+ test("reset drift: helper returns guardian → route adopts it", async () => {
568
+ mockGuardians = [
569
+ {
570
+ channelType: "vellum",
571
+ contactId: "guardian-contact",
572
+ principalId: "vellum-principal-stale",
573
+ address: "vellum-principal-stale",
574
+ status: "active",
575
+ },
576
+ ];
577
+ mockReResolve = { trustClass: "guardian", sourceChannel: "vellum" };
578
+
579
+ expect(await trustClassFor("vellum-principal-healed")).toBe("guardian");
580
+ expect(reResolveCalls).toEqual(["vellum-principal-healed"]);
581
+ });
582
+
583
+ test("helper returns an unknown-class ctx → trust stays unknown (not adopted)", async () => {
584
+ mockGuardians = [
585
+ {
586
+ channelType: "vellum",
587
+ contactId: "guardian-contact",
588
+ principalId: "vellum-principal-stale",
589
+ address: "vellum-principal-stale",
590
+ status: "active",
591
+ },
592
+ ];
593
+ mockReResolve = { trustClass: "unknown", sourceChannel: "vellum" };
594
+
595
+ expect(await trustClassFor("vellum-principal-healed")).toBe("unknown");
596
+ });
597
+
598
+ test("dev-bypass maps the gateway guardian principal to guardian", async () => {
599
+ expect(await trustClassFor("dev-bypass")).toBe("guardian");
600
+ });
601
+
602
+ test("dev-bypass fails closed to unknown on an empty gateway", async () => {
603
+ // No active gateway binding: dev-bypass cannot translate to a real guardian,
604
+ // and the helper (null) leaves trust unknown — parity with /v1/surface-actions.
605
+ mockGuardians = [];
606
+ mockReResolve = null;
607
+ expect(await trustClassFor("dev-bypass")).toBe("unknown");
608
+ });
609
+
610
+ test("preserves the request body channel on the guardian-match happy path", async () => {
611
+ const ctx = await trustContextFor("test-user", "telegram");
612
+ expect(ctx.trustClass).toBe("guardian");
613
+ expect(ctx.sourceChannel).toBe("telegram");
614
+ });
615
+ });
@@ -35,6 +35,8 @@ const addMessageCalls: Array<{
35
35
  }> = [];
36
36
 
37
37
  mock.module("../memory/conversation-crud.js", () => ({
38
+ setConversationProcessingStartedAt: () => {},
39
+ isConversationProcessing: () => false,
38
40
  addMessage: async (
39
41
  conversationId: string,
40
42
  role: string,
@@ -40,6 +40,8 @@ function resetGatewayIpc() {
40
40
  gatewayIpc.calls = [];
41
41
  }
42
42
 
43
+ import type { TrustVerdict } from "@vellumai/gateway-client";
44
+
43
45
  import {
44
46
  findContactChannel,
45
47
  getContact,
@@ -56,6 +58,7 @@ import {
56
58
  type InviteRedemptionOutcome,
57
59
  redeemInvite,
58
60
  redeemInviteByCode,
61
+ resolveMemberGateStatus,
59
62
  } from "../runtime/invite-redemption-service.js";
60
63
  import { hashVoiceCode } from "../util/voice-code.js";
61
64
 
@@ -763,3 +766,43 @@ describe("invite-redemption-service", () => {
763
766
  ).toBeNull();
764
767
  });
765
768
  });
769
+
770
+ describe("resolveMemberGateStatus", () => {
771
+ const memberlessVerdict: TrustVerdict = {
772
+ trustClass: "unverified_contact",
773
+ canonicalSenderId: "telegram:blocked-user",
774
+ };
775
+ const memberVerdict: TrustVerdict = {
776
+ trustClass: "trusted_contact",
777
+ canonicalSenderId: "telegram:active-user",
778
+ contactId: "contact-1",
779
+ channelId: "channel-1",
780
+ type: "telegram",
781
+ address: "active-user",
782
+ status: "active",
783
+ policy: "allow",
784
+ };
785
+
786
+ test("uses the verdict member status when the verdict resolves a member", async () => {
787
+ expect(await resolveMemberGateStatus(memberVerdict, "blocked")).toBe(
788
+ "active",
789
+ );
790
+ });
791
+
792
+ test("falls back to local status when a non-null verdict carries no member", async () => {
793
+ // A previously blocked contact with a valid invite must stay blocked even
794
+ // when the verdict is non-null but memberless (externalChatId-only match /
795
+ // resolutionFailed), so it can't bypass the gate.
796
+ expect(await resolveMemberGateStatus(memberlessVerdict, "blocked")).toBe(
797
+ "blocked",
798
+ );
799
+ });
800
+
801
+ test("falls back to local status when the verdict is null", async () => {
802
+ expect(await resolveMemberGateStatus(null, "blocked")).toBe("blocked");
803
+ });
804
+
805
+ test("returns null when neither verdict member nor local status is present", async () => {
806
+ expect(await resolveMemberGateStatus(memberlessVerdict, null)).toBeNull();
807
+ });
808
+ });
@@ -1827,4 +1827,109 @@ describe("normalizeLlmContextPayloads", () => {
1827
1827
  },
1828
1828
  ]);
1829
1829
  });
1830
+
1831
+ describe("provider error response payloads", () => {
1832
+ test("extracts a structured error from a rejected call's response payload", () => {
1833
+ const normalized = normalizeLlmContextPayloads({
1834
+ createdAt: 1_742_400_000_000,
1835
+ requestPayload: {
1836
+ model: "accounts/fireworks/models/glm-5p2",
1837
+ messages: [{ role: "user", content: "hello" }],
1838
+ },
1839
+ responsePayload: {
1840
+ error: {
1841
+ name: "ProviderError",
1842
+ message:
1843
+ "This model doesn't support image input. Remove the image or switch to a vision-capable model.",
1844
+ code: "PROVIDER_ERROR",
1845
+ provider: "fireworks",
1846
+ statusCode: 400,
1847
+ },
1848
+ },
1849
+ });
1850
+
1851
+ expect(normalized.error).toEqual({
1852
+ name: "ProviderError",
1853
+ message:
1854
+ "This model doesn't support image input. Remove the image or switch to a vision-capable model.",
1855
+ code: "PROVIDER_ERROR",
1856
+ provider: "fireworks",
1857
+ statusCode: 400,
1858
+ });
1859
+ // No response sections are produced for an error payload.
1860
+ expect(normalized.responseSections).toBeUndefined();
1861
+ });
1862
+
1863
+ test("preserves a normalized request alongside the error", () => {
1864
+ const normalized = normalizeLlmContextPayloads({
1865
+ createdAt: 1_742_400_000_000,
1866
+ requestPayload: {
1867
+ model: "gpt-4.1",
1868
+ tool_choice: "auto",
1869
+ messages: [{ role: "user", content: "hi" }],
1870
+ },
1871
+ responsePayload: { error: { message: "boom" } },
1872
+ });
1873
+
1874
+ expect(normalized.error).toEqual({ message: "boom" });
1875
+ // The request side still normalizes so the Prompt tab keeps working.
1876
+ expect(normalized.requestSections?.length).toBeGreaterThan(0);
1877
+ expect(normalized.summary?.provider).toBe("openai");
1878
+ });
1879
+
1880
+ test("carries statusCode 0 and retryAfterMs through", () => {
1881
+ const normalized = normalizeLlmContextPayloads({
1882
+ createdAt: 1_742_400_000_000,
1883
+ requestPayload: null,
1884
+ responsePayload: {
1885
+ error: {
1886
+ name: "ProviderError",
1887
+ message: "rate limited",
1888
+ provider: "anthropic",
1889
+ statusCode: 0,
1890
+ retryAfterMs: 1500,
1891
+ },
1892
+ },
1893
+ });
1894
+
1895
+ expect(normalized.error).toEqual({
1896
+ name: "ProviderError",
1897
+ message: "rate limited",
1898
+ provider: "anthropic",
1899
+ statusCode: 0,
1900
+ retryAfterMs: 1500,
1901
+ });
1902
+ });
1903
+
1904
+ test("does not treat a successful response with no error as failed", () => {
1905
+ const normalized = normalizeLlmContextPayloads({
1906
+ createdAt: 1_742_400_000_000,
1907
+ requestPayload: {
1908
+ model: "gpt-4.1",
1909
+ messages: [{ role: "user", content: "hi" }],
1910
+ },
1911
+ responsePayload: {
1912
+ model: "gpt-4.1",
1913
+ choices: [
1914
+ {
1915
+ finish_reason: "stop",
1916
+ message: { role: "assistant", content: "hello" },
1917
+ },
1918
+ ],
1919
+ },
1920
+ });
1921
+
1922
+ expect(normalized.error).toBeUndefined();
1923
+ });
1924
+
1925
+ test("ignores an empty error object with no identifying fields", () => {
1926
+ const normalized = normalizeLlmContextPayloads({
1927
+ createdAt: 1_742_400_000_000,
1928
+ requestPayload: null,
1929
+ responsePayload: { error: {} },
1930
+ });
1931
+
1932
+ expect(normalized.error).toBeUndefined();
1933
+ });
1934
+ });
1830
1935
  });