@vellumai/assistant 0.11.7 → 0.11.8-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (446) hide show
  1. package/ARCHITECTURE.md +10 -10
  2. package/Dockerfile +1 -0
  3. package/docs/architecture/security.md +1 -1
  4. package/docs/error-handling.md +1 -2
  5. package/docs/guardian-request-flow.md +51 -5
  6. package/docs/trusted-contact-access.md +13 -12
  7. package/knip.json +1 -0
  8. package/node_modules/@vellumai/avatar-catalog/package.json +19 -0
  9. package/{src/avatar/character-components.ts → node_modules/@vellumai/avatar-catalog/src/catalog.ts} +12 -44
  10. package/node_modules/@vellumai/avatar-catalog/src/colors.ts +19 -0
  11. package/node_modules/@vellumai/avatar-catalog/src/index.ts +25 -0
  12. package/node_modules/@vellumai/avatar-catalog/src/types.ts +47 -0
  13. package/node_modules/@vellumai/avatar-catalog/tsconfig.json +20 -0
  14. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  15. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  17. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  18. package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +43 -12
  19. package/node_modules/@vellumai/environments/package.json +2 -1
  20. package/node_modules/@vellumai/environments/src/__tests__/test-path-guard.test.ts +89 -0
  21. package/node_modules/@vellumai/environments/src/test-path-guard.ts +113 -0
  22. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  23. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  24. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  25. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  26. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +8 -18
  27. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +46 -0
  28. package/node_modules/@vellumai/gateway-client/src/__tests__/invite-contract.test.ts +1 -0
  29. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +46 -0
  30. package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +62 -21
  31. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +14 -0
  32. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +53 -0
  33. package/node_modules/@vellumai/gateway-client/src/index.ts +11 -5
  34. package/node_modules/@vellumai/gateway-client/src/invite-contract.ts +1 -0
  35. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  36. package/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  37. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  38. package/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  39. package/openapi.yaml +481 -9
  40. package/package.json +3 -1
  41. package/scripts/sync-llm-catalog.ts +3 -0
  42. package/src/__tests__/agent-loop-callsite-precedence.test.ts +16 -0
  43. package/src/__tests__/agent-loop-compacted-history-transform.test.ts +304 -0
  44. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +3 -0
  45. package/src/__tests__/agent-wake-override-profile.test.ts +3 -0
  46. package/src/__tests__/anthropic-provider.test.ts +56 -0
  47. package/src/__tests__/approval-cascade.test.ts +0 -13
  48. package/src/__tests__/attachments-store.test.ts +117 -6
  49. package/src/__tests__/attachments.test.ts +122 -0
  50. package/src/__tests__/bundled-asset.test.ts +22 -1
  51. package/src/__tests__/channel-approval.test.ts +14 -7
  52. package/src/__tests__/channel-approvals.test.ts +3 -3
  53. package/src/__tests__/channel-delivery-store.test.ts +58 -0
  54. package/src/__tests__/channel-inbound-disk-pressure.test.ts +1 -0
  55. package/src/__tests__/channel-invite-transport.test.ts +1 -0
  56. package/src/__tests__/channel-readiness-slack-remote.test.ts +3 -3
  57. package/src/__tests__/compaction-events.test.ts +0 -12
  58. package/src/__tests__/compactor-call-site-logging.test.ts +7 -1
  59. package/src/__tests__/compactor-retained-image-validation.test.ts +15 -0
  60. package/src/__tests__/config-schema.test.ts +35 -0
  61. package/src/__tests__/contacts-write.test.ts +141 -3
  62. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -13
  63. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +0 -12
  64. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -13
  65. package/src/__tests__/conversation-agent-loop.test.ts +95 -13
  66. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -13
  67. package/src/__tests__/conversation-load-history-repair.test.ts +418 -0
  68. package/src/__tests__/conversation-media-retry.test.ts +33 -2
  69. package/src/__tests__/conversation-pairing.test.ts +54 -0
  70. package/src/__tests__/conversation-process-callsite.test.ts +0 -12
  71. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -13
  72. package/src/__tests__/conversation-queue.test.ts +4 -13
  73. package/src/__tests__/conversation-retry-route.test.ts +5 -0
  74. package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
  75. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
  76. package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
  77. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
  78. package/src/__tests__/conversation-sight-frames.test.ts +615 -0
  79. package/src/__tests__/conversation-slash-queue.test.ts +0 -13
  80. package/src/__tests__/conversation-slash-unknown.test.ts +0 -13
  81. package/src/__tests__/conversation-speed-override.test.ts +0 -12
  82. package/src/__tests__/conversation-store-ephemeral.test.ts +0 -12
  83. package/src/__tests__/conversation-summarize-route.test.ts +5 -0
  84. package/src/__tests__/conversation-summarize-up-to.test.ts +102 -0
  85. package/src/__tests__/conversation-wait-for-idle.test.ts +0 -12
  86. package/src/__tests__/conversation-workspace-cache-state.test.ts +0 -5
  87. package/src/__tests__/conversation-workspace-injection.test.ts +0 -17
  88. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -17
  89. package/src/__tests__/credential-health-service.test.ts +45 -0
  90. package/src/__tests__/credential-record-retire.test.ts +256 -0
  91. package/src/__tests__/credential-record-write-through.test.ts +8 -4
  92. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +0 -4
  93. package/src/__tests__/db-conversation-inference-profile-migration.test.ts +0 -4
  94. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +0 -8
  95. package/src/__tests__/delete-propagation.test.ts +121 -3
  96. package/src/__tests__/evict-conversations-for-reload.test.ts +46 -6
  97. package/src/__tests__/file-ops-service.test.ts +8 -3
  98. package/src/__tests__/gemini-provider.test.ts +64 -0
  99. package/src/__tests__/guardian-action-service-scope.test.ts +118 -0
  100. package/src/__tests__/guardian-card-withdrawal.test.ts +299 -5
  101. package/src/__tests__/guardian-expiry-notifier.test.ts +117 -13
  102. package/src/__tests__/guardian-gateway-sim.test.ts +119 -0
  103. package/src/__tests__/guardian-gateway-sim.ts +56 -20
  104. package/src/__tests__/guardian-reply-router-answer-mode.test.ts +6 -0
  105. package/src/__tests__/guardian-request-status-sync.test.ts +162 -0
  106. package/src/__tests__/guardian-wait-controller.test.ts +6 -0
  107. package/src/__tests__/headless-browser-mode.test.ts +27 -0
  108. package/src/__tests__/helpers/mock-conversation.ts +2 -0
  109. package/src/__tests__/list-messages-system-card.test.ts +70 -0
  110. package/src/__tests__/llm-catalog-parity.test.ts +2 -0
  111. package/src/__tests__/managed-profile-guard.test.ts +2 -2
  112. package/src/__tests__/media-resolve-image-validation.test.ts +106 -0
  113. package/src/__tests__/media-stream-server-integration.test.ts +6 -0
  114. package/src/__tests__/non-member-access-request.test.ts +12 -0
  115. package/src/__tests__/notification-discord-adapter.test.ts +76 -4
  116. package/src/__tests__/oauth-commands-routes.test.ts +21 -0
  117. package/src/__tests__/openai-provider.test.ts +63 -0
  118. package/src/__tests__/outbound-slack-persistence.test.ts +114 -2
  119. package/src/__tests__/persist-unsendable-image-downscale.test.ts +63 -1
  120. package/src/__tests__/persist-unsendable-image.test.ts +19 -0
  121. package/src/__tests__/plugin-app-serve-routes.test.ts +2 -2
  122. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
  123. package/src/__tests__/process-message-background-slack.test.ts +1 -1
  124. package/src/__tests__/process-message-display-content.test.ts +1 -1
  125. package/src/__tests__/processing-flag-persist-failure.test.ts +0 -5
  126. package/src/__tests__/prompt-cache-cross-turn-stability.test.ts +0 -17
  127. package/src/__tests__/qdrant-manager.test.ts +6 -0
  128. package/src/__tests__/reaction-persistence.test.ts +58 -62
  129. package/src/__tests__/retry-thinking-adaptive-only.test.ts +67 -0
  130. package/src/__tests__/server-history-render.test.ts +22 -0
  131. package/src/__tests__/sight-frame-retention.test.ts +362 -0
  132. package/src/__tests__/slack-reaction-approvals.test.ts +44 -24
  133. package/src/__tests__/slack-reaction-guardian-approval.test.ts +29 -1
  134. package/src/__tests__/subagent-fork-notifications.test.ts +2 -0
  135. package/src/__tests__/subagent-manager-notify.test.ts +2 -0
  136. package/src/__tests__/subagent-notify-parent.test.ts +2 -0
  137. package/src/__tests__/subagent-notify-stale-rebuild.test.ts +85 -0
  138. package/src/__tests__/subagent-tool-gate-mode.test.ts +4 -4
  139. package/src/__tests__/terminal-tools.test.ts +9 -9
  140. package/src/__tests__/tool-result-spool.test.ts +5 -8
  141. package/src/__tests__/usage-attribution.test.ts +26 -0
  142. package/src/acp/__tests__/acp-auth-marker-store.test.ts +191 -0
  143. package/src/acp/__tests__/acp-claude-oauth.test.ts +262 -0
  144. package/src/acp/__tests__/acp-connect-card-state.test.ts +17 -0
  145. package/src/acp/__tests__/helpers/acp-history-db.ts +10 -3
  146. package/src/acp/acp-auth-marker-store.ts +196 -0
  147. package/src/acp/acp-claude-oauth.ts +154 -4
  148. package/src/acp/acp-connect-card-state.ts +49 -8
  149. package/src/acp/agent-process.ts +1 -0
  150. package/src/acp/auto-install.ts +1 -0
  151. package/src/acp/client-handler.ts +1 -0
  152. package/src/acp/prepare-agent-env.ts +79 -1
  153. package/src/acp/resolve-agent.ts +12 -0
  154. package/src/acp/session-manager.test.ts +183 -0
  155. package/src/acp/session-manager.ts +110 -6
  156. package/src/acp/types.ts +17 -0
  157. package/src/agent/attachments.ts +7 -2
  158. package/src/agent/loop.ts +59 -1
  159. package/src/api/events/contact-form-closed.ts +32 -0
  160. package/src/api/events/contact-record-request.ts +63 -0
  161. package/src/api/events/contact-request.ts +10 -3
  162. package/src/api/index.ts +20 -0
  163. package/src/api/responses/conversation-message.ts +20 -0
  164. package/src/api/responses/home.ts +101 -0
  165. package/src/approvals/AGENTS.md +1 -1
  166. package/src/approvals/guardian-card-withdrawal.ts +127 -30
  167. package/src/approvals/guardian-decision-primitive.ts +9 -0
  168. package/src/approvals/guardian-expiry-notifier.ts +22 -16
  169. package/src/approvals/guardian-request-status-sync.ts +98 -0
  170. package/src/avatar/svg-compositor.ts +1 -1
  171. package/src/avatar/traits-png-sync.ts +1 -1
  172. package/src/bundler/app-compiler.ts +1 -0
  173. package/src/bundler/compiler-tools.ts +1 -0
  174. package/src/bundler/package-resolver.ts +1 -0
  175. package/src/calls/__tests__/voice-session-bridge.test.ts +183 -3
  176. package/src/calls/guardian-action-sweep.ts +6 -1
  177. package/src/calls/voice-session-bridge.ts +46 -1
  178. package/src/channels/__tests__/gateway-guardian-requests.test.ts +13 -18
  179. package/src/channels/gateway-guardian-requests.ts +37 -18
  180. package/src/cli/__tests__/contacts-record-prompt-cli.test.ts +329 -0
  181. package/src/cli/commands/__tests__/browser.test.ts +65 -1
  182. package/src/cli/commands/browser.ts +30 -10
  183. package/src/cli/commands/contacts.help.ts +173 -11
  184. package/src/cli/commands/contacts.ts +343 -4
  185. package/src/cli/commands/db/refresh.ts +1 -0
  186. package/src/cli/commands/oauth/index.help.ts +6 -3
  187. package/src/cli/commands/oauth/request.test.ts +37 -0
  188. package/src/cli/commands/oauth/request.ts +7 -6
  189. package/src/cli/lib/bundled-marketplace.json +1 -1
  190. package/src/cli/lib/install-from-github.ts +20 -19
  191. package/src/cli/lib/install-plugin-dependencies.ts +3 -5
  192. package/src/cli/lib/merge-plugin-tree.ts +1 -0
  193. package/src/cli/lib/publish-plugin.ts +4 -1
  194. package/src/config/__tests__/balanced-model-experiment.test.ts +63 -31
  195. package/src/config/__tests__/default-profile-catalog.test.ts +2 -2
  196. package/src/config/balanced-model-experiment.ts +15 -9
  197. package/src/config/bundled-skills/contacts/SKILL.md +61 -18
  198. package/src/config/default-profile-catalog.ts +12 -11
  199. package/src/config/feature-flag-registry.json +22 -13
  200. package/src/config/schema.ts +2 -0
  201. package/src/config/schemas/__tests__/live-voice.test.ts +13 -0
  202. package/src/config/schemas/live-voice.ts +7 -0
  203. package/src/config/schemas/sight.ts +45 -0
  204. package/src/config/skills.ts +2 -1
  205. package/src/contacts/__tests__/mirror-reconciler.test.ts +352 -0
  206. package/src/contacts/contact-store.ts +47 -4
  207. package/src/contacts/contacts-write.ts +5 -2
  208. package/src/contacts/mirror-reconciler.ts +210 -0
  209. package/src/contacts/mirror-tombstones.ts +42 -0
  210. package/src/contacts/notify-contacts-changed.ts +11 -2
  211. package/src/context/compactor.ts +47 -19
  212. package/src/context/tool-result-spool.ts +21 -14
  213. package/src/conversations/message-consolidation.ts +2 -2
  214. package/src/credential-health/credential-health-service.ts +37 -6
  215. package/src/daemon/conversation-agent-loop-handlers.ts +19 -2
  216. package/src/daemon/conversation-agent-loop.ts +100 -1
  217. package/src/daemon/conversation-media-retry.ts +11 -6
  218. package/src/daemon/conversation-messaging.ts +293 -30
  219. package/src/daemon/conversation-runtime-assembly.ts +67 -0
  220. package/src/daemon/conversation-sight-frames.ts +187 -0
  221. package/src/daemon/conversation-store.ts +18 -28
  222. package/src/daemon/conversation-surfaces.ts +12 -2
  223. package/src/daemon/conversation-tool-setup.ts +39 -10
  224. package/src/daemon/conversation.ts +159 -16
  225. package/src/daemon/daemon-control.ts +6 -17
  226. package/src/daemon/handlers/config-model.test.ts +1 -0
  227. package/src/daemon/handlers/conversations.ts +10 -11
  228. package/src/daemon/install-assistant-command.ts +1 -0
  229. package/src/daemon/lifecycle.ts +10 -13
  230. package/src/daemon/message-types/sync.ts +4 -0
  231. package/src/daemon/process-message.ts +2 -17
  232. package/src/daemon/reaction-history-render.test.ts +162 -0
  233. package/src/daemon/reaction-history-render.ts +115 -0
  234. package/src/daemon/reaction-record.test.ts +162 -0
  235. package/src/daemon/reaction-record.ts +83 -0
  236. package/src/daemon/shutdown-handlers.ts +0 -2
  237. package/src/daemon/summarize-boundary.ts +12 -0
  238. package/src/daemon/video-thumbnail.ts +1 -1
  239. package/src/home/feed-types.ts +4 -0
  240. package/src/home/feed-writer.ts +25 -0
  241. package/src/live-voice/__tests__/live-voice-attach-frame.test.ts +679 -0
  242. package/src/live-voice/__tests__/live-voice-attach-image.test.ts +8 -5
  243. package/src/live-voice/__tests__/live-voice-photo.test.ts +637 -1
  244. package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +787 -0
  245. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +347 -0
  246. package/src/live-voice/__tests__/live-voice-vad.test.ts +109 -0
  247. package/src/live-voice/live-voice-photo.ts +420 -65
  248. package/src/live-voice/live-voice-session.ts +311 -27
  249. package/src/live-voice/protocol.ts +147 -11
  250. package/src/messaging/provider-message-metadata.ts +7 -0
  251. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +113 -1
  252. package/src/messaging/providers/channel-transport.ts +37 -0
  253. package/src/messaging/providers/discord/api.ts +4 -4
  254. package/src/messaging/providers/discord/send.test.ts +177 -2
  255. package/src/messaging/providers/discord/send.ts +197 -10
  256. package/src/messaging/providers/discord/transport.ts +10 -0
  257. package/src/messaging/providers/index.ts +42 -0
  258. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +129 -3
  259. package/src/messaging/providers/slack/__tests__/auth.test.ts +35 -0
  260. package/src/messaging/providers/slack/__tests__/message-content.test.ts +152 -0
  261. package/src/messaging/providers/slack/adapter.ts +112 -27
  262. package/src/messaging/providers/slack/api.test.ts +10 -0
  263. package/src/messaging/providers/slack/auth.ts +32 -6
  264. package/src/messaging/providers/slack/client.ts +0 -15
  265. package/src/messaging/providers/slack/message-content.ts +137 -0
  266. package/src/messaging/providers/slack/send.test.ts +43 -2
  267. package/src/messaging/providers/slack/send.ts +18 -5
  268. package/src/messaging/providers/slack/transport.ts +10 -0
  269. package/src/messaging/providers/slack/types.ts +21 -2
  270. package/src/messaging/providers/telegram-bot/adapter.ts +5 -1
  271. package/src/messaging/providers/telegram-bot/send.test.ts +49 -2
  272. package/src/messaging/providers/telegram-bot/send.ts +41 -1
  273. package/src/messaging/providers/telegram-bot/transport.ts +10 -0
  274. package/src/messaging/providers/telegram-bot/withdraw.ts +13 -5
  275. package/src/messaging/reaction-envelopes.ts +79 -0
  276. package/src/monitoring/control.ts +2 -2
  277. package/src/monitoring/page-cache.ts +5 -1
  278. package/src/notifications/AGENTS.md +1 -1
  279. package/src/notifications/__tests__/guardian-feed-projection.test.ts +360 -0
  280. package/src/notifications/__tests__/home-feed-side-effect.test.ts +8 -0
  281. package/src/notifications/adapters/discord.ts +62 -9
  282. package/src/notifications/conversation-pairing.ts +31 -4
  283. package/src/notifications/emit-signal.ts +8 -5
  284. package/src/notifications/guardian-delivery-recorder.ts +47 -6
  285. package/src/notifications/guardian-feed-projection.ts +492 -0
  286. package/src/notifications/home-feed-side-effect.ts +59 -11
  287. package/src/oauth/byo-connection.test.ts +22 -0
  288. package/src/oauth/byo-connection.ts +11 -5
  289. package/src/oauth/connection.ts +3 -2
  290. package/src/oauth/platform-connection.test.ts +35 -0
  291. package/src/oauth/platform-connection.ts +18 -13
  292. package/src/oauth/scope-utils.ts +48 -0
  293. package/src/oauth/seed-providers.ts +12 -0
  294. package/src/outbound-proxy/certs.ts +5 -4
  295. package/src/permissions/question-guardian-request.test.ts +7 -1
  296. package/src/persistence/attachments-store.ts +134 -31
  297. package/src/persistence/conversation-crud.ts +137 -4
  298. package/src/persistence/conversation-types.ts +109 -0
  299. package/src/persistence/db-connection.ts +3 -21
  300. package/src/persistence/delivery-crud.ts +42 -12
  301. package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +15 -2
  302. package/src/persistence/embeddings/embedding-local.ts +7 -112
  303. package/src/persistence/embeddings/qdrant-manager.ts +4 -1
  304. package/src/persistence/llm-request-log-store.ts +1 -1
  305. package/src/persistence/migrations/370-acp-session-history-auth-error-code.ts +36 -0
  306. package/src/persistence/migrations/371-acp-session-history-auth-error-credential.ts +40 -0
  307. package/src/persistence/migrations/372-create-acp-refused-credentials.ts +34 -0
  308. package/src/persistence/migrations/373-acp-auth-marker-index.ts +27 -0
  309. package/src/persistence/migrations/__tests__/370-acp-session-history-auth-error-code.test.ts +65 -0
  310. package/src/persistence/migrations/__tests__/371-acp-session-history-auth-error-credential.test.ts +68 -0
  311. package/src/persistence/migrations/__tests__/372-create-acp-refused-credentials.test.ts +64 -0
  312. package/src/persistence/migrations/__tests__/373-acp-auth-marker-index.test.ts +82 -0
  313. package/src/persistence/schema/acp.ts +25 -0
  314. package/src/persistence/steps.ts +8 -0
  315. package/src/platform/platform-patch-queue.test.ts +43 -41
  316. package/src/plugin-api/index.ts +12 -0
  317. package/src/plugins/defaults/compaction/window-manager.ts +2 -0
  318. package/src/plugins/defaults/image-recovery/recover.ts +12 -1
  319. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +166 -1
  320. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +3 -0
  321. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +3 -0
  322. package/src/plugins/defaults/memory/jobs-worker.ts +1 -2
  323. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +53 -9
  324. package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +4 -7
  325. package/src/plugins/defaults/memory/src/memory-worker-routes.ts +1 -1
  326. package/src/plugins/defaults/memory/v1/job-handlers/index-maintenance.ts +15 -1
  327. package/src/plugins/defaults/memory/worker-control.ts +1 -2
  328. package/src/providers/content-block-schema.ts +1 -0
  329. package/src/providers/fireworks/client.ts +9 -1
  330. package/src/providers/gemini/client.ts +22 -15
  331. package/src/providers/media-resolve.ts +24 -10
  332. package/src/providers/model-catalog.ts +91 -0
  333. package/src/providers/openai/chat-completions-provider.ts +44 -5
  334. package/src/providers/retry.ts +16 -11
  335. package/src/providers/types.ts +56 -0
  336. package/src/routes/__tests__/control.test.ts +4 -3
  337. package/src/routes/control.ts +1 -1
  338. package/src/routes/route-host-client.ts +1 -1
  339. package/src/runtime/AGENTS.md +30 -12
  340. package/src/runtime/__tests__/agent-wake.test.ts +71 -6
  341. package/src/runtime/__tests__/guardian-form-registry.test.ts +242 -0
  342. package/src/runtime/access-request-helper.ts +15 -7
  343. package/src/runtime/agent-wake.ts +16 -4
  344. package/src/runtime/channel-approval-types.ts +8 -5
  345. package/src/runtime/channel-invite-transport.ts +2 -0
  346. package/src/runtime/channel-invite-transports/discord.ts +27 -0
  347. package/src/runtime/channel-readiness-service.ts +4 -4
  348. package/src/runtime/channel-reply-delivery.ts +72 -28
  349. package/src/runtime/guardian-action-service.ts +9 -5
  350. package/src/runtime/guardian-form-registry.ts +256 -0
  351. package/src/runtime/guardian-reply-router.ts +33 -14
  352. package/src/runtime/invite-instruction-generator.ts +14 -2
  353. package/src/runtime/migrations/vbundle-metadata-merge.ts +4 -7
  354. package/src/runtime/no-response.ts +13 -74
  355. package/src/runtime/routes/__tests__/acp-routes.test.ts +527 -0
  356. package/src/runtime/routes/__tests__/app-management-shared-apps.test.ts +74 -0
  357. package/src/runtime/routes/__tests__/contact-form-kinds.test.ts +22 -0
  358. package/src/runtime/routes/__tests__/contact-record-prompt-route.test.ts +303 -0
  359. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +3 -4
  360. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +6 -0
  361. package/src/runtime/routes/__tests__/route-host-worker-routes.test.ts +3 -4
  362. package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +5 -6
  363. package/src/runtime/routes/acp-routes-list.test.ts +58 -0
  364. package/src/runtime/routes/acp-routes.ts +253 -47
  365. package/src/runtime/routes/app-management-routes.ts +80 -47
  366. package/src/runtime/routes/approval-routes.ts +9 -0
  367. package/src/runtime/routes/archive-utils.ts +6 -1
  368. package/src/runtime/routes/avatar-routes.ts +1 -1
  369. package/src/runtime/routes/channel-route-shared.ts +18 -20
  370. package/src/runtime/routes/contact-prompt-routes.ts +271 -105
  371. package/src/runtime/routes/conversation-routes.ts +41 -32
  372. package/src/runtime/routes/credential-prompt-routes.ts +10 -1
  373. package/src/runtime/routes/debug-bash-routes.ts +1 -0
  374. package/src/runtime/routes/guardian-expiry-sweep.ts +89 -31
  375. package/src/runtime/routes/guardian-form-routes.ts +115 -0
  376. package/src/runtime/routes/inbound-message-handler.ts +82 -29
  377. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -3
  378. package/src/runtime/routes/inbound-stages/edit-intercept.ts +6 -0
  379. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +6 -0
  380. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +6 -1
  381. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +50 -17
  382. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +97 -94
  383. package/src/runtime/routes/index.ts +2 -0
  384. package/src/runtime/routes/integrations/slack/channel.ts +39 -18
  385. package/src/runtime/routes/migration-routes.ts +1 -3
  386. package/src/runtime/routes/monitoring-routes.ts +0 -3
  387. package/src/runtime/routes/oauth-commands-routes.ts +18 -5
  388. package/src/runtime/routes/route-host-worker-routes.ts +1 -1
  389. package/src/runtime/routes/schedule-worker-routes.ts +1 -1
  390. package/src/runtime/tool-grant-request-helper.ts +4 -0
  391. package/src/schedule/run-script.ts +1 -0
  392. package/src/schedule/scheduler.ts +1 -1
  393. package/src/schedule/worker-control.ts +2 -2
  394. package/src/security/oauth2.ts +2 -11
  395. package/src/security/secure-keys.ts +55 -8
  396. package/src/skills/catalog-install.ts +7 -4
  397. package/src/skills/clawhub.ts +1 -0
  398. package/src/skills/inline-command-runner.ts +1 -0
  399. package/src/stt/__tests__/room-noise-floor.test.ts +108 -0
  400. package/src/stt/room-noise-floor.ts +140 -0
  401. package/src/subagent/notify.ts +49 -3
  402. package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
  403. package/src/tools/acp/spawn.ts +2 -2
  404. package/src/tools/browser/__tests__/browser-status.test.ts +9 -0
  405. package/src/tools/browser/browser-execution.ts +10 -43
  406. package/src/tools/browser/browser-manager.ts +1 -0
  407. package/src/tools/browser/browser-status-constants.ts +6 -5
  408. package/src/tools/browser/runtime-check.ts +2 -0
  409. package/src/tools/channel/react-to-message.test.ts +193 -0
  410. package/src/tools/channel/react-to-message.ts +159 -0
  411. package/src/tools/credentials/metadata-store.ts +115 -19
  412. package/src/tools/credentials/store.ts +0 -2
  413. package/src/tools/filesystem/read.ts +2 -2
  414. package/src/tools/host-filesystem/read.ts +2 -2
  415. package/src/tools/host-terminal/host-shell.test.ts +24 -0
  416. package/src/tools/host-terminal/host-shell.ts +11 -38
  417. package/src/tools/shared/filesystem/file-ops-service.ts +10 -7
  418. package/src/tools/shared/shell-output.test.ts +104 -0
  419. package/src/tools/shared/shell-output.ts +112 -39
  420. package/src/tools/skills/sandbox-runner.ts +1 -0
  421. package/src/tools/system/request-permission.test.ts +34 -0
  422. package/src/tools/system/request-permission.ts +29 -5
  423. package/src/tools/terminal/shell.test.ts +63 -0
  424. package/src/tools/terminal/shell.ts +12 -44
  425. package/src/tools/tool-input-schemas.ts +2 -0
  426. package/src/tools/tool-manifest.ts +2 -0
  427. package/src/usage/attribution.ts +12 -4
  428. package/src/util/__tests__/worker-ownership.test.ts +77 -0
  429. package/src/util/__tests__/worker-process-command.test.ts +246 -1
  430. package/src/util/bun-runtime.ts +44 -13
  431. package/src/util/bundled-asset.ts +9 -1
  432. package/src/util/disk-usage.ts +1 -0
  433. package/src/util/file-use.ts +4 -1
  434. package/src/util/guardian-form-timeouts.ts +50 -0
  435. package/src/util/image-conversion.ts +1 -0
  436. package/src/util/oauth-request-body.test.ts +59 -0
  437. package/src/util/oauth-request-body.ts +56 -5
  438. package/src/util/platform.ts +69 -73
  439. package/src/util/process-table.ts +16 -5
  440. package/src/util/read-stdin.ts +5 -0
  441. package/src/util/spawn.ts +1 -0
  442. package/src/util/sqlite3-runtime.ts +25 -5
  443. package/src/util/worker-ownership.ts +193 -0
  444. package/src/util/worker-process.ts +375 -37
  445. package/src/workspace/git-service.ts +4 -12
  446. package/src/workspace/migrations/010-app-dir-rename.ts +1 -0
