@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,242 @@
1
+ /**
2
+ * Unit tests for the guardian-form registry.
3
+ *
4
+ * The registry is the rail every form-backed command parks on, so the
5
+ * concurrency rules are tested here directly rather than only through the
6
+ * contact forms that happen to use them today. What matters: a form is
7
+ * answered once, a claim stops the answer deadline without ending the form,
8
+ * and a form that nobody answers ends by telling the clients showing it.
9
+ */
10
+
11
+ import { describe, expect, mock, test } from "bun:test";
12
+
13
+ import {
14
+ claimGuardianForm,
15
+ type GuardianFormClosedReason,
16
+ hasUnclaimedGuardianForm,
17
+ openGuardianForm,
18
+ resolveGuardianForm,
19
+ } from "../guardian-form-registry.js";
20
+
21
+ interface TestResult {
22
+ ok: boolean;
23
+ error?: string;
24
+ wrote?: string;
25
+ }
26
+
27
+ /** Open a form and hand back its requestId plus the parked promise. */
28
+ function openForm(options: { kind?: string; timeoutMs?: number } = {}) {
29
+ const closed = mock((_id: string, _reason: GuardianFormClosedReason) => {});
30
+ let requestId = "";
31
+ const settled = openGuardianForm<TestResult>({
32
+ kind: options.kind ?? "test.form",
33
+ timeoutMs: options.timeoutMs ?? 30_000,
34
+ meta: { verify: true },
35
+ broadcast: {
36
+ open: (id) => {
37
+ requestId = id;
38
+ },
39
+ closed,
40
+ },
41
+ });
42
+ return { requestId, settled, closed };
43
+ }
44
+
45
+ const tick = (ms: number) => new Promise((r) => setTimeout(r, ms));
46
+
47
+ describe("openGuardianForm", () => {
48
+ test("broadcasts the form and parks until something resolves it", async () => {
49
+ const { requestId, settled } = openForm();
50
+ expect(requestId).toMatch(/^[0-9a-f-]{36}$/);
51
+
52
+ let done = false;
53
+ void settled.then(() => {
54
+ done = true;
55
+ });
56
+ await tick(10);
57
+ expect(done).toBe(false);
58
+
59
+ resolveGuardianForm(requestId, { ok: true, wrote: "contact-1" });
60
+ expect(await settled).toEqual({ ok: true, wrote: "contact-1" });
61
+ });
62
+
63
+ test("hands the writer's own fields back to the parked call", async () => {
64
+ const { requestId, settled } = openForm();
65
+ resolveGuardianForm(requestId, { ok: false, error: "Cancelled by user" });
66
+ expect(await settled).toEqual({ ok: false, error: "Cancelled by user" });
67
+ });
68
+
69
+ test("an unanswered form ends and tells the clients showing it", async () => {
70
+ const { requestId, settled, closed } = openForm({ timeoutMs: 30 });
71
+
72
+ expect(await settled).toEqual({ ok: false, error: "Prompt timed out" });
73
+ expect(closed).toHaveBeenCalledWith(requestId, "timed_out");
74
+ });
75
+ });
76
+
77
+ describe("claimGuardianForm", () => {
78
+ test("only the first claim wins", async () => {
79
+ const { requestId, settled } = openForm();
80
+
81
+ const first = claimGuardianForm(requestId);
82
+ const second = claimGuardianForm(requestId);
83
+
84
+ expect(first.claimed).toBe(true);
85
+ expect(first.settleMs).toBeGreaterThan(0);
86
+ expect(second).toEqual({ claimed: false, reason: "already_claimed" });
87
+
88
+ resolveGuardianForm(requestId, { ok: true });
89
+ await settled;
90
+ });
91
+
92
+ test("a form nobody is holding claims as unknown", () => {
93
+ expect(claimGuardianForm("no-such-form")).toEqual({
94
+ claimed: false,
95
+ reason: "unknown",
96
+ });
97
+ });
98
+
99
+ test("claiming disarms the answer deadline", async () => {
100
+ // The form is open for 30ms, and the claim lands inside that window. If the
101
+ // claim did not swap the deadline for the settle window, the parked call
102
+ // would report a timeout while the write it started was still running.
103
+ const { requestId, settled, closed } = openForm({ timeoutMs: 30 });
104
+ expect(claimGuardianForm(requestId).claimed).toBe(true);
105
+
106
+ let done = false;
107
+ void settled.then(() => {
108
+ done = true;
109
+ });
110
+ await tick(80);
111
+
112
+ expect(done).toBe(false);
113
+ expect(closed).not.toHaveBeenCalled();
114
+
115
+ resolveGuardianForm(requestId, { ok: true, wrote: "late" });
116
+ expect(await settled).toEqual({ ok: true, wrote: "late" });
117
+ });
118
+
119
+ test("a resolved form cannot be claimed again", async () => {
120
+ const { requestId, settled } = openForm();
121
+ resolveGuardianForm(requestId, { ok: true });
122
+ await settled;
123
+
124
+ expect(claimGuardianForm(requestId)).toEqual({
125
+ claimed: false,
126
+ reason: "unknown",
127
+ });
128
+ });
129
+ });
130
+
131
+ describe("resolveGuardianForm", () => {
132
+ test("retires the card as answered or cancelled", async () => {
133
+ const answered = openForm();
134
+ resolveGuardianForm(answered.requestId, { ok: true });
135
+ await answered.settled;
136
+ expect(answered.closed).toHaveBeenCalledWith(
137
+ answered.requestId,
138
+ "answered",
139
+ );
140
+
141
+ const cancelled = openForm();
142
+ resolveGuardianForm(cancelled.requestId, { ok: false, error: "no" });
143
+ await cancelled.settled;
144
+ expect(cancelled.closed).toHaveBeenCalledWith(
145
+ cancelled.requestId,
146
+ "cancelled",
147
+ );
148
+ });
149
+
150
+ test("reports that nothing was waiting for an unknown form", () => {
151
+ expect(resolveGuardianForm("no-such-form", { ok: true })).toEqual({
152
+ resolved: false,
153
+ });
154
+ });
155
+ });
156
+
157
+ describe("hasUnclaimedGuardianForm", () => {
158
+ test("sees only the kinds it is asked about", async () => {
159
+ const { requestId, settled } = openForm({ kind: "kind.a" });
160
+
161
+ expect(hasUnclaimedGuardianForm(["kind.a"])).toBe(true);
162
+ // The point of taking kinds: an open form of one kind must not block a
163
+ // form of another, which a registry-wide sweep would do.
164
+ expect(hasUnclaimedGuardianForm(["kind.b"])).toBe(false);
165
+ expect(hasUnclaimedGuardianForm(["kind.b", "kind.a"])).toBe(true);
166
+
167
+ resolveGuardianForm(requestId, { ok: true });
168
+ await settled;
169
+ expect(hasUnclaimedGuardianForm(["kind.a"])).toBe(false);
170
+ });
171
+
172
+ test("a claimed form no longer counts as open", async () => {
173
+ const { requestId, settled } = openForm({ kind: "kind.c" });
174
+ expect(hasUnclaimedGuardianForm(["kind.c"])).toBe(true);
175
+
176
+ claimGuardianForm(requestId);
177
+ // Claimed means answered: its card is gone, so the next command should not
178
+ // be refused on account of it.
179
+ expect(hasUnclaimedGuardianForm(["kind.c"])).toBe(false);
180
+
181
+ resolveGuardianForm(requestId, { ok: true });
182
+ await settled;
183
+ });
184
+ });
185
+
186
+ describe("resolveFormFromCallback", () => {
187
+ test("passes a writer's own fields through untouched", async () => {
188
+ // The rail is only reusable if a new form's result reaches its caller. A
189
+ // callback that enumerated contact fields would drop everything else.
190
+ const { requestId, settled } = openForm();
191
+
192
+ const { resolveFormFromCallback } =
193
+ await import("../routes/guardian-form-routes.js");
194
+ resolveFormFromCallback({
195
+ body: { requestId, id: "row-7", rows: 3, skipped: false },
196
+ } as never);
197
+
198
+ expect(await settled).toEqual({
199
+ ok: true,
200
+ id: "row-7",
201
+ rows: 3,
202
+ skipped: false,
203
+ } as never);
204
+ });
205
+
206
+ test("an error makes the outcome a failure", async () => {
207
+ const { requestId, settled } = openForm();
208
+ const { resolveFormFromCallback } =
209
+ await import("../routes/guardian-form-routes.js");
210
+ resolveFormFromCallback({
211
+ body: { requestId, error: "nope", id: "ignored" },
212
+ } as never);
213
+
214
+ expect(await settled).toEqual({ ok: false, error: "nope" });
215
+ });
216
+ });
217
+
218
+ describe("claiming names the form", () => {
219
+ test("a claim for a different kind is refused", async () => {
220
+ // An id alone does not say which form it belongs to. Without this check a
221
+ // submission to form B could claim an open form A, run B's write, and hand
222
+ // A's caller a result for a form it never showed.
223
+ const { requestId, settled } = openForm({ kind: "kind.real" });
224
+
225
+ expect(claimGuardianForm(requestId, "kind.other")).toEqual({
226
+ claimed: false,
227
+ reason: "wrong_kind",
228
+ });
229
+ // Refused, not consumed: the right caller can still claim it.
230
+ expect(claimGuardianForm(requestId, "kind.real").claimed).toBe(true);
231
+
232
+ resolveGuardianForm(requestId, { ok: true });
233
+ await settled;
234
+ });
235
+
236
+ test("a caller that names no kind still claims, as before", async () => {
237
+ const { requestId, settled } = openForm({ kind: "kind.real" });
238
+ expect(claimGuardianForm(requestId).claimed).toBe(true);
239
+ resolveGuardianForm(requestId, { ok: true });
240
+ await settled;
241
+ });
242
+ });
@@ -11,6 +11,8 @@
11
11
  * principal so access requests cannot bind to stale/cross-assistant contacts.
