@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,8 +1,8 @@
1
1
  /**
2
- * Tests for Slack message_deleted propagation into stored messages.
2
+ * Tests for delete propagation into stored messages.
3
3
  *
4
- * The gateway forwards delete events with `callbackData = "message_deleted"`
5
- * and `sourceMetadata.messageId` set to the deleted message's ts. The daemon
4
+ * The gateway forwards delete events with `eventKind: "delete"` and
5
+ * `sourceMetadata.messageId` set to the deleted message's ts. The daemon
6
6
  * marks the corresponding stored row's `slackMeta.deletedAt` while leaving
7
7
  * the `content` column untouched (audit retention; the renderer elides based
8
8
  * on the deletedAt marker).
@@ -154,6 +154,104 @@ function buildSlackDeleteRequest(opts: {
154
154
  });
155
155
  }
156
156
 
157
+ describe("Discord delete propagation (unattributed)", () => {
158
+ beforeEach(() => {
159
+ resetState();
160
+ _setDeleteLookupConfigForTests(2, 20);
161
+ });
162
+
163
+ test("an unattributed delete applies only to an ingested row, without ACL identity", async () => {
164
+ // Discord's MESSAGE_DELETE names no actor, so the gateway forwards the
165
+ // synthetic discord-system id with actorUnattributed stated. No member
166
+ // exists for that id and no trust verdict rides the event; the delete
167
+ // still applies, because the original's author cleared the ACL when the
168
+ // message arrived and the stamp touches only that ingested row.
169
+ const chatId = "999888777666555444";
170
+ const originalId = "111222333444555001";
171
+ const inbound = recordInbound("discord", chatId, originalId, {
172
+ sourceMessageId: originalId,
173
+ });
174
+ const messageId = `msg-${originalId}`;
175
+ getDb()
176
+ .insert(messages)
177
+ .values({
178
+ id: messageId,
179
+ conversationId: inbound.conversationId,
180
+ role: "user",
181
+ content: "A message someone later deleted",
182
+ metadata: JSON.stringify({ userMessageChannel: "discord" }),
183
+ createdAt: Date.now(),
184
+ })
185
+ .run();
186
+ linkMessage(inbound.eventId, messageId);
187
+
188
+ const req = new Request("http://localhost:8080/channels/inbound", {
189
+ method: "POST",
190
+ headers: {
191
+ "Content-Type": "application/json",
192
+ "X-Gateway-Origin": TEST_BEARER_TOKEN,
193
+ },
194
+ body: JSON.stringify({
195
+ sourceChannel: "discord",
196
+ interface: "discord",
197
+ conversationExternalId: chatId,
198
+ externalMessageId: `${originalId}:delete`,
199
+ eventKind: "delete",
200
+ content: "",
201
+ actorExternalId: "discord-system",
202
+ sourceMetadata: {
203
+ messageId: originalId,
204
+ actorUnattributed: true,
205
+ },
206
+ }),
207
+ });
208
+ const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
209
+ const json = (await resp.json()) as Record<string, unknown>;
210
+
211
+ expect(json.accepted).toBe(true);
212
+ expect(json.deleted).toBe(true);
213
+
214
+ const row = getDb()
215
+ .select()
216
+ .from(messages)
217
+ .where(eq(messages.id, messageId))
218
+ .get();
219
+ expect(row!.content).toBe("A message someone later deleted");
220
+ const neutral = readProviderMetadata(row!.metadata);
221
+ expect(neutral).not.toBeNull();
222
+ expect(neutral!.source).toBe("discord");
223
+ expect(neutral!.deletedAt).toBeDefined();
224
+ });
225
+
226
+ test("an unattributed delete for a never-ingested message is a no-op", async () => {
227
+ const req = new Request("http://localhost:8080/channels/inbound", {
228
+ method: "POST",
229
+ headers: {
230
+ "Content-Type": "application/json",
231
+ "X-Gateway-Origin": TEST_BEARER_TOKEN,
232
+ },
233
+ body: JSON.stringify({
234
+ sourceChannel: "discord",
235
+ interface: "discord",
236
+ conversationExternalId: "999888777666555444",
237
+ externalMessageId: "111222333444555002:delete",
238
+ eventKind: "delete",
239
+ content: "",
240
+ actorExternalId: "discord-system",
241
+ sourceMetadata: {
242
+ messageId: "111222333444555002",
243
+ actorUnattributed: true,
244
+ },
245
+ }),
246
+ });
247
+ const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
248
+ const json = (await resp.json()) as Record<string, unknown>;
249
+
250
+ expect(json.accepted).toBe(true);
251
+ expect(json.deleted).toBe(false);
252
+ });
253
+ });
254
+
157
255
  describe("Slack delete propagation", () => {
158
256
  beforeEach(() => {
159
257
  resetState();
@@ -251,6 +349,26 @@ describe("Slack delete propagation", () => {
251
349
  expect(slackMeta!.deletedAt).toBeUndefined();
252
350
  });
253
351
 
352
+ test("a never-ingested delete returns without paying the retry window", async () => {
353
+ // The retry loop exists for one race: an inbound-event row written
354
+ // before its message link lands. No row at all means nothing can appear
355
+ // by waiting, and the wait would hold the conversation's serialized
356
+ // forward lane for every unrelated delete a busy room produces.
357
+ _setDeleteLookupConfigForTests(2, 500);
358
+ const started = Date.now();
359
+ const req = buildSlackDeleteRequest({
360
+ externalChatId: "C0123CHANNEL",
361
+ deletedTs: "0000.0000",
362
+ });
363
+ const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
364
+ const json = (await resp.json()) as Record<string, unknown>;
365
+
366
+ expect(json.accepted).toBe(true);
367
+ expect(json.deleted).toBe(false);
368
+ // Far under a single 500ms retry delay: the loop short-circuited.
369
+ expect(Date.now() - started).toBeLessThan(400);
370
+ });
371
+
254
372
  test("delete for row without slackMeta stamps the neutral metadata", async () => {
255
373
  // A legacy pre-enrichment row still gets its delete marked: the neutral
256
374
  // envelope is synthesized so readProviderMetadata serves the stamp to
@@ -7,8 +7,9 @@
7
7
  * releasing and its queued successor being dispatched.
8
8
  *
9
9
  * In-flight subagents are the other non-idle case: an async spawn leaves the
10
- * parent idle between its own tool calls while children are still running, and
11
- * reload must not abort those children.
10
+ * parent idle between its own tool calls while children are still running.
11
+ * Reload marks that parent stale without aborting the children, then
12
+ * `getOrCreateConversation` rebuilds it once every child is terminal.
12
13
  */
