@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
@@ -72,6 +72,7 @@ mock.module("../../../contacts/contact-store.js", () => ({
72
72
  let recordInboundCalls: Array<{ conversationId?: string }> = [];
73
73
  let recordedEvent: { eventId: string; conversationId: string } | null = null;
74
74
  let outboundTargetConversationId: string | null = null;
75
+ let outboundLookupChannels: string[] = [];
75
76
  let storedTarget: { messageId: string; conversationId: string } | null = null;
76
77
  mock.module("../../../persistence/delivery-crud.js", () => ({
77
78
  recordInbound: (
@@ -89,7 +90,10 @@ mock.module("../../../persistence/delivery-crud.js", () => ({
89
90
  };
90
91
  },
91
92
  findMessageBySourceId: () => storedTarget,
92
- findSlackConversationByMessageTs: () => outboundTargetConversationId,
93
+ findConversationByProviderMessageId: (sourceChannel: string) => {
94
+ outboundLookupChannels.push(sourceChannel);
95
+ return outboundTargetConversationId;
96
+ },
93
97
  findInboundEvent: () => recordedEvent,
94
98
  clearPayload: () => {},
95
99
  linkMessage: () => {},
@@ -134,7 +138,7 @@ mock.module("./guardian-reply-intercept.js", () => ({
134
138
  },
135
139
  }));
136
140
 
137
- import { handleSlackReactionIntercept } from "./reaction-intercept.js";
141
+ import { handleReactionIntercept } from "./reaction-intercept.js";
138
142
 
139
143
  // ---------------------------------------------------------------------------
140
144
  // Fixtures
@@ -178,7 +182,11 @@ function buildParams(overrides: {
178
182
  }) {
179
183
  msgCounter++;
180
184
  return {
181
- callbackData: "reaction:white_check_mark",
185
+ reaction: {
186
+ op: "added" as const,
187
+ emoji: "white_check_mark",
188
+ targetMessageId: "1700000000.1",
189
+ },
182
190
  sourceChannel: "slack" as const,
183
191
  sourceInterface: "slack" as const,
184
192
  conversationExternalId: SLACK_CHANNEL_ID,
@@ -217,6 +225,7 @@ describe("reaction intercept consumes the stamped verdict directly", () => {
217
225
  recordInboundCalls = [];
218
226
  recordedEvent = null;
219
227
  outboundTargetConversationId = null;
228
+ outboundLookupChannels = [];
220
229
  addMessageCalls = 0;
221
230
  guardianReplyCalls = [];
222
231
  guardianReplyResponse = undefined;
@@ -226,7 +235,7 @@ describe("reaction intercept consumes the stamped verdict directly", () => {
226
235
  test("guardian verdict routes the reaction into the approval decision pipeline", async () => {
227
236
  guardianReplyResponse = { accepted: true, canonicalRouter: "applied" };
228
237
 
229
- const result = await handleSlackReactionIntercept(
238
+ const result = await handleReactionIntercept(
230
239
  buildParams({
231
240
  rawSenderId: GUARDIAN_USER_ID,
232
241
  trustVerdict: GUARDIAN_VERDICT,
@@ -242,7 +251,7 @@ describe("reaction intercept consumes the stamped verdict directly", () => {
242
251
  });
243
252
 
244
253
  test("contact verdict records the reaction; the decision pipeline self-gate ignores it", async () => {
245
- const result = await handleSlackReactionIntercept(
254
+ const result = await handleReactionIntercept(
246
255
  buildParams({
247
256
  rawSenderId: MEMBER_USER_ID,
248
257
  trustVerdict: MEMBER_VERDICT,
@@ -261,12 +270,12 @@ describe("reaction intercept consumes the stamped verdict directly", () => {
261
270
  expect(recordInboundCalls).toEqual([{ conversationId: "conv-target" }]);
262
271
  });
263
272
 
264
- test("a reaction on the assistant's own post resolves through slackMeta", async () => {
265
- // Outbound posts open no inbound event, so the ts is only on the row.
273
+ test("a reaction on the assistant's own post resolves through its stored envelope", async () => {
274
+ // Outbound posts open no inbound event, so the id is only on the row.
266
275
  storedTarget = null;
267
276
  outboundTargetConversationId = "conv-assistant-post";
268
277
 
269
- const result = await handleSlackReactionIntercept(
278
+ const result = await handleReactionIntercept(
270
279
  buildParams({
271
280
  rawSenderId: MEMBER_USER_ID,
272
281
  trustVerdict: MEMBER_VERDICT,
@@ -278,12 +287,36 @@ describe("reaction intercept consumes the stamped verdict directly", () => {
278
287
  ]);
279
288
  expect(addMessageCalls).toBe(1);
280
289
  expect(result).toMatchObject({ accepted: true, duplicate: false });
290
+ // The outbound-row lookup is scoped to the reaction's own channel: a
291
+ // Slack reaction never scans another channel's conversations.
292
+ expect(outboundLookupChannels).toEqual(["slack"]);
293
+ });
294
+
295
+ test("a non-Slack reaction resolves the assistant's post on its own channel", async () => {
296
+ storedTarget = null;
297
+ outboundTargetConversationId = "conv-discord-post";
298
+
299
+ const params = {
300
+ ...buildParams({
301
+ rawSenderId: MEMBER_USER_ID,
302
+ trustVerdict: MEMBER_VERDICT,
303
+ }),
304
+ sourceChannel: "discord" as const,
305
+ sourceInterface: undefined,
306
+ };
307
+ const result = await handleReactionIntercept(params);
308
+
309
+ expect(outboundLookupChannels).toEqual(["discord"]);
310
+ expect(recordInboundCalls).toEqual([
311
+ { conversationId: "conv-discord-post" },
312
+ ]);
313
+ expect(result).toMatchObject({ accepted: true, duplicate: false });
281
314
  });
282
315
 
283
316
  test("a reaction on a message that is not stored is dropped without minting", async () => {
284
317
  storedTarget = null;
285
318
 
286
- const result = await handleSlackReactionIntercept(
319
+ const result = await handleReactionIntercept(
287
320
  buildParams({
288
321
  rawSenderId: MEMBER_USER_ID,
289
322
  trustVerdict: MEMBER_VERDICT,
@@ -298,7 +331,7 @@ describe("reaction intercept consumes the stamped verdict directly", () => {
298
331
  test("a redelivered reaction never reaches the guardian rail twice", async () => {
299
332
  recordedEvent = { eventId: "evt-1", conversationId: "conv-target" };
300
333
 
301
- const result = await handleSlackReactionIntercept(
334
+ const result = await handleReactionIntercept(
302
335
  buildParams({
303
336
  rawSenderId: GUARDIAN_USER_ID,
304
337
  trustVerdict: GUARDIAN_VERDICT,
@@ -319,7 +352,7 @@ describe("reaction intercept consumes the stamped verdict directly", () => {
319
352
  storedTarget = null;
320
353
  guardianReplyResponse = { accepted: true, canonicalRouter: "applied" };
321
354
 
322
- const result = await handleSlackReactionIntercept(
355
+ const result = await handleReactionIntercept(
323
356
  buildParams({
324
357
  rawSenderId: GUARDIAN_USER_ID,
325
358
  trustVerdict: GUARDIAN_VERDICT,
@@ -331,21 +364,21 @@ describe("reaction intercept consumes the stamped verdict directly", () => {
331
364
  });
332
365
 
333
366
  test("unknown verdict is dropped before any write", async () => {
334
- const result = await handleSlackReactionIntercept(
367
+ const result = await handleReactionIntercept(
335
368
  buildParams({ rawSenderId: "U_STRANGER", trustVerdict: UNKNOWN_VERDICT }),
336
369
  );
337
370
  expectDropped(result);
338
371
  });
339
372
 
340
373
  test("missing verdict is dropped fail-closed", async () => {
341
- const result = await handleSlackReactionIntercept(
374
+ const result = await handleReactionIntercept(
342
375
  buildParams({ rawSenderId: MEMBER_USER_ID }),
343
376
  );
344
377
  expectDropped(result);
345
378
  });
346
379
 
347
380
  test("resolutionFailed verdict is dropped fail-closed even with a guardian shape", async () => {
348
- const result = await handleSlackReactionIntercept(
381
+ const result = await handleReactionIntercept(
349
382
  buildParams({
350
383
  rawSenderId: GUARDIAN_USER_ID,
351
384
  trustVerdict: { ...GUARDIAN_VERDICT, resolutionFailed: true },
@@ -355,7 +388,7 @@ describe("reaction intercept consumes the stamped verdict directly", () => {
355
388
  });
356
389
 
357
390
  test("memberless guardian verdict is contradictory and dropped fail-closed", async () => {
358
- const result = await handleSlackReactionIntercept(
391
+ const result = await handleReactionIntercept(
359
392
  buildParams({
360
393
  rawSenderId: GUARDIAN_USER_ID,
361
394
  trustVerdict: {
@@ -370,13 +403,13 @@ describe("reaction intercept consumes the stamped verdict directly", () => {
370
403
 
371
404
  test("classification is verdict-only: no IPC, cache, or local-store reads", async () => {
372
405
  guardianReplyResponse = { accepted: true, canonicalRouter: "applied" };
373
- await handleSlackReactionIntercept(
406
+ await handleReactionIntercept(
374
407
  buildParams({
375
408
  rawSenderId: GUARDIAN_USER_ID,
376
409
  trustVerdict: GUARDIAN_VERDICT,
377
410
  }),
378
411
  );
379
- await handleSlackReactionIntercept(
412
+ await handleReactionIntercept(
380
413
  buildParams({
381
414
  rawSenderId: MEMBER_USER_ID,
382
415
  trustVerdict: MEMBER_VERDICT,
@@ -20,25 +20,35 @@
20
20
  * `unknown` (drop). Reactions never drive an agent turn.
21
21
  */
22
22
  import type { SourceMetadata } from "@vellumai/gateway-client";
23
- import { resolveInboundEventKind } from "@vellumai/gateway-client";
23
+ import {
24
+ type InboundReactionPayload,
25
+ resolveInboundEventKind,
26
+ } from "@vellumai/gateway-client";
24
27
 
25
28
  import type { ChannelId, InterfaceId } from "../../../channels/types.js";
29
+ import { findConversation } from "../../../daemon/conversation-registry.js";
26
30
  import { getDiskPressureStatus } from "../../../daemon/disk-pressure-guard.js";
27
31
  import { classifyDiskPressureTurnPolicy } from "../../../daemon/disk-pressure-policy.js";
32
+ import type { TrustContext } from "../../../daemon/trust-context-types.js";
33
+ import { writeSlackMetadata } from "../../../messaging/providers/slack/message-metadata.js";
34
+ import {
35
+ buildNeutralReactionMeta,
36
+ buildSlackReactionMeta,
37
+ } from "../../../messaging/reaction-envelopes.js";
28
38
  import {
29
- type SlackMessageMetadata,
30
- writeSlackMetadata,
31
- } from "../../../messaging/providers/slack/message-metadata.js";
32
- import { addMessage } from "../../../persistence/conversation-crud.js";
39
+ addMessage,
40
+ provenanceFromTrustContext,
41
+ } from "../../../persistence/conversation-crud.js";
33
42
  import {
43
+ findConversationByProviderMessageId,
34
44
  findInboundEvent,
35
45
  findMessageBySourceId,
36
- findSlackConversationByMessageTs,
37
46
  linkMessage,
38
47
  recordInbound,
39
48
  } from "../../../persistence/delivery-crud.js";
40
49
  import { markProcessed } from "../../../persistence/delivery-status.js";
41
50
  import { getLogger } from "../../../util/logger.js";
51
+ import { toTrustContext } from "../../actor-trust-resolver.js";
42
52
  import type { ApprovalConversationGenerator } from "../../http-types.js";
43
53
  import {
44
54
  actorTrustContextFromVerdict,
@@ -49,56 +59,24 @@ import { handleGuardianReplyIntercept } from "./guardian-reply-intercept.js";
49
59
  const log = getLogger("runtime-http");
50
60
 
51
61
  /**
52
- * Detect a Slack reaction event by inspecting the inbound payload's
53
- * `callbackData` prefix. The gateway encodes reactions as a unified
54
- * `SlackInboundEvent` with `callbackData` of the form `reaction:<emoji>`
55
- * (added) or `reaction_removed:<emoji>` (removed) — see
56
- * `gateway/src/slack/normalize.ts`. This helper centralizes that convention
57
- * so the daemon can route reactions to this dedicated stage instead of the
58
- * agent-response pipeline.
62
+ * Whether the inbound payload belongs to the reaction event family, on any
63
+ * channel. Family membership is the kind alone; whether the payload can be
64
+ * acted on is `resolveInboundReactionPayload`'s answer, and the dispatch
65
+ * drops a family member whose payload does not resolve rather than letting
66
+ * it be reinterpreted as a message.
59
67
  */
60
- export function isSlackReactionEvent(body: {
61
- sourceChannel?: string;
68
+ export function isReactionEvent(body: {
62
69
  eventKind?: string;
70
+ isEdit?: boolean;
63
71
  callbackData?: string;
72
+ callbackQueryId?: string;
64
73
  }): boolean {
65
- // The Slack gate is deliberate, not residue: Slack is the only channel
66
- // whose reactions are ingested today, and widening this is a per-channel
67
- // decision made with that channel's reaction wire shape, not a fallthrough.
68
- return (
69
- body.sourceChannel === "slack" &&
70
- resolveInboundEventKind(body) === "reaction"
71
- );
72
- }
73
-
74
- /**
75
- * Parse a reaction `callbackData` string into its op (added/removed) and
76
- * emoji name. Returns `null` when the input is not a reaction prefix or
77
- * when the emoji portion is empty.
78
- */
79
- export function parseSlackReactionCallbackData(
80
- callbackData: string,
81
- ): { op: "added" | "removed"; emoji: string } | null {
82
- let op: "added" | "removed";
83
- let emoji: string;
84
- if (callbackData.startsWith("reaction_removed:")) {
85
- op = "removed";
86
- emoji = callbackData.slice("reaction_removed:".length);
87
- } else if (callbackData.startsWith("reaction:")) {
88
- op = "added";
89
- emoji = callbackData.slice("reaction:".length);
90
- } else {
91
- return null;
92
- }
93
- if (emoji.length === 0) {
94
- return null;
95
- }
96
- return { op, emoji };
74
+ return resolveInboundEventKind(body) === "reaction";
97
75
  }
98
76
 
99
77
  export interface ReactionInterceptParams {
100
- /** The reaction callbackData (`reaction:<emoji>` / `reaction_removed:<emoji>`). */
101
- callbackData: string;
78
+ /** The resolved structured payload: op, emoji, and target message id. */
79
+ reaction: InboundReactionPayload;
102
80
  sourceChannel: ChannelId;
103
81
  sourceInterface: InterfaceId | undefined;
104
82
  conversationExternalId: string;
@@ -113,15 +91,15 @@ export interface ReactionInterceptParams {
113
91
  }
114
92
 
115
93
  /**
116
- * Handle a Slack reaction event end to end. Always consumes the event (the
117
- * caller dispatches here only for `isSlackReactionEvent`), returning the
94
+ * Handle a reaction event end to end, on any channel. Always consumes the event (the
95
+ * caller dispatches here only for `isReactionEvent`), returning the
118
96
  * response the top-level handler should short-circuit with.
119
97
  */
120
- export async function handleSlackReactionIntercept(
98
+ export async function handleReactionIntercept(
121
99
  params: ReactionInterceptParams,
122
100
  ): Promise<Record<string, unknown>> {
123
101
  const {
124
- callbackData,
102
+ reaction,
125
103
  sourceChannel,
126
104
  sourceInterface,
127
105
  conversationExternalId,
@@ -162,10 +140,7 @@ export async function handleSlackReactionIntercept(
162
140
  return { accepted: true, reaction: "dropped_unknown_actor" };
163
141
  }
164
142
 
165
- const reactedMessageTs =
166
- typeof sourceMetadata?.messageId === "string"
167
- ? sourceMetadata.messageId
168
- : undefined;
143
+ const reactedMessageTs = reaction.targetMessageId;
169
144
  // Respect disk-pressure cleanup so reactions don't bypass storage
170
145
  // protection. Guardians resolve to `allow-cleanup-mode` (not `block`), so a
171
146
  // guardian's approval-by-reaction still flows. Blocked silently and before
@@ -210,12 +185,13 @@ export async function handleSlackReactionIntercept(
210
185
  // `reaction_removed:` never does. `handleGuardianReplyIntercept` self-gates
211
186
  // on `trustClass === "guardian"`, so a contact's reaction returns no
212
187
  // response and falls through to persistence.
213
- if (callbackData.startsWith("reaction:") && replyCallbackUrl) {
188
+ if (reaction.op === "added" && replyCallbackUrl) {
214
189
  const reactionIntercept = await handleGuardianReplyIntercept({
215
190
  isDuplicate: false,
216
191
  trimmedContent: "",
217
192
  hasCallbackData: true,
218
- callbackData,
193
+ callbackData: undefined,
194
+ reaction,
219
195
  reactedMessageTs,
220
196
  rawSenderId,
221
197
  canonicalSenderId,
@@ -239,14 +215,16 @@ export async function handleSlackReactionIntercept(
239
215
  // reaction is dropped rather than given a conversation of its own.
240
216
  // Inbound messages carry their provider id on the event that delivered
241
217
  // them. The assistant's own posts open no inbound event, so a reaction on
242
- // one is resolved through the `slackMeta` those rows carry.
218
+ // one is resolved through the envelope those rows carry (`slackMeta` on
219
+ // Slack, the neutral `providerMeta` everywhere else).
243
220
  const targetConversationId = reactedMessageTs
244
221
  ? (findMessageBySourceId(
245
222
  sourceChannel,
246
223
  conversationExternalId,
247
224
  reactedMessageTs,
248
225
  )?.conversationId ??
249
- findSlackConversationByMessageTs(
226
+ findConversationByProviderMessageId(
227
+ sourceChannel,
250
228
  conversationExternalId,
251
229
  reactedMessageTs,
252
230
  ))
@@ -272,15 +250,24 @@ export async function handleSlackReactionIntercept(
272
250
 
273
251
  // Record the reaction as an inline transcript signal.
274
252
  try {
275
- await persistSlackReactionAsMessage({
253
+ await persistReactionAsMessage({
276
254
  conversationId: result.conversationId,
277
255
  conversationExternalId,
278
256
  eventId: result.eventId,
279
- callbackData,
257
+ sourceChannel,
258
+ reaction,
280
259
  actorDisplayName,
260
+ actorExternalId: canonicalSenderId ?? rawSenderId ?? undefined,
281
261
  reactedMessageTs,
282
262
  duplicate: result.duplicate,
263
+ trustCtx: toTrustContext(trustCtx, conversationExternalId),
283
264
  });
265
+ if (!result.duplicate) {
266
+ // Reactions never drive a turn, so a resident conversation would
267
+ // otherwise carry the new row only after eviction. Marking it stale
268
+ // makes the next turn's history reload pick the reaction up.
269
+ findConversation(result.conversationId)?.markHistoryStale();
270
+ }
284
271
  } catch (err) {
285
272
  log.error(
286
273
  { err, conversationId: result.conversationId, eventId: result.eventId },
@@ -305,53 +292,66 @@ export async function handleSlackReactionIntercept(
305
292
  * deduplication and conversation resolution have happened. Duplicate inbound
306
293
  * events are skipped here to keep persistence idempotent.
307
294
  */
308
- async function persistSlackReactionAsMessage(params: {
295
+ async function persistReactionAsMessage(params: {
309
296
  conversationId: string;
310
297
  conversationExternalId: string;
311
298
  eventId: string;
312
- callbackData: string;
299
+ sourceChannel: ChannelId;
300
+ reaction: InboundReactionPayload;
313
301
  actorDisplayName?: string;
302
+ /**
303
+ * The reactor's stable channel identity, canonical when resolution
304
+ * succeeded. Persisted as the envelope's actor id: a display name is
305
+ * sender-controlled labeling, and the rendered history line attributes
306
+ * its fenced content by this id (`origin`), never by the label.
307
+ */
308
+ actorExternalId?: string;
314
309
  reactedMessageTs: string;
315
310
  duplicate: boolean;
311
+ /**
312
+ * The reactor's trust context. Persisted as row provenance so actor-scoped
313
+ * history loads keep the row: `filterMessagesForUntrustedActor` drops any
314
+ * row with no `provenanceTrustClass`, which would hide every reaction from
315
+ * non-guardian turns.
316
+ */
317
+ trustCtx: TrustContext;
316
318
  }): Promise<void> {
317
319
  if (params.duplicate) {
318
320
  return;
319
321
  }
322
+ const facts = {
323
+ channel: params.sourceChannel,
324
+ chatId: params.conversationExternalId,
325
+ targetMessageId: params.reactedMessageTs,
326
+ emoji: params.reaction.emoji,
327
+ op: params.reaction.op,
328
+ ...(params.actorExternalId
329
+ ? { actorExternalId: params.actorExternalId }
330
+ : {}),
331
+ ...(params.actorDisplayName
332
+ ? { actorDisplayName: params.actorDisplayName }
333
+ : {}),
334
+ };
320
335
 
321
- const parsed = parseSlackReactionCallbackData(params.callbackData);
322
- if (!parsed) {
323
- log.debug(
336
+ if (params.sourceChannel !== "slack") {
337
+ const persisted = await addMessage(
338
+ params.conversationId,
339
+ "user",
340
+ "[reaction]",
324
341
  {
325
- conversationId: params.conversationId,
326
- callbackData: params.callbackData,
342
+ metadata: {
343
+ ...provenanceFromTrustContext(params.trustCtx),
344
+ providerMeta: JSON.stringify(buildNeutralReactionMeta(facts)),
345
+ },
346
+ skipIndexing: true,
327
347
  },
328
- "Skipping reaction persistence: unparseable callbackData",
329
348
  );
349
+ linkMessage(params.eventId, persisted.id);
350
+ markProcessed(params.eventId);
330
351
  return;
331
352
  }
332
353
 
333
- const slackMeta: SlackMessageMetadata = {
334
- source: "slack",
335
- channelId: params.conversationExternalId,
336
- channelTs: params.reactedMessageTs,
337
- eventKind: "reaction",
338
- // No `threadTs`: Slack sends none on a reaction, so the gateway's thread
339
- // id here is the reacted message's own ts. It equals `channelTs`, which
340
- // every reader treats as "not in a thread" anyway, and storing it makes
341
- // the row false evidence that a thread belongs to this conversation
342
- // (`legacySlackConversationHasThreadEvidence`).
343
- ...(params.actorDisplayName
344
- ? { displayName: params.actorDisplayName }
345
- : {}),
346
- reaction: {
347
- emoji: parsed.emoji,
348
- targetChannelTs: params.reactedMessageTs,
349
- op: parsed.op,
350
- ...(params.actorDisplayName
351
- ? { actorDisplayName: params.actorDisplayName }
352
- : {}),
353
- },
354
- };
354
+ const slackMeta = buildSlackReactionMeta(facts);
355
355
 
356
356
  // Sentinel content — Slack transcript renderers read `slackMeta` to format
357
357
  // the reaction line; the literal text is never displayed to the model.
@@ -360,7 +360,10 @@ async function persistSlackReactionAsMessage(params: {
360
360
  "user",
361
361
  "[reaction]",
362
362
  {
363
- metadata: { slackMeta: writeSlackMetadata(slackMeta) },
363
+ metadata: {
364
+ ...provenanceFromTrustContext(params.trustCtx),
365
+ slackMeta: writeSlackMetadata(slackMeta),
366
+ },
364
367
  skipIndexing: true,
365
368
  },
366
369
  );
@@ -74,6 +74,7 @@ import { ROUTES as GATEWAY_STATUS_ROUTES } from "./gateway-status-routes.js";
74
74
  import { ROUTES as GLOBAL_SEARCH_ROUTES } from "./global-search-routes.js";
75
75
  import { ROUTES as GROUP_ROUTES } from "./group-routes.js";
76
76
  import { ROUTES as GUARDIAN_ACTION_ROUTES } from "./guardian-action-routes.js";
77
+ import { GUARDIAN_FORM_ROUTES } from "./guardian-form-routes.js";
77
78
  import { ROUTES as HEARTBEAT_ROUTES } from "./heartbeat-routes.js";
78
79
  import { ROUTES as HOME_FEED_ROUTES } from "./home-feed-routes.js";
79
80
  import { ROUTES as HOME_STATE_ROUTES } from "./home-state-routes.js";
@@ -193,6 +194,7 @@ export const ROUTES: RouteDefinition[] = [
193
194
  ...CLIENT_ROUTES,
194
195
  ...CONTENT_SOURCE_ROUTES,
195
196
  ...CONTACT_PROMPT_ROUTES,
197
+ ...GUARDIAN_FORM_ROUTES,
196
198
  ...CONTACT_ROUTES,
197
199
  ...CONVERSATION_ATTENTION_ROUTES,
198
200
  ...CONVERSATION_CLI_ROUTES,
@@ -19,8 +19,39 @@ import {
19
19
  } from "../../../../daemon/handlers/config-slack-channel.js";
20
20
  import { ACTOR_PRINCIPALS } from "../../../auth/route-policy.js";
21
21
  import { BadRequestError } from "../../errors.js";
22
+ import { parseBody } from "../../parse-body.js";
22
23
  import type { RouteDefinition, RouteHandlerArgs } from "../../types.js";
23
24
 
25
+ // ---------------------------------------------------------------------------
26
+ // Body schemas
27
+ //
28
+ // Each is named once and used twice: as the route's `requestBody`, a codegen
29
+ // signal only, and by `parseBody` in the handler, which is what rejects a
30
+ // malformed request. One declaration keeps the wire contract and the runtime
31
+ // check in agreement.
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /**
35
+ * Every field is optional because `setSlackChannelConfig` accepts any subset:
36
+ * a user token alone is a valid update that leaves the bot and app tokens in
37
+ * place. The wizard requires both bot and app tokens, and enforces that in
38
+ * `use-save-slack-config.ts` where that requirement belongs.
39
+ */
40
+ const SetSlackChannelConfigBody = z.object({
41
+ botToken: z.string().optional().describe("Slack bot token"),
42
+ appToken: z.string().optional().describe("Slack app-level token"),
43
+ userToken: z
44
+ .string()
45
+ .optional()
46
+ .describe("Slack user token, for reading channels the bot is not in"),
47
+ });
48
+
49
+ const PatchSlackChannelConfigBody = z.object({
50
+ threadMode: SlackThreadMode.describe(
51
+ "Controls whether the bot follows threads after an initial @mention",
52
+ ).optional(),
53
+ });
54
+
24
55
  // ---------------------------------------------------------------------------
25
56
  // Handlers
26
57
  // ---------------------------------------------------------------------------
@@ -32,22 +63,19 @@ async function handleGetSlackChannelConfig() {
32
63
  export async function handleSetSlackChannelConfig({
33
64
  body = {},
34
65
  }: RouteHandlerArgs) {
35
- const { botToken, appToken, userToken } = body as {
36
- botToken?: string;
37
- appToken?: string;
38
- userToken?: string;
39
- };
66
+ const { botToken, appToken, userToken } = parseBody(
67
+ SetSlackChannelConfigBody,
68
+ body,
69
+ );
40
70
  const result = await setSlackChannelConfig(botToken, appToken, userToken);
41
71
  if (!result.success) {
42
- throw new BadRequestError(
43
- (result as { error?: string }).error ?? "Failed to set Slack config",
44
- );
72
+ throw new BadRequestError(result.error ?? "Failed to set Slack config");
45
73
  }
46
74
  return result;
47
75
  }
48
76
 
49
77
  async function handlePatchSlackChannelConfig({ body = {} }: RouteHandlerArgs) {
50
- const { threadMode } = body as { threadMode?: string };
78
+ const { threadMode } = parseBody(PatchSlackChannelConfigBody, body);
51
79
  if (threadMode !== undefined) {
52
80
  const parsed = SlackThreadMode.safeParse(threadMode);
53
81
  if (!parsed.success) {
@@ -95,10 +123,7 @@ export const ROUTES: RouteDefinition[] = [
95
123
  description: "Validate and store Slack channel credentials.",
96
124
  tags: ["integrations"],
97
125
  handler: handleSetSlackChannelConfig,
98
- requestBody: z.object({
99
- botToken: z.string().describe("Slack bot token"),
100
- appToken: z.string().describe("Slack app-level token"),
101
- }),
126
+ requestBody: SetSlackChannelConfigBody,
102
127
  responseBody: SlackChannelConfigResultSchema,
103
128
  },
104
129
  {
@@ -113,11 +138,7 @@ export const ROUTES: RouteDefinition[] = [
113
138
  description: "Update Slack channel behavior settings (e.g. thread mode).",
114
139
  tags: ["integrations"],
115
140
  handler: handlePatchSlackChannelConfig,
116
- requestBody: z.object({
117
- threadMode: SlackThreadMode.describe(
118
- "Controls whether the bot follows threads after an initial @mention",
119
- ).optional(),
120
- }),
141
+ requestBody: PatchSlackChannelConfigBody,
121
142
  responseBody: SlackChannelConfigResultSchema,
122
143
  },
123
144
  {
@@ -44,7 +44,6 @@ import {
44
44
  } from "../../security/secure-keys.js";
45
45
  import {
46
46
  getCredentialMetadata,
47
- persistCredentialMetadata,
48
47
  upsertCredentialMetadata,
49
48
  } from "../../tools/credentials/metadata-store.js";
50
49
  import { getLogger } from "../../util/logger.js";
@@ -157,8 +156,7 @@ export async function reconcileVellumMetadataFromCes(warningSink: {
157
156
  if (getCredentialMetadata("vellum", field)) {
158
157
  continue;
159
158
  }
160
- const metadata = upsertCredentialMetadata("vellum", field, {});
161
- await persistCredentialMetadata(metadata);
159
+ upsertCredentialMetadata("vellum", field, {});
162
160
  log.info(
163
161
  { field },
164
162
  "Reconciled vellum:* metadata entry from CES after import",
@@ -139,10 +139,7 @@ const statusResponseSchema = z.object({
139
139
  async function handleMonitoringStart() {
140
140
  let result: { pid: number; alreadyRunning: boolean };
141
141
  try {
142
- // `detached: false` parents the monitor to the daemon so it appears in
143
- // `assistant ps` and is torn down on shutdown.
144
142
  result = await spawnMonitoringWorkerProcess({
145
- detached: false,
146
143
  terminateOnTimeout: true,
147
144
  });
148
145
  } catch (err) {