@@ -0,0 +1,360 @@
1
+ import * as fs from "node:fs";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import {
6
+ afterEach,
7
+ beforeEach,
8
+ describe,
9
+ expect,
10
+ mock,
11
+ spyOn,
12
+ test,
13
+ } from "bun:test";
14
+
15
+ // The feed writer publishes `home_feed_updated` on every write; the hub is
16
+ // stubbed so tests run without an SSE server. Mocked before the writer
17
+ // module is imported so the import picks it up.
18
+ mock.module("../../runtime/assistant-event-hub.js", () => ({
19
+ assistantEventHub: {
20
+ publish: async () => {},
21
+ subscribe: () => () => {},
22
+ },
23
+ broadcastMessage: () => {},
24
+ }));
25
+
26
+ // Gateway reads used by reconciliation. Stubs are swapped per test; the
27
+ // spread keeps every other export real so transitive importers keep
28
+ // working (partial factories break at import time).
29
+ const actualGateway =
30
+ await import("../../channels/gateway-guardian-requests.js");
31
+ let listGuardianRequestsStub: () => Promise<unknown[]> = async () => [];
32
+ let getGuardianRequestStub: (id: string) => Promise<unknown> = async () => null;
33
+ mock.module("../../channels/gateway-guardian-requests.js", () => ({
34
+ ...actualGateway,
35
+ listGuardianRequests: (...args: unknown[]) =>
36
+ (listGuardianRequestsStub as (...a: unknown[]) => Promise<unknown[]>)(
37
+ ...args,
38
+ ),
39
+ getGuardianRequest: (id: string) => getGuardianRequestStub(id),
40
+ }));
41
+
42
+ const {
43
+ buildPendingGuardianProjection,
44
+ guardianFeedItemId,
45
+ reconcileGuardianFeedProjections,
46
+ requestIdFromGuardianFeedItemId,
47
+ writeGuardianFeedReceipt,
48
+ } = await import("../guardian-feed-projection.js");
49
+ const { appendFeedItem, bulkSetFeedItemStatus, getHomeFeedPath, readHomeFeed } =
50
+ await import("../../home/feed-writer.js");
51
+ const { isPendingGuardianFeedItem } =
52
+ await import("../../api/responses/home.js");
53
+ type FeedItem = import("../../api/responses/home.js").FeedItem;
54
+
55
+ let workspaceDir: string;
56
+ let origWorkspaceDir: string | undefined;
57
+
58
+ beforeEach(() => {
59
+ workspaceDir = mkdtempSync(join(tmpdir(), "vellum-gfp-"));
60
+ origWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR;
61
+ process.env.VELLUM_WORKSPACE_DIR = workspaceDir;
62
+ listGuardianRequestsStub = async () => [];
63
+ getGuardianRequestStub = async () => null;
64
+ });
65
+
66
+ afterEach(() => {
67
+ if (origWorkspaceDir === undefined) {
68
+ delete process.env.VELLUM_WORKSPACE_DIR;
69
+ } else {
70
+ process.env.VELLUM_WORKSPACE_DIR = origWorkspaceDir;
71
+ }
72
+ rmSync(workspaceDir, { recursive: true, force: true });
73
+ });
74
+
75
+ const toolApprovalPayload = {
76
+ requestKind: "tool_approval",
77
+ requestId: "req-1",
78
+ requestCode: "ABC123",
79
+ questionText: "Alice asked the assistant to look up an issue",
80
+ toolName: "linear_graphql",
81
+ sourceChannel: "slack",
82
+ sourceChatId: "C0123456789",
83
+ requesterIdentifier: "Alice",
84
+ };
85
+
86
+ function pendingGuardianItem(requestId: string): FeedItem {
87
+ const projection = buildPendingGuardianProjection({
88
+ ...toolApprovalPayload,
89
+ requestId,
90
+ });
91
+ if (!projection) {
92
+ throw new Error("projection should build");
93
+ }
94
+ return {
95
+ id: guardianFeedItemId(requestId),
96
+ type: "notification",
97
+ priority: 50,
98
+ summary: "Alice asked the assistant to look up an issue",
99
+ timestamp: "2026-08-31T12:00:00.000Z",
100
+ createdAt: "2026-08-31T12:00:00.000Z",
101
+ status: "new",
102
+ urgency: "high",
103
+ guardianRequest: projection,
104
+ };
105
+ }
106
+
107
+ describe("buildPendingGuardianProjection", () => {
108
+ test("tool approval projects as a pending approval with source facts", () => {
109
+ const projection = buildPendingGuardianProjection(toolApprovalPayload);
110
+ expect(projection).toMatchObject({
111
+ requestId: "req-1",
112
+ kind: "tool_approval",
113
+ intent: "approval",
114
+ status: "pending",
115
+ requesterLabel: "Alice",
116
+ toolName: "linear_graphql",
117
+ sourceChannel: "slack",
118
+ sourceContextLabel: "Slack #C0123456789",
119
+ });
120
+ });
121
+
122
+ test("a question without a tool projects as question intent", () => {
123
+ const projection = buildPendingGuardianProjection({
124
+ requestKind: "pending_question",
125
+ requestId: "req-q",
126
+ requestCode: "Q1",
127
+ questionText: "Which venue should I book?",
128
+ });
129
+ expect(projection?.intent).toBe("question");
130
+ expect(projection?.status).toBe("pending");
131
+ });
132
+
133
+ test("a sent Slack delivery yields the card deep link", () => {
134
+ const projection = buildPendingGuardianProjection(toolApprovalPayload, {
135
+ channel: "slack",
136
+ destination: "D0AAAAAAAAA",
137
+ status: "sent",
138
+ messageId: "1725100000.000100",
139
+ });
140
+ expect(projection?.slackCardUrl).toContain("D0AAAAAAAAA");
141
+ });
142
+
143
+ test("a payload without a requestId projects nothing", () => {
144
+ expect(
145
+ buildPendingGuardianProjection({ requestKind: "tool_approval" }),
146
+ ).toBeNull();
147
+ });
148
+
149
+ test("an access-request payload projects with the event's implied kind", () => {
150
+ const projection = buildPendingGuardianProjection(
151
+ {
152
+ requestId: "req-a",
153
+ requestCode: "AC1234",
154
+ sourceChannel: "telegram",
155
+ senderIdentifier: "Alice",
156
+ },
157
+ undefined,
158
+ "access_request",
159
+ );
160
+ expect(projection).toMatchObject({
161
+ requestId: "req-a",
162
+ kind: "access_request",
163
+ intent: "approval",
164
+ status: "pending",
165
+ requesterLabel: "Alice",
166
+ });
167
+ });
168
+ });
169
+
170
+ describe("guardianFeedItemId", () => {
171
+ test("round-trips through requestIdFromGuardianFeedItemId", () => {
172
+ expect(requestIdFromGuardianFeedItemId(guardianFeedItemId("req-9"))).toBe(
173
+ "req-9",
174
+ );
175
+ expect(requestIdFromGuardianFeedItemId("notif:xyz")).toBeNull();
176
+ });
177
+ });
178
+
179
+ describe("bulk-dismiss protection", () => {
180
+ test("clear-all skips the pending item and clears its receipt", async () => {
181
+ await appendFeedItem(pendingGuardianItem("req-1"));
182
+ await appendFeedItem({
183
+ id: "notif:other",
184
+ type: "notification",
185
+ priority: 50,
186
+ summary: "routine",
187
+ timestamp: "2026-08-31T12:00:00.000Z",
188
+ createdAt: "2026-08-31T12:00:00.000Z",
189
+ status: "seen",
190
+ });
191
+
192
+ const first = await bulkSetFeedItemStatus(
193
+ ["new", "seen", "acted_on"],
194
+ "dismissed",
195
+ );
196
+ let items = readHomeFeed().items;
197
+ const guardianItem = items.find(
198
+ (i) => i.id === guardianFeedItemId("req-1"),
199
+ );
200
+ // Exactly the routine item flipped; the pending projection is intact
201
+ // and still actionable.
202
+ expect(first).toBe(1);
203
+ expect(guardianItem?.status).toBe("new");
204
+ expect(items.find((i) => i.id === "notif:other")?.status).toBe("dismissed");
205
+ expect(guardianItem && isPendingGuardianFeedItem(guardianItem)).toBe(true);
206
+
207
+ // Sensitivity: once the request resolves, the same bulk op clears it.
208
+ await writeGuardianFeedReceipt({ requestId: "req-1", status: "approved" });
209
+ const second = await bulkSetFeedItemStatus(
210
+ ["new", "seen", "acted_on"],
211
+ "dismissed",
212
+ );
213
+ items = readHomeFeed().items;
214
+ expect(second).toBe(1);
215
+ expect(
216
+ items.find((i) => i.id === guardianFeedItemId("req-1"))?.status,
217
+ ).toBe("dismissed");
218
+ });
219
+
220
+ test("bulk read-marking never dismisses but may still touch guardian items", async () => {
221
+ await appendFeedItem(pendingGuardianItem("req-2"));
222
+ const updated = await bulkSetFeedItemStatus(["new"], "seen");
223
+ // Only dismissal is protected; a bulk `seen` transition (not offered
224
+ // for guardian rows by the web client, which excludes their ids) is
225
+ // not blocked by the writer.
226
+ expect(updated).toBe(1);
227
+ });
228
+ });
229
+
230
+ describe("writeGuardianFeedReceipt", () => {
231
+ test("a decision rewrites the item into a clearable receipt", async () => {
232
+ await appendFeedItem(pendingGuardianItem("req-3"));
233
+ const ok = await writeGuardianFeedReceipt({
234
+ requestId: "req-3",
235
+ status: "approved",
236
+ decidedAction: "approve_once",
237
+ decidedAtMs: Date.parse("2026-08-31T13:00:00.000Z"),
238
+ });
239
+ expect(ok).toBe(true);
240
+
241
+ const item = readHomeFeed().items.find(
242
+ (i) => i.id === guardianFeedItemId("req-3"),
243
+ );
244
+ expect(item?.guardianRequest).toMatchObject({
245
+ status: "approved",
246
+ decidedAction: "approve_once",
247
+ decidedAt: "2026-08-31T13:00:00.000Z",
248
+ });
249
+ expect(item?.urgency).toBe("medium");
250
+ expect(item && isPendingGuardianFeedItem(item)).toBe(false);
251
+ });
252
+
253
+ test("a superseded auto-deny carries its reason and no decider", async () => {
254
+ await appendFeedItem(pendingGuardianItem("req-4"));
255
+ await writeGuardianFeedReceipt({
256
+ requestId: "req-4",
257
+ status: "denied",
258
+ decidedAction: "reject",
259
+ terminalReason: "superseded",
260
+ });
261
+ const item = readHomeFeed().items.find(
262
+ (i) => i.id === guardianFeedItemId("req-4"),
263
+ );
264
+ expect(item?.guardianRequest?.terminalReason).toBe("superseded");
265
+ expect(item?.guardianRequest?.decidedByLabel).toBeUndefined();
266
+ });
267
+
268
+ test("a request with no item resolves true (nothing to retry)", async () => {
269
+ expect(
270
+ await writeGuardianFeedReceipt({ requestId: "ghost", status: "expired" }),
271
+ ).toBe(true);
272
+ });
273
+
274
+ test("a lost write on an existing item resolves false so callers retry", async () => {
275
+ await appendFeedItem(pendingGuardianItem("req-5"));
276
+ // Fail the next feed-file write while the item is still present in
277
+ // the read path, so the missing-item and lost-write cases separate.
278
+ const feedPath = getHomeFeedPath();
279
+ const originalWrite = fs.writeFileSync;
280
+ const spy = spyOn(fs, "writeFileSync").mockImplementation(((
281
+ path: fs.PathOrFileDescriptor,
282
+ data: string | NodeJS.ArrayBufferView,
283
+ options?: fs.WriteFileOptions,
284
+ ) => {
285
+ if (typeof path === "string" && path === feedPath) {
286
+ throw new Error("Simulated write failure");
287
+ }
288
+ return originalWrite(path, data, options);
289
+ }) as typeof fs.writeFileSync);
290
+ try {
291
+ expect(
292
+ await writeGuardianFeedReceipt({
293
+ requestId: "req-5",
294
+ status: "expired",
295
+ }),
296
+ ).toBe(false);
297
+ } finally {
298
+ spy.mockRestore();
299
+ }
300
+ });
301
+ });
302
+
303
+ describe("reconcileGuardianFeedProjections", () => {
304
+ const wireRequest = (overrides: Record<string, unknown>) => ({
305
+ id: "req-r",
306
+ kind: "tool_approval",
307
+ sourceType: "channel",
308
+ sourceChannel: "slack",
309
+ sourceConversationId: "conv-1",
310
+ requesterExternalUserId: "U1",
311
+ requesterChatId: null,
312
+ guardianExternalUserId: null,
313
+ guardianPrincipalId: null,
314
+ callSessionId: null,
315
+ pendingQuestionId: null,
316
+ questionText: "Alice asked the assistant to look up an issue",
317
+ requestCode: "ABC123",
318
+ toolName: "linear_graphql",
319
+ inputDigest: null,
320
+ commandPreview: null,
321
+ riskLevel: null,
322
+ activityText: null,
323
+ executionTarget: null,
324
+ requesterSignals: null,
325
+ requestTrigger: null,
326
+ status: "pending",
327
+ answerText: null,
328
+ decidedByExternalUserId: null,
329
+ decidedByPrincipalId: null,
330
+ followupState: null,
331
+ expiresAt: null,
332
+ createdAt: Date.parse("2026-08-31T12:00:00.000Z"),
333
+ updatedAt: Date.parse("2026-08-31T12:00:00.000Z"),
334
+ ...overrides,
335
+ });
336
+
337
+ test("backfills an item for a pending request that has none", async () => {
338
+ listGuardianRequestsStub = async () => [wireRequest({ id: "req-r" })];
339
+ await reconcileGuardianFeedProjections();
340
+ const item = readHomeFeed().items.find(
341
+ (i) => i.id === guardianFeedItemId("req-r"),
342
+ );
343
+ expect(item?.guardianRequest?.status).toBe("pending");
344
+ expect(item?.conversationId).toBe("conv-1");
345
+ expect(item?.detailPanel?.kind).toBe("permissionChat");
346
+ });
347
+
348
+ test("receipts an actionable item whose request went terminal", async () => {
349
+ await appendFeedItem(pendingGuardianItem("req-t"));
350
+ listGuardianRequestsStub = async () => [];
351
+ getGuardianRequestStub = async () =>
352
+ wireRequest({ id: "req-t", status: "denied" });
353
+ await reconcileGuardianFeedProjections();
354
+ const item = readHomeFeed().items.find(
355
+ (i) => i.id === guardianFeedItemId("req-t"),
356
+ );
357
+ expect(item?.guardianRequest?.status).toBe("denied");
358
+ expect(item && isPendingGuardianFeedItem(item)).toBe(false);
359
+ });
360
+ });
@@ -40,7 +40,13 @@ mock.module("../../runtime/sync/resource-sync-events.js", () => ({
40
40
  },
41
41
  }));
