@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
@@ -1,18 +1,26 @@
1
1
  /**
2
2
  * Guardian request expiry sweep.
3
3
  *
4
- * Periodically asks the gateway to CAS-expire pending guardian requests
5
- * whose `expiresAt` timestamp has passed (`guardian_requests_sweep_expired`),
6
- * so stale requests are cleaned up even when no follow-up traffic arrives
7
- * from either the guardian or the requester.
4
+ * Each round reads a bounded batch of pending requests past their
5
+ * `expiresAt` and, per request, runs the expiry side effects (withdrawing
6
+ * the approval cards on every surface, notifying the requester, releasing
7
+ * any in-memory pending interaction), and only then confirms with the
8
+ * gateway's per-request expire CAS. The status flip is the receipt that the
9
+ * side effects ran, not the announcement that they should: a lost IPC
10
+ * response, a timeout, or a crash at any point leaves the row pending and
11
+ * past-deadline, so the next round lists it again and re-runs its fan-out.
8
12
  *
9
- * The gateway owns the status transition (the single source of truth); the
10
- * daemon fans out the side effects for each expired row it returns:
11
- * withdrawing the approval cards on every surface, notifying the requester
12
- * that their request expired, and releasing any in-memory pending
13
- * interaction. Requester notices are delivered straight to the requester's
14
- * channel — not the guardian-facing notification pipeline — and the guardian
15
- * stays passive, since the withdrawn card already reflects expiry.
13
+ * That order is safe because every decision path checks `expiresAt` before
14
+ * any write: a past-deadline row is undecidable while it waits for its
15
+ * fan-out. The cost is at-least-once side effects: a round that dies
16
+ * between the notice and the CAS re-notifies the requester next round,
17
+ * which is the correct trade against silently losing the withdrawal and the
18
+ * notice, since a stale card is an actionable control on every surface that
19
+ * renders buttons.
20
+ *
21
+ * Requester notices are delivered straight to the requester's channel, not
22
+ * the guardian-facing notification pipeline, and the guardian stays
23
+ * passive, since the withdrawn card already reflects expiry.
16
24
  *
17
25
  * Unreachable-gateway posture: log and skip the round — the next tick
18
26
  * retries, and expiry only ever moves forward.
@@ -21,9 +29,11 @@
21
29
  import { withdrawGuardianRequestCards } from "../../approvals/guardian-card-withdrawal.js";
22
30
  import { notifyExpiredGuardianRequest } from "../../approvals/guardian-expiry-notifier.js";
23
31
  import {
32
+ expireGuardianRequest,
24
33
  type GuardianRequestWire,
25
- sweepExpiredGuardianRequests,
34
+ listExpiredPendingGuardianRequests,
26
35
  } from "../../channels/gateway-guardian-requests.js";
36
+ import { reconcileGuardianFeedProjections } from "../../notifications/guardian-feed-projection.js";
27
37
  import { getLogger } from "../../util/logger.js";
28
38
 
29
39
  const log = getLogger("guardian-expiry-sweep");
@@ -31,6 +41,12 @@ const log = getLogger("guardian-expiry-sweep");
31
41
  /** Interval at which the expiry sweep runs (60 seconds). */
32
42
  const SWEEP_INTERVAL_MS = 60_000;
33
43
 
44
+ /**
45
+ * Requests handled per round. Bounds every round's IPC payload and fan-out
46
+ * however large a backlog grows; the remainder drains on later rounds.
47
+ */
48
+ const SWEEP_BATCH_LIMIT = 50;
49
+
34
50
  /** Timer handle for the sweep so it can be stopped in tests and shutdown. */
35
51
  let sweepTimer: ReturnType<typeof setInterval> | null = null;
36
52
 
@@ -38,15 +54,24 @@ let sweepTimer: ReturnType<typeof setInterval> | null = null;
38
54
  let sweepInProgress = false;
39
55
 