13
14
  import { beforeEach, describe, expect, mock, test } from "bun:test";
14
15
 
@@ -32,7 +33,10 @@ import {
32
33
  findConversation,
33
34
  setConversation,
34
35
  } from "../daemon/conversation-registry.js";
35
- import { evictConversationsForReload } from "../daemon/conversation-store.js";
36
+ import {
37
+ evictConversationsForReload,
38
+ getOrCreateConversation,
39
+ } from "../daemon/conversation-store.js";
36
40
 
37
41
  interface FakeConversation {
38
42
  disposed: boolean;
@@ -41,7 +45,7 @@ interface FakeConversation {
41
45
 
42
46
  function register(
43
47
  id: string,
44
- state: { processing: boolean; queued: boolean },
48
+ state: { processing: boolean; queued: boolean; stale?: boolean },
45
49
  ): FakeConversation {
46
50
  const fake: FakeConversation & Record<string, unknown> = {
47
51
  disposed: false,
@@ -49,6 +53,9 @@ function register(
49
53
  conversationId: id,
50
54
  isProcessing: () => state.processing,
51
55
  hasQueuedMessages: () => state.queued,
56
+ isStale: () => state.stale === true || fake.markedStale,
57
+ hasInFlightWork: () =>
58
+ state.processing || state.queued || activeParents.has(id),
52
59
  dispose() {
53
60
  fake.disposed = true;
54
61
  },
@@ -103,7 +110,7 @@ describe("evictConversationsForReload", () => {
103
110
  expect(abortedParents).toEqual([]);
104
111
  });
105
112
 
106
- test("skips an idle parent with in-flight subagents", () => {
113
+ test("marks an idle parent with in-flight subagents stale", () => {
107
114
  const parent = register("reload-with-children", {
108
115
  processing: false,
109
116
  queued: false,
@@ -113,7 +120,7 @@ describe("evictConversationsForReload", () => {
113
120
  evictConversationsForReload();
114
121
 
115
122
  expect(parent.disposed).toBe(false);
116
- expect(parent.markedStale).toBe(false);
123
+ expect(parent.markedStale).toBe(true);
117
124
  expect(findConversation("reload-with-children")).toBeDefined();
118
125
  expect(abortedParents).toEqual([]);
119
126
  });
@@ -132,9 +139,42 @@ describe("evictConversationsForReload", () => {
132
139
  evictConversationsForReload();
133
140
 
134
141
  expect(protectedParent.disposed).toBe(false);
142
+ expect(protectedParent.markedStale).toBe(true);
135
143
  expect(findConversation("reload-protected")).toBeDefined();
136
144
  expect(idle.disposed).toBe(true);
137
145
  expect(findConversation("reload-unprotected")).toBeUndefined();
138
146
  expect(abortedParents).toEqual(["reload-unprotected"]);
139
147
  });
148
+
149
+ test("defers stale rebuild while subagents are in flight", async () => {
150
+ const parent = register("stale-parent", {
151
+ processing: false,
152
+ queued: false,
153
+ stale: true,
154
+ });
155
+ activeParents.add("stale-parent");
156
+
157
+ const result = await getOrCreateConversation("stale-parent");
158
+
159
+ expect(result as unknown).toBe(parent);
160
+ expect(parent.disposed).toBe(false);
161
+ expect(abortedParents).toEqual([]);
162
+ });
163
+
164
+ test("rebuilds a stale parent once every child is terminal", async () => {
165
+ const parent = register("stale-parent", {
166
+ processing: false,
167
+ queued: false,
168
+ stale: true,
169
+ });
170
+
171
+ try {
172
+ await getOrCreateConversation("stale-parent");
173
+ } catch {
174
+ // Isolated test has no provider wiring; rebuild intent is abort + dispose.
175
+ }
176
+
177
+ expect(parent.disposed).toBe(true);
178
+ expect(abortedParents).toEqual(["stale-parent"]);
179
+ });
140
180
  });
@@ -10,7 +10,8 @@ import { tmpdir } from "node:os";
10
10
  import { join } from "node:path";
11
11
  import { afterEach, describe, expect, test } from "bun:test";
12
12
 
13
- import { THRESHOLD_CHARS } from "../context/post-turn-tool-result-truncation.js";
13
+ import { FILE_READ_TOOL_NAMES } from "../context/post-turn-tool-result-truncation.js";
14
+ import { RESULT_TIME_SPOOL_EXEMPT_TOOLS } from "../context/tool-result-spool.js";
14
15
  import {
15
16
  FileSystemOps,
16
17
  type PathPolicy,
@@ -330,8 +331,12 @@ describe("FileSystemOps.readFileSafe", () => {
330
331
  expect(body).toBe("abcd");
331
332
  });
332
333
 
333
- test("the read budget stays under the tool-result spool threshold", () => {
334
- expect(READ_CHAR_BUDGET).toBeLessThan(THRESHOLD_CHARS);
334
+ test("file reads are result-time spool-exempt so a full window survives its turn", () => {
335
+ // The budget exceeds the spool threshold, so without the exemption a
336
+ // default read of a large file would be stubbed before the model saw it.
337
+ for (const name of FILE_READ_TOOL_NAMES) {
338
+ expect(RESULT_TIME_SPOOL_EXEMPT_TOOLS.has(name)).toBe(true);
339
+ }
335
340
  });
336
341
  });
337
342
 
@@ -403,6 +403,70 @@ describe("GeminiProvider", () => {
403
403
  });
404
404
  });
405
405
 
406
+ test("3.7 Flash: disabled maps to the LOW floor, not MINIMAL", async () => {
407
+ fakeChunks = [textChunk("OK"), finishChunk("STOP", 10, 2)];
408
+
409
+ const flash37Provider = new GeminiProvider(
410
+ "test-api-key",
411
+ "gemini-3.7-flash",
412
+ );
413
+ await flash37Provider.sendMessage(
414
+ [{ role: "user", content: [{ type: "text", text: "Hi" }] }],
415
+ { config: { thinking: { type: "disabled" } } },
416
+ );
417
+
418
+ const config = lastStreamParams!.config as Record<string, unknown>;
419
+ expect(config.thinkingConfig).toEqual({
420
+ thinkingLevel: "LOW",
421
+ includeThoughts: false,
422
+ });
423
+ });
424
+
425
+ test("3.7 Flash: an explicit minimal level is clamped up to LOW", async () => {
426
+ fakeChunks = [textChunk("OK"), finishChunk("STOP", 10, 2)];
427
+
428
+ const flash37Provider = new GeminiProvider(
429
+ "test-api-key",
430
+ "models/gemini-3.7-flash",
431
+ );
432
+ await flash37Provider.sendMessage(
433
+ [{ role: "user", content: [{ type: "text", text: "Hi" }] }],
434
+ {
435
+ config: {
436
+ thinking: {
437
+ type: "adaptive",
438
+ level: "minimal",
439
+ streamThinking: false,
440
+ },
441
+ },
442
+ },
443
+ );
444
+
445
+ const config = lastStreamParams!.config as Record<string, unknown>;
446
+ expect(config.thinkingConfig).toEqual({
447
+ thinkingLevel: "LOW",
448
+ includeThoughts: false,
449
+ });
450
+ });
451
+
452
+ test("3.7 Flash: adaptive with no level keeps Google's default", async () => {
453
+ fakeChunks = [textChunk("OK"), finishChunk("STOP", 10, 2)];
454
+
455
+ const flash37Provider = new GeminiProvider(
456
+ "test-api-key",
457
+ "gemini-3.7-flash",
458
+ );
459
+ await flash37Provider.sendMessage(
460
+ [{ role: "user", content: [{ type: "text", text: "Hi" }] }],
461
+ { config: { thinking: { type: "adaptive", streamThinking: true } } },
462
+ );
463
+
464
+ const config = lastStreamParams!.config as Record<string, unknown>;
465
+ expect(config.thinkingConfig).toEqual({
466
+ includeThoughts: true,
467
+ });
468
+ });
469
+
406
470
  test("Pro: a supported explicit level passes through unchanged", async () => {
407
471
  fakeChunks = [textChunk("OK"), finishChunk("STOP", 10, 2)];
408
472
 
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Conversation scoping for in-app guardian decisions
3
+ * (`processGuardianDecision` step 2).
4
+ *
5
+ * A guardian request projects an actionable card into every conversation a
6
+ * delivery was paired with: the vellum card's pinned conversation AND each
7
+ * channel card's paired conversation (the decision engine injects the same
8
+ * seed blocks into every channel's copy). Taps from any of those
9
+ * conversations are legitimate; taps from unrelated conversations are not.
10
+ * The scope check must therefore accept any recorded delivery destination
11
+ * conversation regardless of the delivery's channel: narrowing to vellum
12
+ * rows silently no-opped every tap on a channel card's in-app projection
13
+ * (LUM-3489's "one request, two independent tasks" confusion).
14
+ */
15
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
16
+
17
+ import {
18
+ bridgeState,
19
+ gatewayGuardianRequestsStoreBridge,
20
+ } from "./helpers/gateway-guardian-requests-store-bridge.js";
21
+
22
+ mock.module(
23
+ "../channels/gateway-guardian-requests.js",
24
+ () => gatewayGuardianRequestsStoreBridge,
25
+ );
26
+
27
+ const applyGuardianDecision = mock(async () => ({
28
+ applied: true as const,
29
+ requestId: "req",
30
+ grantMinted: false,
31
+ }));
32
+ const actualPrimitive =
33
+ await import("../approvals/guardian-decision-primitive.js");
34
+ mock.module("../approvals/guardian-decision-primitive.js", () => ({
35
+ ...actualPrimitive,
36
+ applyGuardianDecision,
37
+ }));
38
+
39
+ import { processGuardianDecision } from "../runtime/guardian-action-service.js";
40
+
41
+ const PRINCIPAL_ID = "scope-test-principal";
42
+
43
+ function seedRequestWithProjections() {
44
+ const req = bridgeState.seedRequest({
45
+ kind: "tool_approval",
46
+ sourceType: "channel",
47
+ sourceChannel: "slack",
48
+ sourceConversationId: "conv-thread",
49
+ toolName: "shell",
50
+ guardianPrincipalId: PRINCIPAL_ID,
51
+ });
52
+ bridgeState.seedDelivery({
53
+ requestId: req.id,
54
+ destinationChannel: "vellum",
55
+ destinationConversationId: "conv-thread",
56
+ });
57
+ bridgeState.seedDelivery({
58
+ requestId: req.id,
59
+ destinationChannel: "slack",
60
+ destinationChatId: "C1",
61
+ destinationMessageId: "1.0",
62
+ destinationConversationId: "conv-dm",
63
+ });
64
+ return req;
65
+ }
66
+
67
+ async function decideFrom(requestId: string, conversationId: string) {
68
+ return processGuardianDecision({
69
+ requestId,
70
+ action: "approve_once",
71
+ conversationId,
72
+ channel: "vellum",
73
+ actorContext: {
74
+ actorPrincipalId: PRINCIPAL_ID,
75
+ guardianPrincipalId: PRINCIPAL_ID,
76
+ },
77
+ });
78
+ }
79
+
80
+ describe("processGuardianDecision conversation scoping", () => {
81
+ beforeEach(() => {
82
+ bridgeState.reset();
83
+ applyGuardianDecision.mockClear();
84
+ });
85
+
86
+ test("accepts a tap from the request's source conversation", async () => {
87
+ const req = seedRequestWithProjections();
88
+ const result = await decideFrom(req.id, "conv-thread");
89
+ expect(result).toMatchObject({ ok: true, applied: true });
90
+ expect(applyGuardianDecision).toHaveBeenCalledTimes(1);
91
+ });
92
+
93
+ test("accepts a tap from a channel card's paired conversation", async () => {
94
+ const req = seedRequestWithProjections();
95
+ const result = await decideFrom(req.id, "conv-dm");
96
+ expect(result).toMatchObject({ ok: true, applied: true });
97
+ expect(applyGuardianDecision).toHaveBeenCalledTimes(1);
98
+ });
99
+
100
+ test("rejects a tap from a conversation holding no projection", async () => {
101
+ const req = seedRequestWithProjections();
102
+ const result = await decideFrom(req.id, "conv-unrelated");
103
+ expect(result).toMatchObject({
104
+ ok: true,
105
+ applied: false,
106
+ reason: "not_found",
107
+ });
108
+ expect(applyGuardianDecision).not.toHaveBeenCalled();
109
+ });
110
+
111
+ test("threads the acting conversation to the primitive for broadcast suppression", async () => {
112
+ const req = seedRequestWithProjections();
113
+ await decideFrom(req.id, "conv-dm");
114
+ expect(applyGuardianDecision).toHaveBeenCalledWith(
115
+ expect.objectContaining({ originConversationId: "conv-dm" }),
116
+ );
117
+ });
118
+ });