42
42
 
43
+ // Both factories spread the actual module: the module under test reaches
44
+ // feed-writer through `guardian-feed-projection` too, and the real
45
+ // feed-writer needs `parseFeedFile` from feed-types, so a partial factory
46
+ // would fail at import time on any export it omitted.
47
+ const actualFeedTypes = await import("../../home/feed-types.js");
43
48
  mock.module("../../home/feed-types.js", () => ({
49
+ ...actualFeedTypes,
44
50
  feedItemSchema: {
45
51
  parse: (item: unknown) => {
46
52
  if (feedItemSchemaShouldReject) {
@@ -51,7 +57,9 @@ mock.module("../../home/feed-types.js", () => ({
51
57
  },
52
58
  }));
53
59
 
60
+ const actualFeedWriter = await import("../../home/feed-writer.js");
54
61
  mock.module("../../home/feed-writer.js", () => ({
62
+ ...actualFeedWriter,
55
63
  appendFeedItem: async (item: FeedItem) => {
56
64
  appendCalls.push(item);
57
65
  },
@@ -8,14 +8,15 @@
8
8
  * (cached per user) and a room id on the binding can never become a delivery
9
9
  * target. That is Slack's D-prefix DM gate, achieved by construction.
10
10
  *
11
- * Approval notifications carry the typed-command instructions instead of
12
- * component buttons: nothing ingests INTERACTION_CREATE yet, and a button
13
- * whose press goes nowhere is a dead control. When interaction ingest lands,
14
- * buttons arrive here as a rendering upgrade, not a new decision path.
11
+ * Approval notifications render component buttons whose presses the gateway
12
+ * ingests as button events on the shared `apr:` callback convention; a
13
+ * rendering failure falls back to the plain-text card with typed-command
14
+ * instructions, so a card is always actionable one way or the other.
15
15
  */
16
16
 
17
17
  import { openDiscordDmChannel } from "../../messaging/providers/discord/api.js";
18
18
  import {
19
+ DiscordPartialSendError,
19
20
  editDiscordMessage,
20
21
  sendDiscordReply,
21
22
  } from "../../messaging/providers/discord/send.js";
@@ -53,14 +54,66 @@ export class DiscordAdapter implements ChannelAdapter {
53
54
  };
54
55
  }
55
56
 
56
- const text = appendPlainTextFallback(
57
- resolveMessageText(payload),
58
- payload.approvalContext,
59
- );
57
+ const messageText = resolveMessageText(payload);
58
+ const approval = payload.approvalContext;
60
59
 
61
60
  try {
62
61
  const channelId = await openDiscordDmChannel(guardianUserId);
63
- const sent = await sendDiscordReply({ channelId }, text);
62
+
63
+ if (approval) {
64
+ // Attempt rich delivery with component buttons; on failure, fall
65
+ // back to the plain-text card below.
66
+ try {
67
+ const sent = await sendDiscordReply(
68
+ { channelId },
69
+ messageText,
70
+ approval,
71
+ );
72
+ log.info(
73
+ { sourceEventName: payload.sourceEventName, guardianUserId },
74
+ "Discord approval notification delivered with buttons",
75
+ );
76
+ // The message id lets the delivery row address the card later
77
+ // (reactions, button presses, in-place withdrawal).
78
+ return { success: true, messageId: sent.lastMessageId };
79
+ } catch (richErr) {
80
+ if (richErr instanceof DiscordPartialSendError) {
81
+ // The leading chunks are delivered and cannot be unsent, so a
82
+ // full plain-text fallback would duplicate them. Complete the
83
+ // card instead: the undelivered remainder plus the typed-command
84
+ // instructions, whose message becomes the card's address.
85
+ log.warn(
86
+ {
87
+ err: richErr,
88
+ sourceEventName: payload.sourceEventName,
89
+ guardianUserId,
90
+ chunksSent: richErr.chunksSent,
91
+ },
92
+ "Rich Discord delivery failed mid-card, completing in plain text",
93
+ );
94
+ const completion = await sendDiscordReply(
95
+ { channelId },
96
+ appendPlainTextFallback(richErr.remainingText, approval),
97
+ );
98
+ return { success: true, messageId: completion.lastMessageId };
99
+ }
100
+ log.warn(
101
+ {
102
+ err: richErr,
103
+ sourceEventName: payload.sourceEventName,
104
+ guardianUserId,
105
+ },
106
+ "Rich Discord delivery failed, falling back to plain text",
107
+ );
108
+ }
109
+ }
110
+
111
+ // When falling back from rich delivery, append the plain-text
112
+ // instructions so the guardian still knows how to approve/reject.
113
+ const sent = await sendDiscordReply(
114
+ { channelId },
115
+ appendPlainTextFallback(messageText, approval),
116
+ );
64
117
 
65
118
  log.info(
66
119
  { sourceEventName: payload.sourceEventName, guardianUserId },
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * Generic notification conversation pairing.
3
3
  *
4
- * Materializes a conversation + message for each notification delivery
5
- * before the adapter sends it. This ensures every delivery has an
6
- * auditable conversation trail and enables the macOS/iOS client to
7
- * deep-link directly into the notification conversation.
4
+ * Materializes a conversation + message for a notification delivery
5
+ * before the adapter sends it, so the delivery has an auditable
6
+ * conversation trail and the macOS/iOS client can deep-link into it.
7
+ * Guardian-request deliveries to channels are the exception: they are
8
+ * delivery projections of a canonical request and pair nothing (see the
9
+ * guard below); only their vellum delivery carries a conversation.
8
10
  *
9
11
  * Resolution order:
10
12
  * 1. Explicit `reuse_existing` conversation action — highest precedence.
@@ -37,6 +39,7 @@ import {
37
39
  composeConversationSeed,
38
40
  isConversationSeedSane,
39
41
  } from "./conversation-seed-composer.js";
42
+ import { isGuardianRequestSignalEvent } from "./guardian-feed-projection.js";
40
43
  import type { NotificationSignal } from "./signal.js";
41
44
  import type {
42
45
  ConversationAction,
@@ -122,6 +125,30 @@ export async function pairDeliveryWithConversation(
122
125
  };
123
126
  }
124
127
 
128
+ // A channel-delivered guardian-request approval card (tool
129
+ // approvals, questions, and access requests alike) is a delivery
130
+ // projection of a canonical guardian request, not conversation
131
+ // content: its in-app homes are the home-feed "Needs attention"
132
+ // item and the source conversation's card (the vellum delivery,
133
+ // which still pairs below). Pairing a channel card here would
134
+ // either write it into the guardian's bound chat transcript or
135
+ // mint a fresh conversation for a transient work item, so it gets
136
+ // neither a row nor a conversation. The gateway delivery row (chat
137
+ // id + channel-native message id) remains its only persisted
138
+ // envelope.
139
+ if (
140
+ isGuardianRequestSignalEvent(signal.sourceEventName) &&
141
+ channel !== "vellum"
142
+ ) {
143
+ return {
144
+ conversationId: null,
145
+ messageId: null,
146
+ strategy,
147
+ createdNewConversation: false,
148
+ conversationFallbackUsed: false,
149
+ };
150
+ }
151
+
125
152
  const conversationAction = options?.conversationAction;
126
153
  const bindingContext = options?.bindingContext;
127
154
 
@@ -547,11 +547,14 @@ export async function emitNotificationSignal<TEventName extends string>(
547
547
  const vellumDelivery = dispatchResult.deliveryResults.find(
548
548
  (r) => r.channel === "vellum",
549
549
  );
550
- await writeHomeFeedItemForSignal(signal, decision, vellumDelivery).catch(
551
- (err) => {
552
- log.warn({ err, signalId }, "writeHomeFeedItemForSignal threw");
553
- },
554
- );
550
+ await writeHomeFeedItemForSignal(
551
+ signal,
552
+ decision,
553
+ vellumDelivery,
554
+ dispatchResult.deliveryResults,
555
+ ).catch((err) => {
556
+ log.warn({ err, signalId }, "writeHomeFeedItemForSignal threw");
557
+ });
555
558
 
556
559
  log.info(
557
560
  {
@@ -20,8 +20,11 @@
20
20
  * read it back.
21
21
  */
22
22
 
23
+ import { DELIVERY_STATUS } from "@vellumai/gateway-client";
24
+
23
25
  import {
24
26
  createGuardianRequestDelivery,
27
+ getGuardianRequestOrNull,
25
28
  type GuardianRequestDeliveryWire,
26
29
  updateGuardianRequestDelivery,
27
30
  } from "../channels/gateway-guardian-requests.js";
@@ -47,7 +50,7 @@ export interface ApprovalCardDeliveryAddress {
47
50
  chatId?: string;
48
51
  /** Channel-native message id (e.g. Slack `ts`) — the reaction/withdrawal key. */
49
52
  messageId?: string;
50
- /** Initial delivery status (defaults to "pending"). */
53
+ /** Initial delivery status (defaults to `DELIVERY_STATUS.pending`). */
51
54
  status?: string;
52
55
  }
53
56
 
@@ -91,14 +94,17 @@ export async function recordApprovalCardDelivery(
91
94
  * that row's id as `vellumDeliveryId` and it is reused (only its status applied)
92
95
  * — otherwise the vellum row is created here from the result.
93
96
  *
94
- * Every addressable result records the internal `conversationId` the card is
95
- * shown in, so a conversation's pending cards can be found uniformly
96
- * regardless of channel. Platform push results are skipped entirely: a push
97
+ * The vellum result records the internal `conversationId` its card is shown
98
+ * in. Channel guardian cards pair no conversation (they are delivery
99
+ * projections; see `conversation-pairing.ts`), so their rows carry no
100
+ * `conversationId`. Platform push results are skipped entirely: a push
97
101
  * has no channel-native card surface to address back to.
98
102
  * Channel results additionally carry the chat (`destination`) and channel-native
99
103
  * id (`messageId`) used to match inbound replies/reactions; a blank `destination`
100
104
  * is recorded as unknown rather than persisting the literal channel name as a
101
- * chat id. Status is diagnostic — the read paths key off addressing, not status.
105
+ * chat id. Status has two readers: the voice guardian-action sweep acts only
106
+ * on `sent`/`pending` rows, and card withdrawal skips rows already marked
107
+ * `withdrawn` (its per-surface receipt). Addressing lookups ignore status.
102
108
  *
103
109
  * Best-effort like the create: a status-patch failure is logged, not thrown.
104
110
  *
@@ -146,7 +152,10 @@ export async function recordGuardianRequestDeliveries(params: {
146
152
  if (deliveryId) {
147
153
  try {
148
154
  await updateGuardianRequestDelivery(deliveryId, {
149
- status: result.status === "sent" ? "sent" : "failed",
155
+ status:
156
+ result.status === "sent"
157
+ ? DELIVERY_STATUS.sent
158
+ : DELIVERY_STATUS.failed,
150
159
  });
151
160
  } catch (err) {
152
161
  log.error(
@@ -157,5 +166,37 @@ export async function recordGuardianRequestDeliveries(params: {
157
166
  }
158
167
  }
159
168
 
169
+ await withdrawIfRequestAlreadyTerminal(requestId);
170
+
160
171
  return vellumDeliveryId;
161
172
  }
173
+
174
+ /**
175
+ * Close the delivery/decision race: recording happens after the notification
176
+ * pipeline finishes, so a guardian who acts on a card the moment it lands can
177
+ * resolve the request before its delivery rows exist. That decision's
178
+ * withdrawal pass then finds nothing to withdraw, and the rows recorded here
179
+ * would describe live cards for an already-terminal request. Re-running
180
+ * withdrawal after recording converges them; rows a prior pass already
181
+ * withdrew are skipped, so the overlap never re-edits a surface.
182
+ *
183
+ * Best-effort like the rest of the recorder; the decided action isn't
184
+ * recoverable here, so denied outcomes render the plain status word.
185
+ */
186
+ async function withdrawIfRequestAlreadyTerminal(
187
+ requestId: string,
188
+ ): Promise<void> {
189
+ const request = await getGuardianRequestOrNull(requestId);
190
+ if (!request || request.status === "pending") {
191
+ return;
192
+ }
193
+ // Imported at call time: the withdrawal module reaches the daemon's
194
+ // conversation-surface graph, and a static import here would put this
195
+ // recorder (imported by the guardian-request producers) into that cycle.
196
+ const { withdrawGuardianRequestCards } =
197
+ await import("../approvals/guardian-card-withdrawal.js");
198
+ await withdrawGuardianRequestCards({
199
+ request,
200
+ status: request.status,
201
+ });
202
+ }