40
56
  /**
41
- * Run one expiry sweep round: the gateway CAS-expires every pending request
42
- * past its `expiresAt` (a concurrent decision that wins the race is never
43
- * overwritten), then the daemon runs the per-request side effects. Returns
44
- * the count of requests transitioned to expired.
57
+ * Run one expiry sweep round: read a bounded batch of past-deadline pending
58
+ * requests, run each one's side effects, then confirm each with the
59
+ * gateway's per-request expire CAS (a concurrent decision that wins the
60
+ * race is never overwritten; the CAS requires `pending`). Returns the
61
+ * count of requests whose full cycle completed.
45
62
  */
46
63
  export async function runGuardianExpirySweep(): Promise<number> {
47
- let expired: GuardianRequestWire[];
64
+ // Converge the home-feed projection against canonical state on the
65
+ // same cadence: backfill items for pending requests that lost (or
66
+ // predate) theirs, and receipt any item still actionable for a
67
+ // terminal request. Never blocks expiry.
68
+ await reconcileGuardianFeedProjections().catch((err) => {
69
+ log.warn({ err }, "Guardian feed reconciliation failed");
70
+ });
71
+
72
+ let stale: GuardianRequestWire[];
48
73
  try {
49
- expired = await sweepExpiredGuardianRequests();
74
+ stale = await listExpiredPendingGuardianRequests(SWEEP_BATCH_LIMIT);
50
75
  } catch (err) {
51
76
  log.warn(
52
77
  { err },
@@ -55,9 +80,8 @@ export async function runGuardianExpirySweep(): Promise<number> {
55
80
  return 0;
56
81
  }
57
82
 
58
- // The sweep returns the full rows, so the side-effect fan-out can never be
59
- // stranded by a failed follow-up read after the status flip.
60
- for (const request of expired) {
83
+ let expiredCount = 0;
84
+ for (const request of stale) {
61
85
  log.info(
62
86
  {
63
87
  event: "guardian_request_expired",
@@ -65,34 +89,68 @@ export async function runGuardianExpirySweep(): Promise<number> {
65
89
  kind: request.kind,
66
90
  expiresAt: request.expiresAt,
67
91
  },
68
- "Expired guardian request via sweep",
92
+ "Expiring guardian request via sweep",
69
93
  );
70
94
 
71
95
  // Withdraw the now-stale approval cards on every surface. No origin
72
96
  // channel — the expiry is system-driven, so all surfaces (including
73
- // in-app) are withdrawn. Best-effort and non-throwing.
74
- await withdrawGuardianRequestCards({
97
+ // in-app) are withdrawn. Incomplete withdrawal defers the whole
98
+ // request: the notice has not been sent yet, so the retry next round
99
+ // repeats only idempotent card edits, never a delivered notice. A
100
+ // surface that fails persistently keeps its request in the pending set
101
+ // with this warning every round, so the stall is observable rather
102
+ // than silent.
103
+ const withdrawal = await withdrawGuardianRequestCards({
75
104
  request,
76
105
  status: "expired",
77
106
  });
107
+ if (!withdrawal.complete) {
108
+ log.warn(
109
+ { requestId: request.id },
110
+ "Card withdrawal incomplete; leaving the request pending for the next round",
111
+ );
112
+ continue;
113
+ }
78
114
 
79
115
  // Notify the requester their request expired and release any in-memory
80
- // pending interaction. Best-effort and non-throwing, like the card
81
- // withdrawal above.
82
- await notifyExpiredGuardianRequest(request);
116
+ // pending interaction. A failed notice also defers the request: an
117
+ // unsent notice is exactly what a retry can still deliver.
118
+ const notice = await notifyExpiredGuardianRequest(request);
119
+ if (!notice.complete) {
120
+ log.warn(
121
+ { requestId: request.id },
122
+ "Expiry notice incomplete; leaving the request pending for the next round",
123
+ );
124
+ continue;
125
+ }
126
+
127
+ // The receipt: only after the side effects does the row leave the
128
+ // pending set. A failure here leaves it discoverable for the next
129
+ // round rather than silently done; the notice-then-lost-confirmation
130
+ // window is the one duplicate-notice case, and it is bounded to that
131
+ // crash or timeout.
132
+ try {
133
+ await expireGuardianRequest(request.id);
134
+ expiredCount += 1;
135
+ } catch (err) {
136
+ log.warn(
137
+ { err, requestId: request.id },
138
+ "Expire confirmation failed; the next round re-runs this request",
139
+ );
140
+ }
83
141
  }
84
142
 
85
- if (expired.length > 0) {
143
+ if (expiredCount > 0) {
86
144
  log.info(
87
145
  {
88
146
  event: "guardian_expiry_sweep_complete",
89
- expiredCount: expired.length,
147
+ expiredCount,
90
148
  },
91
- `Guardian expiry sweep: expired ${expired.length} request(s)`,
149
+ `Guardian expiry sweep: expired ${expiredCount} request(s)`,
92
150
  );
93
151
  }
94
152
 
95
- return expired.length;
153
+ return expiredCount;
96
154
  }
97
155
 
98
156
  /**
@@ -0,0 +1,115 @@
1
+ /**
2
+ * The form-agnostic half of the guardian-form IPC surface.
3
+ *
4
+ * A form's own route carries its schema and its wire events; these two are the
5
+ * same for every form, so a new one gets them without adding routes of its own:
6
+ *
7
+ * - `guardian_form_claim` the writer takes the form before writing
8
+ * - `resolve_guardian_form` the writer reports back, unblocking the command
9
+ *
10
+ * The contact forms predate these and keep their own `contact_prompt_claim` /
11
+ * `resolve_contact_prompt` names, because the gateway that calls them ships
12
+ * separately from the daemon and an older one still uses the old names. New
13
+ * forms should use these.
14
+ */
15
+
16
+ import { z } from "zod";
17
+
18
+ import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
19
+ import {
20
+ claimGuardianForm,
21
+ type GuardianFormResult,
22
+ resolveGuardianForm,
23
+ } from "../guardian-form-registry.js";
24
+ import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
25
+
26
+ const RequestIdParams = z.object({
27
+ requestId: z.string().describe("The pending form's id."),
28
+ });
29
+
30
+ const ClaimParams = RequestIdParams.extend({
31
+ kind: z
32
+ .string()
33
+ .optional()
34
+ .describe(
35
+ "The form the caller believes it is writing for. Rejected when it is not what the id is holding. Omitted by callers that predate the check.",
36
+ ),
37
+ });
38
+
39
+ /**
40
+ * Hand a writer's report to the call parked on the form.
41
+ *
42
+ * Everything but `requestId` and `error` is passed through untouched, so a
43
+ * form's result is whatever its writer chose to send. An `error` is what makes
44
+ * it a failure; there is no other flag.
45
+ */
46
+ export function resolveFormFromCallback({ body = {} }: RouteHandlerArgs): {
47
+ resolved: boolean;
48
+ } {
49
+ const {
50
+ requestId,
51
+ error,
52
+ ...rest
53
+ }: { requestId: string; error?: string } & Record<string, unknown> =
54
+ body as never;
55
+
56
+ const result: GuardianFormResult = error
57
+ ? { ok: false, error }
58
+ : { ok: true, ...rest };
59
+ return resolveGuardianForm(requestId, result);
60
+ }
61
+
62
+ export function claimForm({ body = {} }: RouteHandlerArgs): {
63
+ claimed: boolean;
64
+ reason?: "already_claimed" | "unknown" | "wrong_kind";
65
+ settleMs?: number;
66
+ } {
67
+ const { requestId, kind } = ClaimParams.parse(body);
68
+ return claimGuardianForm(requestId, kind);
69
+ }
70
+
71
+ const CLAIM_RESPONSE = z.object({
72
+ claimed: z.boolean(),
73
+ reason: z.enum(["already_claimed", "unknown", "wrong_kind"]).optional(),
74
+ settleMs: z
75
+ .number()
76
+ .optional()
77
+ .describe(
78
+ "How long a granted claim has to report its write back before the waiting call gives up.",
79
+ ),
80
+ });
81
+
82
+ export const GUARDIAN_FORM_ROUTES: RouteDefinition[] = [
83
+ {
84
+ operationId: "guardian_form_claim",
85
+ endpoint: "guardian_form_claim",
86
+ method: "POST",
87
+ policy: {
88
+ requiredScopes: ["settings.write"],
89
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
90
+ },
91
+ handler: claimForm,
92
+ summary: "Claim a pending guardian form for one submission",
93
+ description:
94
+ "Marks a pending form as answered so a second client submitting the same form writes nothing. Returns claimed=false with reason 'already_claimed' when somebody got there first, 'unknown' when no such form is pending, or 'wrong_kind' when the id belongs to a different form than the caller named. A granted claim carries the window its write has to report back in.",
95
+ tags: ["guardian-forms"],
96
+ requestBody: ClaimParams,
97
+ responseBody: CLAIM_RESPONSE,
98
+ },
99
+ {
100
+ operationId: "resolve_guardian_form",
101
+ endpoint: "resolve_guardian_form",
102
+ method: "POST",
103
+ policy: {
104
+ requiredScopes: ["settings.write"],
105
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
106
+ },
107
+ handler: resolveFormFromCallback,
108
+ summary: "Report a guardian form's outcome back to the waiting command",
109
+ description:
110
+ "Called by the writer once it has committed (or failed). Fields other than requestId and error are passed through to the parked caller untouched; an error makes the outcome a failure.",
111
+ tags: ["guardian-forms"],
112
+ requestBody: RequestIdParams.catchall(z.unknown()),
113
+ responseBody: z.object({ resolved: z.boolean() }),
114
+ },
115
+ ];
@@ -6,7 +6,10 @@
6
6
  * messages reach this handler.
7
7
  */
8
8
  import type { SourceMetadata } from "@vellumai/gateway-client";
9
- import { resolveInboundEventKind } from "@vellumai/gateway-client";
9
+ import {
10
+ resolveInboundEventKind,
11
+ resolveInboundReactionPayload,
12
+ } from "@vellumai/gateway-client";
10
13
  import {
11
14
  ADMISSION_POLICY_DEFAULT,
12
15
  type AdmissionPolicy,
@@ -18,6 +21,7 @@ import {
18
21
  attachmentsToContentBlocks,
19
22
  type MessageAttachmentInput,
20
23
  } from "../../agent/attachments.js";
24
+ import { listGuardianRequestDeliveriesByChat } from "../../channels/gateway-guardian-requests.js";
21
25
  import { audienceForReader } from "../../channels/message-audience.js";
22
26
  import {
23
27
  CHANNEL_IDS,
@@ -88,6 +92,7 @@ import { applyDeterministicTitleIfReplaceable } from "../../persistence/conversa
88
92
  import {
89
93
  clearPayload,
90
94
  findMessageBySourceId,
95
+ hasInboundEventForSource,
91
96
  recordInbound,
92
97
  } from "../../persistence/delivery-crud.js";
93
98
  import { markProcessed } from "../../persistence/delivery-status.js";
@@ -120,8 +125,8 @@ import { handleGuardianActivationIntercept } from "./inbound-stages/guardian-act
120
125
  import { handleGuardianReplyIntercept } from "./inbound-stages/guardian-reply-intercept.js";
121
126
  import { prepareChannelInboundContent } from "./inbound-stages/inbound-content-prep.js";
122
127
  import {
123
- handleSlackReactionIntercept,
124
- isSlackReactionEvent,
128
+ handleReactionIntercept,
129
+ isReactionEvent,
125
130
  } from "./inbound-stages/reaction-intercept.js";
126
131
  import { runSecretIngressCheck } from "./inbound-stages/secret-ingress-check.js";
127
132
  import { tryTranscribeAudioAttachments } from "./inbound-stages/transcribe-audio.js";
@@ -372,11 +377,13 @@ export async function handleChannelInbound({
372
377
  const hasCallbackData =
373
378
  typeof body.callbackData === "string" && body.callbackData.length > 0;
374
379
 
380
+ // Only a plain message must carry a body: every other family refers to
381
+ // another message and legitimately arrives empty (a delete has no
382
+ // content at all).
375
383
  if (
376
384
  trimmedContent.length === 0 &&
377
385
  !hasAttachments &&
378
- !isEdit &&
379
- !hasCallbackData
386
+ eventKind === "message"
380
387
  ) {
381
388
  throw new BadRequestError("content or attachmentIds is required");
382
389
  }
@@ -421,7 +428,7 @@ export async function handleChannelInbound({
421
428
  return guardianActivationResponse;
422
429
  }
423
430
 
424
- // ── Slack reaction handling ──
431
+ // ── Reaction handling ──
425
432
  // Reactions are passive channel signals — not messages, and not access
426
433
  // attempts. Dispatch them to a dedicated interceptor BEFORE the message
427
434
  // pipeline (ACL, admission floor, disk-pressure, conversation binding) so a
@@ -431,10 +438,20 @@ export async function handleChannelInbound({
431
438
  // transcript signals in the conversation of the reacted message, and routes
432
439
  // a guardian's reaction on an approval card through the guardian decision
433
440
  // pipeline. Reactions never mint a conversation and never drive an agent
434
- // turn.
435
- if (isSlackReactionEvent(body)) {
436
- return handleSlackReactionIntercept({
437
- callbackData: body.callbackData!,
441
+ // turn. A family member whose payload does not resolve (no emoji or no
442
+ // target message id) is dropped as noise here: the kind names the family,
443
+ // so it must never fall through and be read as a message.
444
+ if (isReactionEvent(body)) {
445
+ const reaction = resolveInboundReactionPayload(body);
446
+ if (!reaction) {
447
+ log.debug(
448
+ { sourceChannel, conversationExternalId },
449
+ "Dropping reaction with unresolvable payload",
450
+ );
451
+ return { accepted: true, reaction: "dropped_unresolvable_payload" };
452
+ }
453
+ return handleReactionIntercept({
454
+ reaction,
438
455
  sourceChannel,
439
456
  sourceInterface,
440
457
  conversationExternalId,
@@ -476,7 +493,10 @@ export async function handleChannelInbound({
476
493
  // respond to one by minting a verification challenge or creating an access
477
494
  // request (LUM-2673). Reaction callbacks never reach this point — the
478
495
  // intercept above returns for them.
479
- const isCallbackInteraction = hasCallbackData;
496
+ const isCallbackInteraction =
497
+ eventKind === "button" ||
498
+ eventKind === "reaction" ||
499
+ eventKind === "delete";
480
500
 
481
501
  // ── Ingress ACL enforcement ──
482
502
  const aclResult = await enforceIngressAcl({
@@ -493,26 +513,25 @@ export async function handleChannelInbound({
493
513
  assistantId,
494
514
  effectiveAdmissionPolicy: effectiveAdmissionPolicyForAcl,
495
515
  isCallbackInteraction,
516
+ // Scoped to deletes: the one family whose wire can name no actor. Any
517
+ // other kind carrying the flag still faces the full ACL.
518
+ actorUnattributed:
519
+ eventKind === "delete" && sourceMetadata?.actorUnattributed === true,
496
520
  });
497
521
  if (aclResult.earlyResponse) {
498
522
  return aclResult.earlyResponse;
499
523
  }
500
524
  const { resolvedMember } = aclResult;
501
525
 
502
- // ── Slack delete propagation ──
503
- // Slack message_deleted events are forwarded by the gateway with the
504
- // sentinel `callbackData = "message_deleted"` and `sourceMetadata.messageId`
505
- // set to the original (deleted) message's ts. Short-circuit the rest of
506
- // the pipeline: the agent loop should not run for delete notifications,
507
- // and routing the event through approval / agent paths would be incorrect.
508
- // We mark the stored row as deleted in slackMeta but leave `content`
509
- // untouched for audit purposes — rendering elides based on the deletedAt
510
- // marker. Gated behind ingress ACL so non-members cannot drive deletes
511
- // (matches the edit-intercept policy).
512
- // The Slack gate mirrors the reaction intercept's: deletes are ingested
513
- // from Slack alone today, and each further channel arrives as its own
514
- // decision with its own wire shape.
515
- if (sourceChannel === "slack" && eventKind === "delete") {
526
+ // ── Delete propagation ──
527
+ // A delete names the original via `sourceMetadata.messageId` and
528
+ // short-circuits the rest of the pipeline: no agent loop, no approval
529
+ // routing. The stored row keeps its content for audit; rendering elides on
530
+ // the deletedAt marker. An attributed delete (the wire names the deleted
531
+ // message's author, as Slack's does) passed the ingress ACL above; an
532
+ // unattributed one bypassed it under the ACL's stated contract and applies
533
+ // only to a row this daemon ingested.
534
+ if (eventKind === "delete") {
516
535
  const deletedMessageTs =
517
536
  typeof sourceMetadata?.messageId === "string"
518
537
  ? sourceMetadata.messageId
@@ -521,7 +540,7 @@ export async function handleChannelInbound({
521
540
  if (!deletedMessageTs) {
522
541
  log.debug(
523
542
  { conversationExternalId },
524
- "Slack message_deleted event missing sourceMetadata.messageId; ignoring",
543
+ "Delete event missing sourceMetadata.messageId; ignoring",
525
544
  );
526
545
  return { accepted: true, deleted: false };
527
546
  }
@@ -545,6 +564,20 @@ export async function handleChannelInbound({
545
564
  if (original) {
546
565
  break;
547
566
  }
567
+ // The retry window exists for one race: the original's inbound-event
568
+ // row is written before its message link lands. A source with no row
569
+ // at all was never ingested, so nothing can appear by waiting, and
570
+ // waiting would hold this conversation's serialized lane through the
571
+ // full miss window for every unrelated delete a busy room produces.
572
+ if (
573
+ !hasInboundEventForSource(
574
+ sourceChannel,
575
+ conversationExternalId,
576
+ deletedMessageTs,
577
+ )
578
+ ) {
579
+ break;
580
+ }
548
581
  if (attempt < deleteLookupRetries) {
549
582
  log.info(
550
583
  {
@@ -564,7 +597,7 @@ export async function handleChannelInbound({
564
597
  if (!original) {
565
598
  log.debug(
566
599
  { conversationExternalId, deletedMessageTs },
567
- "No stored message found for Slack delete after retries; ignoring",
600
+ "No stored message found for delete after retries; ignoring",
568
601
  );
569
602
  return { accepted: true, deleted: false };
570
603
  }
@@ -598,6 +631,9 @@ export async function handleChannelInbound({
598
631
  { deletedAt: Date.now() },
599
632
  );
600
633
  updateMessageMetadata(original.messageId, { providerMeta });
634
+ // The stamp lands in the store only; stale-marking makes a resident
635
+ // conversation's next turn reload and see the row as deleted.
636
+ findConversation(original.conversationId)?.markHistoryStale();
601
637
  log.info(
602
638
  {
603
639
  conversationExternalId,
@@ -618,6 +654,7 @@ export async function handleChannelInbound({
618
654
  // (channel, interface, provenance, etc.) untouched. Content column
619
655
  // is intentionally not updated.
620
656
  updateMessageMetadata(original.messageId, { slackMeta: updatedSlackMeta });
657
+ findConversation(original.conversationId)?.markHistoryStale();
621
658
 
622
659
  log.info(
623
660
  {
@@ -1168,11 +1205,11 @@ export async function handleChannelInbound({
1168
1205
  // so checking for empty content alone would miss stale callbacks.
1169
1206
  //
1170
1207
  // Reaction events (`reaction:` / `reaction_removed:`) are persisted by
1171
- // the earlier `isSlackReactionEvent` branch and never reach here; guard
1208
+ // the earlier `isReactionEvent` branch and never reach here; guard
1172
1209
  // explicitly so a future refactor can't let a reaction ts drive a
1173
1210
  // "This approval request has been resolved." edit that would clobber
1174
1211
  // the user's reacted-to message.
1175
- if (hasCallbackData && !isSlackReactionEvent(body)) {
1212
+ if (hasCallbackData && !isReactionEvent(body)) {
1176
1213
  // Record seen signal even for stale callbacks — the user still interacted
1177
1214
  if (sourceChannel === "telegram" || sourceChannel === "slack") {
1178
1215
  try {
@@ -2106,6 +2143,19 @@ async function runBackfillSlackDmIfCold(params: {
2106
2143
  return;
2107
2144
  }
2108
2145
 
2146
+ // Guardian approval cards posted to this chat are delivery
2147
+ // projections of canonical requests, never conversation content, so
2148
+ // their message ts must not be imported as transcript rows. The
2149
+ // gateway delivery registry is the authority on which messages
2150
+ // those are (it covers cards from every daemon build). An
2151
+ // unreachable gateway aborts the backfill rather than importing a
2152
+ // card: the DM stays cold, so the next inbound message retries.
2153
+ const guardianCardTs = new Set(
2154
+ (await listGuardianRequestDeliveriesByChat("slack", params.channelId))
2155
+ .map((delivery) => delivery.destinationMessageId)
2156
+ .filter((ts): ts is string => typeof ts === "string" && ts.length > 0),
2157
+ );
2158
+
2109
2159
  const seen = readStoredSlackChannelTs(params.conversationId);
2110
2160
  let written = 0;
2111
2161
  // Slack's conversation.history returns most-recent first. Reverse so
@@ -2117,6 +2167,9 @@ async function runBackfillSlackDmIfCold(params: {
2117
2167
  if (seen.has(message.id)) {
2118
2168
  continue;
2119
2169
  }
2170
+ if (guardianCardTs.has(message.id)) {
2171
+ continue;
2172
+ }
2120
2173
  if (await isSlackAssistantThreadPlaceholder(message, params.account)) {
2121
2174
  continue;
2122
2175
  }
@@ -116,9 +116,9 @@ export interface AclEnforcementParams {
116
116
  */
117
117
  effectiveAdmissionPolicy?: AdmissionPolicy;
118
118
  /**
119
- * True when the inbound event is an interaction callback (e.g. a Slack
120
- * Block Kit button press or a message_deleted sentinel) rather than a
121
- * message the sender composed. Callbacks are decision attempts / lifecycle
119
+ * True when the inbound event is an interaction callback (a button
120
+ * press, a reaction, a delete: any kind that is not a composed message
121
+ * or an edit of one). Callbacks are decision attempts / lifecycle
122
122
  * events, not access attempts: a denied callback must never mint a
123
123
  * verification challenge or create an access request — a stale button
124
124
  * press from an unrecognized sender would otherwise spawn a fresh
@@ -127,6 +127,15 @@ export interface AclEnforcementParams {
127
127
  * caller makes the classification explicitly.
128
128
  */
129
129
  isCallbackInteraction: boolean;
130
+ /**
131
+ * The platform named no actor for this event (the id is a channel's
132
+ * synthetic system identity). There is no identity claim to enforce, so
133
+ * the ACL neither resolves a member nor denies: the family stage that
134
+ * consumes such an event applies it only to rows whose author cleared
135
+ * this ACL when the original message arrived. Callers set this ONLY for
136
+ * event kinds that cannot start an agent turn.
137
+ */
138
+ actorUnattributed?: boolean;
130
139
  }
131
140
 
132
141
  /**
@@ -206,6 +215,10 @@ export async function enforceIngressAcl(
206
215
  isCallbackInteraction,
207
216
  } = params;
208
217
 
218
+ if (params.actorUnattributed) {
219
+ return { resolvedMember: null };
220
+ }
221
+
209
222
  let validatedBootstrapSession: VerificationSessionWire | undefined;
210
223
 
211
224
  // Identity signals forwarded via sourceMetadata: bot flag (Slack users.info
@@ -16,6 +16,7 @@
16
16
  * resolvable target is dropped.
17
17
  */
18
18
  import type { ChannelId } from "../../../channels/types.js";
19
+ import { findConversation } from "../../../daemon/conversation-registry.js";
19
20
  import {
20
21
  mergeSlackMetadata,
21
22
  readSlackMetadata,
@@ -227,6 +228,11 @@ export async function handleEditIntercept(
227
228
  // path, so reindex the message into the lexical index — the idempotent
228
229
  // upsert replaces the stale Qdrant point with the edited content.
229
230
  enqueueLexicalIndexForMessage(original.messageId);
231
+ // The rewrite lands in the store only; a resident conversation keeps the
232
+ // pre-edit text in memory until it reloads, so the next turn would answer
233
+ // a revision the sender already replaced. Stale-marking makes that turn's
234
+ // history reload pick up the edit.
235
+ findConversation(original.conversationId)?.markHistoryStale();
230
236
  markProcessed(editResult.eventId);
231
237
  log.info(
232
238
  { assistantId, sourceMessageId, messageId: original.messageId },
@@ -9,7 +9,13 @@ let mockIdentityRequests: { id: string }[] = [];
9
9
  let routeGuardianReplyCalls: unknown[] = [];
10
10
  let deliverChannelReplyCalls: unknown[][] = [];
11
11
 
12
+ // Spread the actual module so transitive importers of names this
13
+ // factory does not stub keep resolving (a partial factory breaks at
14
+ // import time when the graph gains a new named import).
15
+ const actualGatewayGuardianRequests =
16
+ await import("../../../channels/gateway-guardian-requests.js");
12
17
  mock.module("../../../channels/gateway-guardian-requests.js", () => ({
18
+ ...actualGatewayGuardianRequests,
13
19
  listPendingRequestsByDestinationOrEmpty: async () =>
14
20
  mockDeliveryScopedRequests,
15
21
  listGuardianRequestsOrEmpty: async () => mockIdentityRequests,
@@ -9,6 +9,8 @@
9
9
  * Extracted from inbound-message-handler.ts to keep the top-level handler
10
10
  * focused on orchestration.
11
11
  */
12
+ import type { InboundReactionPayload } from "@vellumai/gateway-client";
13
+
12
14
  import {
13
15
  listGuardianRequestsOrEmpty,
14
16
  listPendingRequestsByDestinationOrEmpty,
@@ -35,6 +37,7 @@ export interface GuardianReplyInterceptParams {
35
37
  trimmedContent: string;
36
38
  hasCallbackData: boolean;
37
39
  callbackData: string | undefined;
40
+ reaction?: InboundReactionPayload;
38
41
  /**
39
42
  * For emoji-reaction decisions: the channel-native id (Slack `ts`) of the
40
43
  * message the reaction was attached to. Threaded to the router so it can
@@ -81,6 +84,7 @@ export async function handleGuardianReplyIntercept(
81
84
  trimmedContent,
82
85
  hasCallbackData,
83
86
  callbackData,
87
+ reaction,
84
88
  reactedMessageTs,
85
89
  rawSenderId,
86
90
  canonicalSenderId,
@@ -136,7 +140,7 @@ export async function handleGuardianReplyIntercept(
136
140
  // Reactions address one specific request by the reacted card's message id,
137
141
  // so they bypass the pending-request list scoping that the text/NL paths
138
142
  // need — the router's reaction branch resolves the target directly.
139
- const isReaction = callbackData?.startsWith("reaction:") === true;
143
+ const isReaction = reaction !== undefined;
140
144
  let pendingScope: GuardianPendingScope | undefined;
141
145
  if (!isReaction) {
142
146
  // Hint reads degrade to empty on gateway failure: Slack then blocks the
@@ -178,6 +182,7 @@ export async function handleGuardianReplyIntercept(
178
182
  },
179
183
  conversationId,
180
184
  callbackData,
185
+ ...(reaction ? { reaction } : {}),
181
186
  reactedMessageTs,
182
187
  pendingScope,
183
188
  approvalConversationGenerator,