12
12
  */
13
13
 
14
+ import { isGuardianRequestExpired } from "@vellumai/gateway-client";
15
+
14
16
  import {
15
17
  createGuardianRequest,
16
18
  listGuardianRequestsOrEmpty,
@@ -228,13 +230,19 @@ export async function notifyGuardianOfAccessRequest(
228
230
  // notified: true with the existing request ID so callers know the guardian
229
231
  // was already notified. A degraded (empty) read falls through to creation,
230
232
  // whose fail-closed throw prevents a prompt without a persisted request.
231
- const existingPending = await listGuardianRequestsOrEmpty({
232
- status: "pending",
233
- requesterExternalUserId: actorExternalId,
234
- sourceChannel,
235
- kind: "access_request",
236
- sourceConversationId: conversationId,
237
- });
233
+ // Past-deadline rows never dedupe: a pending row can sit past its
234
+ // expiresAt until the sweep expires it, it is already undecidable, and
235
+ // letting it absorb a fresh attempt would silently drop the new guardian
236
+ // notification.
237
+ const existingPending = (
238
+ await listGuardianRequestsOrEmpty({
239
+ status: "pending",
240
+ requesterExternalUserId: actorExternalId,
241
+ sourceChannel,
242
+ kind: "access_request",
243
+ sourceConversationId: conversationId,
244
+ })
245
+ ).filter((r) => !isGuardianRequestExpired(r));
238
246
  if (existingPending.length > 0) {
239
247
  log.debug(
240
248
  { sourceChannel, actorExternalId, existingId: existingPending[0].id },
@@ -145,9 +145,9 @@ function sanitizeEventAttr(value: string): string {
145
145
  /**
146
146
  * Untrusted third-party output to fence inside a persisted wake trigger via
147
147
  * {@link wrapUntrustedContent}. `maxChars` overrides the per-source character
148
- * budget — used for preformatted shell output that `formatShellOutput` already
149
- * bounded (to `MAX_OUTPUT_LENGTH`) and appended an `<output_truncated file=…/>`
150
- * recovery marker to, so the wrapper does not re-truncate that marker off.
148
+ * budget. Used for preformatted shell output that is already bounded to
149
+ * `MAX_OUTPUT_LENGTH` with an `<output_truncated />` marker, so the wrapper
150
+ * does not slice that marker off.
151
151
  */
152
152
  interface WakeUntrustedOutput {
153
153
  content: string;
@@ -1018,7 +1018,19 @@ export async function wakeAgentForOpportunity(
1018
1018
  },
1019
1019
  ];
1020
1020
  const wakeHintMessageCount = wakeMessages.length;
1021
- const runInput: Message[] = [...baseline, ...wakeMessages];
1021
+ // Camera-frame retention, the wake's equivalent of the orchestrator's
1022
+ // pre-run pass. The wake sends this array itself and keeps the in-loop
1023
+ // budget gate disabled, so neither the loop's post-compaction transform nor
1024
+ // `runAgentLoopImpl`'s pre-run pass ever sees it; a wake on a long camera
1025
+ // session would otherwise carry every frame the conversation holds,
1026
+ // including any the gate above just retained. The pass replaces blocks
1027
+ // inside messages without adding, dropping, or reordering one, so
1028
+ // `baselineLength` and `wakeHintMessageCount` still index the same
1029
+ // positions.
1030
+ const runInput: Message[] = conversation.trimAgedSightFrames([
1031
+ ...baseline,
1032
+ ...wakeMessages,
1033
+ ]);
1022
1034
 
1023
1035
  // Event handling runs in two modes. While `mode === "buffering"`,
1024
1036
  // events accumulate in `buffered` so that a wake which ultimately
@@ -160,12 +160,15 @@ export interface ChannelApprovalPrompt {
160
160
  // Decision result (daemon-internal)
161
161
  // ---------------------------------------------------------------------------
162
162
 
163
- /** How the user communicated their decision. */
163
+ /**
164
+ * How the user communicated their decision: the modality, never the channel.
165
+ * A channel-prefixed value here would misattribute the next channel that
166
+ * joins (every channel's buttons ride the same `apr:` callback), and no
167
+ * consumer reads a channel off this field.
168
+ */
164
169
  export type ApprovalDecisionSource =
165
- | "telegram_button"
166
- | "whatsapp_button"
167
- | "slack_button"
168
- | "slack_reaction"
170
+ | "button"
171
+ | "reaction"
169
172
  | "vellum_surface"
170
173
  | "plain_text";
171
174
 
@@ -13,6 +13,7 @@
13
13
  */
14
14
 
15
15
  import type { ChannelId } from "../channels/types.js";
16
+ import { discordInviteAdapter } from "./channel-invite-transports/discord.js";
16
17
  import { emailInviteAdapter } from "./channel-invite-transports/email.js";
17
18
  import { slackInviteAdapter } from "./channel-invite-transports/slack.js";
18
19
  import { telegramInviteAdapter } from "./channel-invite-transports/telegram.js";
@@ -96,6 +97,7 @@ export async function resolveAdapterHandle(
96
97
  /** Create a registry instance with built-in adapters registered. */
97
98
  export function createInviteAdapterRegistry(): InviteAdapterRegistry {
98
99
  const registry = new InviteAdapterRegistry();
100
+ registry.register(discordInviteAdapter);
99
101
  registry.register(emailInviteAdapter);
100
102
  registry.register(slackInviteAdapter);
101
103
  registry.register(telegramInviteAdapter);
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Discord channel invite adapter.
3
+ *
4
+ * Resolves the assistant's Discord bot username for use in invite
5
+ * instructions. Discord invites use the universal 6-digit code path for
6
+ * redemption, so this adapter only implements `resolveChannelHandle`,
7
+ * no `buildShareLink` or `extractInboundToken` needed. The platform
8
+ * constraint that shapes the instruction copy lives with the copy
9
+ * (`invite-instruction-generator.ts`): a person can only DM the bot once
10
+ * they share a server with it.
11
+ */
12
+
13
+ import type { ChannelId } from "../../channels/types.js";
14
+ import { getConfig } from "../../config/loader.js";
15
+ import type { ChannelInviteAdapter } from "../channel-invite-types.js";
16
+
17
+ export const discordInviteAdapter: ChannelInviteAdapter = {
18
+ channel: "discord" as ChannelId,
19
+
20
+ resolveChannelHandle(): string | undefined {
21
+ const { botUsername } = getConfig().discord;
22
+ if (!botUsername) {
23
+ return undefined;
24
+ }
25
+ return `@${botUsername}`;
26
+ },
27
+ };
@@ -26,10 +26,10 @@ export const REMOTE_TTL_MS = 5 * 60 * 1000;
26
26
  * Bot scopes the Slack manifest requests that the app cannot work without.
27
27
  *
28
28
  * Slack's install flow can return a token carrying a fraction of the manifest's
29
- * scopes while `auth.test` still succeeds — a live install produced 2 of 18, and
30
- * the first real API call failed with `missing_scope`. `auth.test` passing is
31
- * therefore not evidence the install is usable; the granted set has to be read
32
- * off the `x-oauth-scopes` response header and compared.
29
+ * scopes while `auth.test` still succeeds, leaving the first real API call to
30
+ * fail with `missing_scope`. `auth.test` passing is therefore not evidence the
31
+ * install is usable; the granted set has to be read off the `x-oauth-scopes`
32
+ * response header and compared.
33
33
  *
34
34
  * Mirrors the non-optional half of `oauth_config.scopes.bot` in
35
35
  * `skills/slack-app-setup/scripts/build-manifest.ts` (and its copy in
@@ -3,6 +3,7 @@ import type { MessageAudience } from "@vellumai/gateway-client";
3
3
  import { stripVellumLinks } from "../daemon/assistant-attachments.js";
4
4
  import type { RenderedHistoryContent } from "../daemon/handlers/shared.js";
5
5
  import { renderHistoryContent } from "../daemon/handlers/shared.js";
6
+ import { readProviderMessageMetadata } from "../messaging/provider-message-metadata.js";
6
7
  import { editChannelMessage } from "../messaging/providers/index.js";
7
8
  import { readSlackMetadata } from "../messaging/providers/slack/message-metadata.js";
8
9
  import { getAttachmentMetadataForMessage } from "../persistence/attachments-store.js";
@@ -383,16 +384,16 @@ async function deliverPersistedAssistantMessageViaCallback(
383
384
  return false;
384
385
  }
385
386
 
386
- // Compose an `onMessageTs` that reconciles `slackMeta.channelTs` on the
387
- // persisted assistant row once Slack returns the authoritative ts. The
388
- // assistant row was written BEFORE the gateway POST in
389
- // `handleMessageComplete`, so the partial `slackMeta` it carries is
390
- // missing `channelTs` and would otherwise be rejected by
391
- // `readSlackMetadata`, dropping the row out of chronological/thread-tag
392
- // rendering. We only act on the FIRST ts (top-level segment); any
393
- // subsequent split segments become independent Slack messages with
394
- // their own ts and are not represented as separate DB rows.
395
- const reconcileOnMessageTs = makeChannelTsReconciler(msg.id);
387
+ // Compose an `onMessageTs` that reconciles the persisted assistant row's
388
+ // provider message ids as the transport reports the authoritative ones.
389
+ // The assistant row is written BEFORE the gateway POST, so its pre-send
390
+ // envelope names no id of its own: a Slack row's partial `slackMeta` lacks
391
+ // `channelTs` and reads as null through `readSlackMetadata`, and a
392
+ // neutral-envelope row lacks the `messageId` a later reaction
393
+ // naming it resolves by. A reply split into several segments reports one
394
+ // id per posted provider message, all reconciled onto this one row; see
395
+ // `makeSentMessageIdReconciler` for the per-envelope rules.
396
+ const reconcileOnMessageTs = makeSentMessageIdReconciler(msg.id);
396
397
  const callerOnMessageTs = options?.onMessageTs;
397
398
  const composedOnMessageTs = (ts: string): void => {
398
399
  reconcileOnMessageTs(ts);
@@ -491,39 +492,40 @@ export async function deliverReplyViaCallback(
491
492
  }
492
493
 
493
494
  /**
494
- * Build a one-shot `onMessageTs` handler that reconciles the persisted
495
- * assistant message's `slackMeta.channelTs` from Slack's authoritative `ts`.
495
+ * Build an `onMessageTs` handler that reconciles the persisted assistant
496
+ * row's provider message ids from the transport's authoritative ones:
497
+ * `slackMeta.channelTs` for a Slack row, `providerMeta.messageId` plus
498
+ * `additionalMessageIds` for a row carrying the neutral envelope (Discord
499
+ * today, any transport whose delivery result reports the sent id). The
500
+ * back-filled ids are what let a later reaction naming the
501
+ * assistant's own post resolve back to this row.
496
502
  *
497
503
  * Behavior:
498
- * - Acts only on the first invocation per delivery (subsequent segments
499
- * correspond to independent Slack messages with their own ts and are not
500
- * represented as separate DB rows).
501
- * - No-op when the row was not persisted with a `slackMeta` envelope (the
502
- * channel was not Slack at write-time, e.g. vellum/telegram outbound).
503
- * - No-op when the row's existing `slackMeta` already parses cleanly via
504
- * `readSlackMetadata` (channelTs already present, e.g. from a prior
505
- * reconciliation).
504
+ * - A Slack row acts on the first invocation only: `channelTs` is the one
505
+ * id its envelope names, and later split segments are independent Slack
506
+ * messages it cannot carry.
507
+ * - A neutral-envelope row records every reported id: the first as
508
+ * `messageId`, the rest under `additionalMessageIds`, all naming this
509
+ * same row. An id already recorded (a redelivery) is skipped.
510
+ * - No-op when the row was persisted with neither envelope (e.g. vellum
511
+ * outbound).
506
512
  * - Failures are logged and swallowed so a transient DB error cannot break
507
513
  * the outbound delivery itself.
508
514
  */
509
- function makeChannelTsReconciler(messageId: string): (ts: string) => void {
510
- let applied = false;
515
+ function makeSentMessageIdReconciler(messageId: string): (ts: string) => void {
516
+ let slackApplied = false;
511
517
  return (ts: string): void => {
512
- if (applied) {
513
- return;
514
- }
515
- applied = true;
516
518
  if (!ts) {
517
519
  return;
518
520
  }
519
521
  try {
520
522
  // Re-read the row's current metadata so a concurrent edit-propagation
521
523
  // write (e.g. `editedAt`) is not clobbered. `updateMessageMetadata`
522
- // shallow-merges into the top-level envelope, and the slackMeta
524
+ // shallow-merges into the top-level envelope, and the per-channel
523
525
  // sub-object is merged manually below so we can preserve fields on
524
526
  // the partial pre-send envelope (`mergeSlackMetadata` would call
525
527
  // `readSlackMetadata` which rejects the partial form for lacking
526
- // channelTs — exactly the state we are reconciling).
528
+ // channelTs, exactly the state we are reconciling).
527
529
  const row = getMessageById(messageId);
528
530
  if (row === null || row.metadata === null) {
529
531
  return;
@@ -537,8 +539,13 @@ function makeChannelTsReconciler(messageId: string): (ts: string) => void {
537
539
  const slackMetaRaw =
538
540
  typeof envelope.slackMeta === "string" ? envelope.slackMeta : null;
539
541
  if (slackMetaRaw === null) {
542
+ reconcileProviderMessageId(messageId, envelope, ts);
540
543
  return;
541
544
  }
545
+ if (slackApplied) {
546
+ return;
547
+ }
548
+ slackApplied = true;
542
549
  // If the existing slackMeta already parses cleanly via the strict
543
550
  // reader, channelTs is already present (a prior reconciliation ran,
544
551
  // or backfill stamped the field) — nothing to do.
@@ -579,3 +586,40 @@ function makeChannelTsReconciler(messageId: string): (ts: string) => void {
579
586
  }
580
587
  };
581
588
  }
589
+
590
+ /**
591
+ * The neutral-envelope arm of the reconciliation: record the transport's
592
+ * sent-message id on the row's `providerMeta`. The first reported id
593
+ * becomes `messageId`; further ids (a reply split at tool boundaries posts
594
+ * several provider messages) accumulate under `additionalMessageIds`. An id
595
+ * the envelope already names is a redelivery and is skipped, and a row
596
+ * carrying no valid neutral envelope is left untouched.
597
+ */
598
+ function reconcileProviderMessageId(
599
+ messageId: string,
600
+ envelope: Record<string, unknown>,
601
+ ts: string,
602
+ ): void {
603
+ const providerMeta = readProviderMessageMetadata(envelope.providerMeta);
604
+ if (providerMeta === null) {
605
+ return;
606
+ }
607
+ if (providerMeta.messageId === undefined) {
608
+ updateMessageMetadata(messageId, {
609
+ providerMeta: JSON.stringify({ ...providerMeta, messageId: ts }),
610
+ });
611
+ return;
612
+ }
613
+ if (
614
+ providerMeta.messageId === ts ||
615
+ providerMeta.additionalMessageIds?.includes(ts)
616
+ ) {
617
+ return;
618
+ }
619
+ updateMessageMetadata(messageId, {
620
+ providerMeta: JSON.stringify({
621
+ ...providerMeta,
622
+ additionalMessageIds: [...(providerMeta.additionalMessageIds ?? []), ts],
623
+ }),
624
+ });
625
+ }
@@ -101,6 +101,10 @@ export async function processGuardianDecision(
101
101
  // 2. Verify conversationId scoping before applying the decision. The
102
102
  // decision is allowed when the conversationId matches the request's
103
103
  // source conversation OR a recorded delivery destination conversation.
104
+ // Deliberately not narrowed by delivery channel: a delivery row's
105
+ // destinationConversationId is always an internal conversation id, and
106
+ // a channel card's paired conversation renders the same actionable
107
+ // in-app card, so its taps are as legitimate as the vellum card's.
104
108
  // Reads degrade fail-closed: an unreachable gateway scopes to not_found
105
109
  // (and the decide below fails loudly anyway).
106
110
  if (conversationId) {
@@ -108,11 +112,7 @@ export async function processGuardianDecision(
108
112
  if (
109
113
  request &&
110
114
  request.sourceConversationId &&
111
- !(await isGuardianRequestInScopeOrFalse(
112
- requestId,
113
- conversationId,
114
- channel,
115
- ))
115
+ !(await isGuardianRequestInScopeOrFalse(requestId, conversationId))
116
116
  ) {
117
117
  return { ok: true, applied: false, reason: "not_found" };
118
118
  }
@@ -128,6 +128,10 @@ export async function processGuardianDecision(
128
128
  channel,
129
129
  guardianPrincipalId: actorContext.guardianPrincipalId,
130
130
  },
131
+ // The acting conversation: card withdrawal suppresses only this
132
+ // conversation's completion broadcast (the acting client already shows
133
+ // its optimistic completion); sibling in-app projections still get theirs.
134
+ ...(conversationId ? { originConversationId: conversationId } : {}),
131
135
  userText: undefined,
132
136
  });
133
137