@vellumai/assistant 0.9.1-staging.1 → 0.10.0-staging.2

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 (433) hide show
  1. package/docs/activation-funnel-telemetry.md +24 -18
  2. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +97 -0
  3. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
  4. package/node_modules/@vellumai/gateway-client/src/index.ts +15 -0
  5. package/openapi.yaml +852 -15
  6. package/package.json +1 -1
  7. package/src/__tests__/access-request-card-view.test.ts +98 -0
  8. package/src/__tests__/access-request-seed-content-blocks.test.ts +2 -4
  9. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +59 -7
  10. package/src/__tests__/agent-loop-compaction-strip.test.ts +17 -16
  11. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +16 -13
  12. package/src/__tests__/app-compiler.test.ts +15 -1
  13. package/src/__tests__/auth-fallback-events-store.test.ts +6 -14
  14. package/src/__tests__/call-pointer-messages.test.ts +28 -0
  15. package/src/__tests__/cancel-clears-processing.test.ts +89 -0
  16. package/src/__tests__/channel-approval-routes.test.ts +0 -4
  17. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -15
  18. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +3 -4
  19. package/src/__tests__/compactor-image-manifest-trust.test.ts +21 -1
  20. package/src/__tests__/compactor-summary-call-truncation.test.ts +223 -0
  21. package/src/__tests__/config-loader-backfill.test.ts +174 -30
  22. package/src/__tests__/config-schema.test.ts +35 -0
  23. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -2
  24. package/src/__tests__/contact-store-user-file.test.ts +0 -6
  25. package/src/__tests__/contacts-tools.test.ts +29 -0
  26. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  27. package/src/__tests__/conversation-agent-loop.test.ts +58 -0
  28. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  29. package/src/__tests__/conversation-lifecycle.test.ts +7 -9
  30. package/src/__tests__/conversation-load-history-repair.test.ts +101 -0
  31. package/src/__tests__/conversation-routes-guardian-reply.test.ts +15 -12
  32. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -3
  33. package/src/__tests__/conversation-title-service.test.ts +62 -0
  34. package/src/__tests__/credential-execution-shell-lockdown.test.ts +18 -11
  35. package/src/__tests__/credential-prompt-route.test.ts +1 -0
  36. package/src/__tests__/credential-security-invariants.test.ts +2 -0
  37. package/src/__tests__/disk-pressure-policy.test.ts +12 -0
  38. package/src/__tests__/disk-usage.test.ts +65 -0
  39. package/src/__tests__/dynamic-page-surface.test.ts +51 -0
  40. package/src/__tests__/gateway-flag-listener.test.ts +110 -1
  41. package/src/__tests__/guardian-binding-drift-heal.test.ts +1 -1
  42. package/src/__tests__/guardian-card-withdrawal.test.ts +403 -0
  43. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
  44. package/src/__tests__/guardian-grant-minting.test.ts +3 -35
  45. package/src/__tests__/guardian-routing-invariants.test.ts +64 -26
  46. package/src/__tests__/guardian-routing-state.test.ts +0 -1
  47. package/src/__tests__/headless-browser-mode.test.ts +10 -0
  48. package/src/__tests__/headless-browser-navigate.test.ts +8 -3
  49. package/src/__tests__/helpers/create-guardian-binding.ts +0 -1
  50. package/src/__tests__/host-browser-proxy.test.ts +87 -0
  51. package/src/__tests__/injector-v3-suppression.test.ts +27 -20
  52. package/src/__tests__/internal-telemetry-routes.test.ts +6 -14
  53. package/src/__tests__/invite-redemption-service.test.ts +0 -3
  54. package/src/__tests__/llm-catalog-parity.test.ts +30 -1
  55. package/src/__tests__/llm-resolver.test.ts +21 -0
  56. package/src/__tests__/llm-schema.test.ts +1 -0
  57. package/src/__tests__/managed-profile-guard.test.ts +163 -4
  58. package/src/__tests__/mcp-health-check.test.ts +6 -7
  59. package/src/__tests__/media-stream-server-integration.test.ts +317 -13
  60. package/src/__tests__/path-policy.test.ts +34 -0
  61. package/src/__tests__/persona-resolver.test.ts +38 -0
  62. package/src/__tests__/plugin-api-provider.test.ts +24 -0
  63. package/src/__tests__/plugin-tool-contribution.test.ts +6 -3
  64. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +214 -0
  65. package/src/__tests__/provider-send-message-override-profile.test.ts +76 -0
  66. package/src/__tests__/reaction-persistence.test.ts +150 -29
  67. package/src/__tests__/relay-server.test.ts +285 -0
  68. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  69. package/src/__tests__/scheduler-reuse-conversation.test.ts +8 -5
  70. package/src/__tests__/skill-execute-input.test.ts +5 -0
  71. package/src/__tests__/skills.test.ts +51 -0
  72. package/src/__tests__/slack-notification-approval-card.test.ts +176 -0
  73. package/src/__tests__/slack-reaction-canonical-approval.test.ts +285 -0
  74. package/src/__tests__/subagent-tools.test.ts +150 -0
  75. package/src/__tests__/task-progress-nudge-hook.test.ts +1 -1
  76. package/src/__tests__/title-generate-hook.test.ts +100 -3
  77. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +1 -1
  78. package/src/__tests__/tool-audit-listener.test.ts +7 -7
  79. package/src/__tests__/tool-executor-lifecycle-events.test.ts +6 -3
  80. package/src/__tests__/trusted-contact-approval-notifier.test.ts +4 -2
  81. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +220 -3
  82. package/src/__tests__/trusted-contact-verification.test.ts +2 -4
  83. package/src/__tests__/twilio-routes.test.ts +81 -1
  84. package/src/__tests__/voice-invite-redemption.test.ts +0 -1
  85. package/src/__tests__/weak-open-model.test.ts +30 -0
  86. package/src/__tests__/workspace-migration-105-enable-memory-v3-live-for-new-workspaces.test.ts +149 -0
  87. package/src/__tests__/workspace-migration-108-drop-balanced-economy-profile.test.ts +285 -0
  88. package/src/__tests__/workspace-migration-add-send-diagnostics.test.ts +1 -1
  89. package/src/__tests__/workspace-migration-drop-collect-usage-data.test.ts +118 -0
  90. package/src/__tests__/workspace-migration-drop-send-diagnostics.test.ts +118 -0
  91. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +0 -4
  92. package/src/agent/loop.ts +33 -33
  93. package/src/api/events/tool-result.ts +6 -0
  94. package/src/api/events/workflow-completed.ts +53 -0
  95. package/src/api/events/workflow-leaf-finished.ts +38 -0
  96. package/src/api/events/workflow-leaf-started.ts +35 -0
  97. package/src/api/events/workflow-progress.ts +32 -0
  98. package/src/api/events/workflow-started.ts +31 -0
  99. package/src/api/index.ts +40 -0
  100. package/src/api/responses/conversation-message.ts +26 -0
  101. package/src/api/responses/home.ts +26 -0
  102. package/src/api/responses/workflow-journal.ts +53 -0
  103. package/src/approvals/guardian-card-withdrawal.ts +145 -0
  104. package/src/approvals/guardian-decision-primitive.ts +26 -3
  105. package/src/approvals/guardian-request-resolvers.ts +181 -78
  106. package/src/calls/__tests__/channel-admission-reader.test.ts +132 -0
  107. package/src/calls/__tests__/relay-setup-router.test.ts +350 -0
  108. package/src/calls/call-pointer-messages.ts +10 -4
  109. package/src/calls/channel-admission-reader.ts +104 -0
  110. package/src/calls/guardian-dispatch.ts +17 -45
  111. package/src/calls/media-stream-server.ts +84 -2
  112. package/src/calls/relay-server.ts +66 -0
  113. package/src/calls/relay-setup-router.ts +82 -1
  114. package/src/calls/twilio-routes.ts +17 -8
  115. package/src/cli/commands/clients.ts +3 -0
  116. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2-compare-render.test.ts +1 -1
  117. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2.test.ts +8 -7
  118. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v3.test.ts +5 -4
  119. package/src/cli/commands/memory/index.ts +30 -0
  120. package/src/cli/commands/{memory-v2-compare-render.ts → memory/memory-v2-compare-render.ts} +1 -1
  121. package/src/cli/commands/{memory-v2.ts → memory/memory-v2.ts} +6 -15
  122. package/src/cli/commands/{memory-v3.ts → memory/memory-v3.ts} +97 -11
  123. package/src/cli/commands/oauth/status.test.ts +36 -0
  124. package/src/cli/commands/oauth/status.ts +23 -3
  125. package/src/cli/commands/plugins.ts +57 -5
  126. package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
  127. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +134 -4
  128. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +111 -0
  129. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +53 -11
  130. package/src/cli/lib/inspect-plugin.ts +12 -1
  131. package/src/cli/lib/merge-plugin-tree.ts +149 -49
  132. package/src/cli/lib/plugin-surfaces.ts +104 -0
  133. package/src/cli/lib/upgrade-plugin.ts +64 -36
  134. package/src/cli/program.ts +2 -4
  135. package/src/config/__tests__/sync-gated-profiles.test.ts +368 -0
  136. package/src/config/assistant-feature-flags.ts +22 -7
  137. package/src/config/bundled-skills/contacts/tools/contact-search.ts +0 -1
  138. package/src/config/bundled-skills/messaging/SKILL.md +6 -4
  139. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +9 -8
  140. package/src/config/bundled-skills/workflows/SKILL.md +14 -7
  141. package/src/config/call-site-defaults.ts +3 -0
  142. package/src/config/feature-flag-registry.json +49 -18
  143. package/src/config/llm-resolver.ts +3 -0
  144. package/src/config/memory-v3-gate.ts +11 -0
  145. package/src/config/schema.ts +8 -6
  146. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  147. package/src/config/schemas/call-site-catalog.ts +7 -0
  148. package/src/config/schemas/channels.ts +11 -0
  149. package/src/config/schemas/llm.ts +31 -0
  150. package/src/config/schemas/memory-lifecycle.ts +3 -7
  151. package/src/config/schemas/memory-v3.ts +6 -0
  152. package/src/config/schemas/services.ts +18 -0
  153. package/src/config/seed-inference-profiles.ts +94 -34
  154. package/src/config/skills.ts +21 -0
  155. package/src/config/sync-gated-profiles.ts +220 -0
  156. package/src/contacts/contact-store.ts +2 -10
  157. package/src/contacts/contacts-write.ts +1 -2
  158. package/src/contacts/types.ts +0 -1
  159. package/src/context/compactor.ts +86 -52
  160. package/src/context/strip-injections.ts +58 -10
  161. package/src/context/token-estimator.ts +1 -1
  162. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -2
  163. package/src/daemon/conversation-agent-loop-handlers.ts +2 -0
  164. package/src/daemon/conversation-agent-loop.ts +100 -19
  165. package/src/daemon/conversation-history.ts +1 -1
  166. package/src/daemon/conversation-lifecycle.ts +3 -5
  167. package/src/daemon/conversation-process.ts +13 -5
  168. package/src/daemon/conversation-runtime-assembly.ts +13 -15
  169. package/src/daemon/conversation-surfaces.ts +26 -0
  170. package/src/daemon/conversation-tool-setup.ts +16 -11
  171. package/src/daemon/conversation.ts +64 -14
  172. package/src/daemon/disk-pressure-policy.ts +5 -3
  173. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -1
  174. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -1
  175. package/src/daemon/handlers/config-a2a.ts +0 -2
  176. package/src/daemon/handlers/config-channels.ts +5 -10
  177. package/src/daemon/handlers/config-slack-channel.ts +20 -0
  178. package/src/daemon/handlers/conversations.ts +107 -0
  179. package/src/daemon/host-browser-proxy.ts +41 -0
  180. package/src/daemon/lifecycle.ts +55 -20
  181. package/src/daemon/message-provenance.ts +2 -0
  182. package/src/daemon/message-types/contacts.ts +0 -1
  183. package/src/daemon/message-types/web-activity.ts +7 -1
  184. package/src/daemon/message-types/workflows.ts +83 -1
  185. package/src/daemon/tool-setup-types.ts +4 -0
  186. package/src/daemon/trust-context.ts +1 -1
  187. package/src/events/tool-audit-listener.ts +2 -2
  188. package/src/home/feed-source-enrichment.test.ts +151 -0
  189. package/src/home/feed-source-enrichment.ts +176 -0
  190. package/src/instrument.ts +18 -6
  191. package/src/ipc/__tests__/binary-result-ipc.test.ts +81 -0
  192. package/src/ipc/__tests__/clients-list-ipc.test.ts +20 -0
  193. package/src/ipc/assistant-server.ts +37 -4
  194. package/src/ipc/gateway-flag-listener.ts +18 -2
  195. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +5 -16
  196. package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +7 -11
  197. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +37 -7
  198. package/src/memory/__tests__/memory-retrospective-job.test.ts +34 -0
  199. package/src/memory/__tests__/onboarding-events-store.test.ts +7 -7
  200. package/src/memory/auth-fallback-events-store.ts +2 -2
  201. package/src/memory/auto-analysis-enqueue.ts +3 -5
  202. package/src/memory/canonical-guardian-store.ts +39 -1
  203. package/src/memory/conversation-crud.ts +9 -4
  204. package/src/memory/conversation-key-store.ts +17 -2
  205. package/src/memory/conversation-title-service.ts +64 -7
  206. package/src/memory/db-init.ts +10 -0
  207. package/src/memory/embedding-backend.ts +15 -1
  208. package/src/memory/jobs-worker.ts +2 -1
  209. package/src/memory/lifecycle-events-store.ts +2 -2
  210. package/src/memory/memory-retrospective-enqueue.ts +31 -6
  211. package/src/memory/memory-retrospective-job.ts +9 -0
  212. package/src/memory/migrations/129-contact-channels-access-fields.ts +18 -9
  213. package/src/memory/migrations/131-drop-legacy-member-guardian-tables.ts +14 -2
  214. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +10 -0
  215. package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +10 -0
  216. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.test.ts +67 -0
  217. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.ts +25 -0
  218. package/src/memory/migrations/293-workflow-journal-leaf-tokens.ts +32 -0
  219. package/src/memory/migrations/294-drop-external-user-id.ts +31 -0
  220. package/src/memory/migrations/295-drop-approval-prompt-ts-tracker.ts +20 -0
  221. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.test.ts +110 -0
  222. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.ts +68 -0
  223. package/src/memory/migrations/__tests__/131-drop-legacy-member-guardian-tables.test.ts +154 -0
  224. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +31 -0
  225. package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +30 -0
  226. package/src/memory/migrations/index.ts +5 -0
  227. package/src/memory/onboarding-events-store.ts +3 -3
  228. package/src/memory/schema/contacts.ts +0 -1
  229. package/src/memory/skill-loaded-events-store.test.ts +7 -15
  230. package/src/memory/skill-loaded-events-store.ts +2 -2
  231. package/src/memory/tool-executed-events-store.test.ts +7 -7
  232. package/src/memory/turn-trace-store.test.ts +736 -0
  233. package/src/memory/turn-trace-store.ts +364 -0
  234. package/src/memory/v2/__tests__/consolidation-job.test.ts +8 -0
  235. package/src/memory/v2/__tests__/skill-content.test.ts +30 -0
  236. package/src/memory/v2/consolidation-job.ts +2 -2
  237. package/src/memory/v2/skill-content.ts +25 -7
  238. package/src/memory/v2/skill-store.ts +7 -1
  239. package/src/memory/v3-eval/__tests__/eval-packets.test.ts +248 -0
  240. package/src/memory/v3-eval/eval-packets.ts +546 -0
  241. package/src/messaging/providers/slack/api.ts +31 -0
  242. package/src/messaging/providers/slack/send.test.ts +114 -2
  243. package/src/messaging/providers/slack/send.ts +30 -7
  244. package/src/messaging/providers/slack/withdraw.test.ts +200 -0
  245. package/src/messaging/providers/slack/withdraw.ts +161 -0
  246. package/src/notifications/AGENTS.md +2 -0
  247. package/src/notifications/access-request-copy.ts +72 -59
  248. package/src/notifications/adapters/slack.ts +55 -73
  249. package/src/notifications/approval-card-data.ts +333 -0
  250. package/src/notifications/broadcaster.ts +6 -2
  251. package/src/notifications/canonical-delivery-recorder.ts +139 -0
  252. package/src/notifications/copy-composer.ts +3 -3
  253. package/src/notifications/decision-engine.ts +4 -2
  254. package/src/notifications/destination-resolver.ts +4 -6
  255. package/src/notifications/guardian-question-mode.ts +10 -0
  256. package/src/notifications/home-feed-side-effect.ts +3 -13
  257. package/src/notifications/notification-utils.ts +2 -1
  258. package/src/notifications/signal.ts +79 -43
  259. package/src/notifications/types.ts +98 -128
  260. package/src/permissions/checker.test.ts +51 -0
  261. package/src/permissions/checker.ts +185 -26
  262. package/src/permissions/ipc-risk-types.ts +24 -0
  263. package/src/permissions/question-prompter.test.ts +27 -0
  264. package/src/permissions/question-prompter.ts +4 -0
  265. package/src/platform/client.test.ts +119 -0
  266. package/src/platform/client.ts +66 -0
  267. package/src/platform/consent-cache.test.ts +267 -0
  268. package/src/platform/consent-cache.ts +174 -0
  269. package/src/plugin-api/index.ts +27 -0
  270. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +56 -0
  271. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +43 -0
  272. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +137 -0
  273. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +153 -0
  274. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +138 -0
  275. package/src/plugins/defaults/advisor/__tests__/transcript.test.ts +147 -0
  276. package/src/plugins/defaults/advisor/advisor-gate.ts +29 -0
  277. package/src/plugins/defaults/advisor/advisor-state-store.ts +94 -0
  278. package/src/plugins/defaults/advisor/config.ts +21 -0
  279. package/src/plugins/defaults/advisor/consult.ts +93 -0
  280. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +34 -0
  281. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +30 -0
  282. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +19 -0
  283. package/src/plugins/defaults/advisor/package.json +14 -0
  284. package/src/plugins/defaults/advisor/steering.ts +67 -0
  285. package/src/plugins/defaults/advisor/tools/advisor.ts +65 -0
  286. package/src/plugins/defaults/advisor/transcript.ts +76 -0
  287. package/src/plugins/defaults/index.ts +35 -0
  288. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +22 -9
  289. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
  290. package/src/plugins/defaults/memory-retrieval/tail-reinjection-strip.ts +64 -0
  291. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +29 -21
  292. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1 -0
  293. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  294. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +75 -7
  295. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +31 -4
  296. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +77 -2
  297. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +1 -0
  298. package/src/plugins/defaults/memory-v3-shadow/injector.ts +7 -10
  299. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +37 -4
  300. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +32 -20
  301. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +56 -3
  302. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +23 -2
  303. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +2 -12
  304. package/src/plugins/defaults/title-generate/hooks/stop.ts +56 -21
  305. package/src/prompts/persona-resolver.ts +12 -2
  306. package/src/prompts/templates/system-sections.ts +7 -2
  307. package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
  308. package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
  309. package/src/providers/__tests__/retry-callsite.test.ts +176 -0
  310. package/src/providers/atlascloud/client.ts +85 -0
  311. package/src/providers/fetch-provider-catalog.ts +85 -0
  312. package/src/providers/inference/adapter-factory.ts +3 -0
  313. package/src/providers/model-catalog.ts +58 -0
  314. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +33 -0
  315. package/src/providers/openai/chat-completions-provider.ts +7 -0
  316. package/src/providers/openai/responses-provider.ts +10 -0
  317. package/src/providers/provider-send-message.ts +11 -3
  318. package/src/providers/retry.ts +53 -12
  319. package/src/providers/search-provider-catalog.ts +10 -0
  320. package/src/providers/weak-open-model.ts +22 -0
  321. package/src/runtime/__tests__/agent-wake.test.ts +181 -0
  322. package/src/runtime/__tests__/client-health.test.ts +44 -0
  323. package/src/runtime/access-request-helper.ts +21 -53
  324. package/src/runtime/actor-trust-resolver.ts +49 -21
  325. package/src/runtime/agent-wake.ts +52 -0
  326. package/src/runtime/assistant-event-hub.ts +18 -4
  327. package/src/runtime/auth/__tests__/route-policy.test.ts +12 -0
  328. package/src/runtime/auth/require-bound-guardian.ts +1 -4
  329. package/src/runtime/capabilities.test.ts +120 -0
  330. package/src/runtime/capabilities.ts +197 -0
  331. package/src/runtime/channel-approval-types.ts +5 -1
  332. package/src/runtime/channel-retry-sweep.ts +1 -0
  333. package/src/runtime/channel-verification-service.ts +1 -2
  334. package/src/runtime/client-health.ts +26 -0
  335. package/src/runtime/confirmation-request-guardian-bridge.ts +38 -29
  336. package/src/runtime/effective-capabilities.test.ts +128 -0
  337. package/src/runtime/effective-capabilities.ts +84 -0
  338. package/src/runtime/guardian-reply-router.ts +106 -21
  339. package/src/runtime/invite-redemption-service.ts +6 -22
  340. package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +123 -0
  341. package/src/runtime/migrations/vbundle-builder.ts +49 -20
  342. package/src/runtime/pending-interactions.ts +15 -0
  343. package/src/runtime/routes/__tests__/client-routes.test.ts +13 -0
  344. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +67 -0
  345. package/src/runtime/routes/__tests__/plugins-routes.test.ts +35 -13
  346. package/src/runtime/routes/browser-tabs-routes.ts +9 -0
  347. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +17 -8
  348. package/src/runtime/routes/client-routes.ts +10 -0
  349. package/src/runtime/routes/contact-routes.ts +31 -8
  350. package/src/runtime/routes/conversation-management-routes.ts +80 -1
  351. package/src/runtime/routes/conversation-query-routes.ts +68 -22
  352. package/src/runtime/routes/conversation-routes.ts +37 -12
  353. package/src/runtime/routes/events-routes.ts +1 -3
  354. package/src/runtime/routes/guardian-approval-interception.ts +14 -73
  355. package/src/runtime/routes/guardian-approval-prompt.ts +22 -4
  356. package/src/runtime/routes/home-feed-routes.ts +8 -3
  357. package/src/runtime/routes/inbound-message-handler.ts +214 -228
  358. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +88 -6
  359. package/src/runtime/routes/inbound-stages/admission-policy.test.ts +154 -0
  360. package/src/runtime/routes/inbound-stages/admission-policy.ts +140 -0
  361. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +3 -3
  362. package/src/runtime/routes/inbound-stages/background-dispatch.ts +11 -6
  363. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +1 -2
  364. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +1 -2
  365. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +7 -7
  366. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +47 -28
  367. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +358 -0
  368. package/src/runtime/routes/index.ts +2 -0
  369. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +8 -0
  370. package/src/runtime/routes/integrations/slack/channel.ts +36 -0
  371. package/src/runtime/routes/internal-telemetry-routes.ts +1 -1
  372. package/src/runtime/routes/mcp-auth-routes.ts +233 -41
  373. package/src/runtime/routes/memory-eval-routes.ts +87 -0
  374. package/src/runtime/routes/notification-routes.ts +122 -133
  375. package/src/runtime/routes/platform-routes.ts +2 -2
  376. package/src/runtime/routes/plugins-routes.ts +40 -7
  377. package/src/runtime/routes/secret-routes.ts +10 -0
  378. package/src/runtime/routes/surface-action-routes.ts +2 -1
  379. package/src/runtime/routes/tool-call-question-enrichment.test.ts +146 -0
  380. package/src/runtime/routes/tool-call-question-enrichment.ts +66 -0
  381. package/src/runtime/routes/workflow-routes.test.ts +225 -1
  382. package/src/runtime/routes/workflow-routes.ts +131 -1
  383. package/src/runtime/tool-grant-request-helper.ts +18 -16
  384. package/src/runtime/trust-context-resolver.ts +8 -5
  385. package/src/schedule/schedule-store.ts +1 -1
  386. package/src/schedule/scheduler-types.ts +5 -1
  387. package/src/security/__tests__/provider-key-env-fallback.test.ts +6 -0
  388. package/src/security/secret-patterns.ts +3 -0
  389. package/src/subagent/manager.ts +11 -4
  390. package/src/telemetry/trace-collection-policy.test.ts +28 -0
  391. package/src/telemetry/trace-collection-policy.ts +30 -0
  392. package/src/telemetry/types.ts +89 -0
  393. package/src/telemetry/usage-telemetry-reporter.test.ts +586 -36
  394. package/src/telemetry/usage-telemetry-reporter.ts +148 -41
  395. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +31 -0
  396. package/src/tools/browser/browser-execution.ts +29 -18
  397. package/src/tools/document/document-tool.ts +2 -3
  398. package/src/tools/executor.ts +5 -3
  399. package/src/tools/host-terminal/host-shell.ts +5 -4
  400. package/src/tools/memory/register.ts +2 -2
  401. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +360 -0
  402. package/src/tools/network/__tests__/web-search.test.ts +143 -0
  403. package/src/tools/network/web-fetch.ts +372 -1
  404. package/src/tools/network/web-search.ts +213 -10
  405. package/src/tools/permission-checker.ts +3 -2
  406. package/src/tools/registry.ts +20 -0
  407. package/src/tools/schedule/create.ts +4 -3
  408. package/src/tools/schedule/update.ts +2 -1
  409. package/src/tools/shared/filesystem/path-policy.ts +39 -13
  410. package/src/tools/skills/execute.ts +1 -2
  411. package/src/tools/subagent/spawn.ts +37 -13
  412. package/src/tools/terminal/shell.ts +10 -4
  413. package/src/tools/tool-approval-handler.ts +17 -10
  414. package/src/tools/types.ts +9 -0
  415. package/src/tools/ui-surface/definitions.ts +25 -2
  416. package/src/tools/verification-control-plane-policy.ts +3 -1
  417. package/src/tools/workflows/run-workflow.ts +1 -0
  418. package/src/util/disk-usage.ts +78 -23
  419. package/src/util/platform.ts +8 -1
  420. package/src/watcher/telemetry.ts +2 -2
  421. package/src/workflows/engine.test.ts +175 -1
  422. package/src/workflows/engine.ts +82 -0
  423. package/src/workflows/journal-store.test.ts +70 -0
  424. package/src/workflows/journal-store.ts +18 -3
  425. package/src/workflows/run-manager.test.ts +171 -3
  426. package/src/workflows/run-manager.ts +64 -0
  427. package/src/workspace/migrations/105-enable-memory-v3-live-for-new-workspaces.ts +63 -0
  428. package/src/workspace/migrations/106-drop-collect-usage-data.ts +47 -0
  429. package/src/workspace/migrations/107-drop-send-diagnostics.ts +47 -0
  430. package/src/workspace/migrations/108-drop-balanced-economy-profile.ts +129 -0
  431. package/src/workspace/migrations/registry.ts +8 -0
  432. package/src/notifications/tool-approval-copy.ts +0 -142
  433. package/src/runtime/routes/approval-prompt-ts-tracker.ts +0 -78
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Post-compaction re-injection idempotency.
3
+ *
4
+ * The agent loop hands the post-compaction hook the full injected continuation
5
+ * history (it does not pre-strip it), so the hook must clear the tail's stale
6
+ * per-turn injection blocks before re-applying them. Without that strip,
7
+ * `applyRuntimeInjections` double-stacks every non-presence-gated block —
8
+ * `<turn_context>`, `<config_reset_notice>`, `<active_documents>`,
9
+ * `<document_comments>` — because it appends to the tail without removing an
10
+ * existing copy. These tests cover the strip primitive directly and the
11
+ * end-to-end re-injection it protects.
12
+ */
13
+ import { afterEach, describe, expect, test } from "bun:test";
14
+
15
+ import {
16
+ clearConversations,
17
+ setConversation,
18
+ } from "../daemon/conversation-registry.js";
19
+ import { applyRuntimeInjections } from "../daemon/conversation-runtime-assembly.js";
20
+ import { stripTailInjectionsForReinjection } from "../plugins/defaults/memory-retrieval/tail-reinjection-strip.js";
21
+ import type { Message } from "../providers/types.js";
22
+
23
+ const WORKSPACE_BLOCK = "<workspace>\nRoot: /sandbox\n</workspace>";
24
+ const INFO_BLOCK = "<info>\nRemembered fact about project-x\n</info>";
25
+ const TURN_CONTEXT_BLOCK =
26
+ "<turn_context>\ncurrent_time: noon\n</turn_context>";
27
+ const CONFIG_RESET_BLOCK =
28
+ "<config_reset_notice>\nSettings were reset.\n</config_reset_notice>";
29
+ const ACTIVE_DOCUMENTS_BLOCK =
30
+ "<active_documents>\nThe following documents are open: notes.md\n</active_documents>";
31
+ const DOCUMENT_COMMENTS_BLOCK =
32
+ "<document_comments>\nOpen comments on notes.md\n</document_comments>";
33
+ const TURN_BODY = "Please continue the task.";
34
+
35
+ function userMsg(...texts: string[]): Message {
36
+ return {
37
+ role: "user",
38
+ content: texts.map((text) => ({ type: "text" as const, text })),
39
+ };
40
+ }
41
+
42
+ function assistantMsg(text: string): Message {
43
+ return { role: "assistant", content: [{ type: "text", text }] };
44
+ }
45
+
46
+ function tailContentTexts(messages: Message[]): string[] {
47
+ const last = messages[messages.length - 1];
48
+ return last.content.map((block) => (block.type === "text" ? block.text : ""));
49
+ }
50
+
51
+ function countTailBlocksWithPrefix(
52
+ messages: Message[],
53
+ prefix: string,
54
+ ): number {
55
+ return tailContentTexts(messages).filter((text) => text.startsWith(prefix))
56
+ .length;
57
+ }
58
+
59
+ describe("stripTailInjectionsForReinjection", () => {
60
+ test("clears every per-turn injection block from the tail user message", () => {
61
+ // GIVEN a tail user message carrying the real turn body plus the full
62
+ // per-turn injection set, including the four blocks compaction keeps in
63
+ // durable history
64
+ const messages: Message[] = [
65
+ userMsg(
66
+ TURN_BODY,
67
+ WORKSPACE_BLOCK,
68
+ INFO_BLOCK,
69
+ TURN_CONTEXT_BLOCK,
70
+ CONFIG_RESET_BLOCK,
71
+ ACTIVE_DOCUMENTS_BLOCK,
72
+ DOCUMENT_COMMENTS_BLOCK,
73
+ ),
74
+ ];
75
+
76
+ // WHEN the tail injections are stripped for re-injection
77
+ const result = stripTailInjectionsForReinjection(messages);
78
+
79
+ // THEN only the real turn body survives on the tail
80
+ expect(tailContentTexts(result)).toEqual([TURN_BODY]);
81
+ });
82
+
83
+ test("leaves injection blocks on earlier messages untouched", () => {
84
+ // GIVEN an earlier user message and the tail both carrying a turn-context
85
+ // block
86
+ const messages: Message[] = [
87
+ userMsg("Earlier turn", TURN_CONTEXT_BLOCK),
88
+ assistantMsg("Working on it."),
89
+ userMsg(TURN_BODY, TURN_CONTEXT_BLOCK),
90
+ ];
91
+
92
+ // WHEN the tail injections are stripped
93
+ const result = stripTailInjectionsForReinjection(messages);
94
+
95
+ // THEN the earlier message keeps its historical turn-context grounding
96
+ expect(result[0].content).toHaveLength(2);
97
+ expect((result[0].content[1] as { text: string }).text).toBe(
98
+ TURN_CONTEXT_BLOCK,
99
+ );
100
+ // AND only the tail is cleared
101
+ expect(tailContentTexts(result)).toEqual([TURN_BODY]);
102
+ });
103
+
104
+ test("returns the messages unchanged when the tail carries no injections", () => {
105
+ // GIVEN a tail with only real user content
106
+ const messages: Message[] = [userMsg(TURN_BODY)];
107
+
108
+ // WHEN the tail injections are stripped
109
+ const result = stripTailInjectionsForReinjection(messages);
110
+
111
+ // THEN the array is returned unchanged
112
+ expect(result).toBe(messages);
113
+ });
114
+
115
+ test("preserves the tail user message even when every block is stripped", () => {
116
+ // GIVEN a tail composed entirely of injection blocks
117
+ const messages: Message[] = [
118
+ assistantMsg("Earlier reply."),
119
+ userMsg(WORKSPACE_BLOCK, TURN_CONTEXT_BLOCK),
120
+ ];
121
+
122
+ // WHEN the tail injections are stripped
123
+ const result = stripTailInjectionsForReinjection(messages);
124
+
125
+ // THEN the tail user message remains so the re-injection tail invariant
126
+ // holds, just with empty content
127
+ expect(result).toHaveLength(2);
128
+ expect(result[1].role).toBe("user");
129
+ expect(result[1].content).toHaveLength(0);
130
+ });
131
+
132
+ test("is idempotent — stripping an already-stripped tail is a no-op", () => {
133
+ // GIVEN a tail that has already been stripped once
134
+ const messages: Message[] = [userMsg(TURN_BODY, TURN_CONTEXT_BLOCK)];
135
+ const once = stripTailInjectionsForReinjection(messages);
136
+
137
+ // WHEN it is stripped again
138
+ const twice = stripTailInjectionsForReinjection(once);
139
+
140
+ // THEN the second strip changes nothing
141
+ expect(twice).toBe(once);
142
+ expect(tailContentTexts(twice)).toEqual([TURN_BODY]);
143
+ });
144
+ });
145
+
146
+ describe("applyRuntimeInjections re-injection idempotency", () => {
147
+ // `applyRuntimeInjections` synthesizes this conversation id when no
148
+ // turnContext is supplied, so the injectors resolve their blocks from the
149
+ // registry under this key.
150
+ const FALLBACK_CONVERSATION_ID = "runtime-assembly-fallback";
151
+
152
+ // Seed the fallback conversation with a workspace block (presence-gated) and
153
+ // a frozen temporal snapshot so the non-presence-gated `<turn_context>` block
154
+ // is produced every assembly.
155
+ function seedConversation(): void {
156
+ setConversation(FALLBACK_CONVERSATION_ID, {
157
+ conversationId: FALLBACK_CONVERSATION_ID,
158
+ workingDir: "/sandbox",
159
+ workspaceTopLevelContext: WORKSPACE_BLOCK,
160
+ workspaceTopLevelDirty: false,
161
+ currentTurnTemporalSnapshot: { clientTimezone: null },
162
+ } as never);
163
+ }
164
+
165
+ afterEach(() => {
166
+ clearConversations();
167
+ });
168
+
169
+ test("re-injecting an already-injected base without the strip double-stacks the non-presence-gated block", async () => {
170
+ // GIVEN a fresh turn assembled once, so its tail carries one workspace and
171
+ // one turn-context block
172
+ seedConversation();
173
+ const { messages: injectedOnce } = await applyRuntimeInjections(
174
+ [userMsg(TURN_BODY)],
175
+ { conversationId: FALLBACK_CONVERSATION_ID },
176
+ );
177
+ expect(countTailBlocksWithPrefix(injectedOnce, "<turn_context>")).toBe(1);
178
+
179
+ // WHEN injections are applied again to that already-injected base
180
+ const { messages: injectedTwice } = await applyRuntimeInjections(
181
+ injectedOnce,
182
+ { conversationId: FALLBACK_CONVERSATION_ID },
183
+ );
184
+
185
+ // THEN the non-presence-gated turn-context block double-stacks
186
+ expect(countTailBlocksWithPrefix(injectedTwice, "<turn_context>")).toBe(2);
187
+ // AND the presence-gated workspace block stays single
188
+ expect(countTailBlocksWithPrefix(injectedTwice, "<workspace>")).toBe(1);
189
+ });
190
+
191
+ test("stripping the tail before re-injecting keeps every block single", async () => {
192
+ // GIVEN a fresh turn assembled once
193
+ seedConversation();
194
+ const { messages: injectedOnce } = await applyRuntimeInjections(
195
+ [userMsg(TURN_BODY)],
196
+ { conversationId: FALLBACK_CONVERSATION_ID },
197
+ );
198
+
199
+ // WHEN the tail is stripped before re-injection, exactly as the
200
+ // post-compaction hook does
201
+ const { messages: reinjected } = await applyRuntimeInjections(
202
+ stripTailInjectionsForReinjection(injectedOnce),
203
+ { conversationId: FALLBACK_CONVERSATION_ID },
204
+ );
205
+
206
+ // THEN both the non-presence-gated and presence-gated blocks stay single
207
+ expect(countTailBlocksWithPrefix(reinjected, "<turn_context>")).toBe(1);
208
+ expect(countTailBlocksWithPrefix(reinjected, "<workspace>")).toBe(1);
209
+ // AND the real turn body survives
210
+ expect(
211
+ tailContentTexts(reinjected).some((text) => text === TURN_BODY),
212
+ ).toBe(true);
213
+ });
214
+ });
@@ -261,3 +261,79 @@ describe("SendMessageOptions.config.overrideProfile", () => {
261
261
  expect(captured?.overrideProfile).toBeUndefined();
262
262
  });
263
263
  });
264
+
265
+ describe("SendMessageOptions.config.forceOverrideProfile", () => {
266
+ test("CallSiteConfiguredProvider forwards forceOverrideProfile into the send config", async () => {
267
+ let captured: SendMessageOptions | undefined;
268
+ const inner: Provider = {
269
+ name: "anthropic",
270
+ async sendMessage(
271
+ _messages: Message[],
272
+ options?: SendMessageOptions,
273
+ ): Promise<ProviderResponse> {
274
+ captured = options;
275
+ return makeResponse("anthropic");
276
+ },
277
+ };
278
+
279
+ const provider = new CallSiteConfiguredProvider(
280
+ inner,
281
+ "inference",
282
+ "strong",
283
+ true,
284
+ );
285
+ await provider.sendMessage(DUMMY_MESSAGES, {});
286
+
287
+ expect(captured?.config).toMatchObject({
288
+ callSite: "inference",
289
+ overrideProfile: "strong",
290
+ forceOverrideProfile: true,
291
+ });
292
+ });
293
+
294
+ test("forceOverrideProfile floats the override above a call-site profile pin", async () => {
295
+ // The advisor scenario in miniature: the `inference` call site is pinned to
296
+ // a cheap profile, but a caller forces a stronger profile for its own send.
297
+ setLlmConfig({
298
+ default: { provider: "anthropic", model: "claude-opus-4-7" },
299
+ profiles: {
300
+ cheap: { provider: "anthropic", model: "claude-haiku-4-5-20251001" },
301
+ strong: { provider: "anthropic", model: "claude-opus-4-8" },
302
+ },
303
+ callSites: { inference: { profile: "cheap" } },
304
+ });
305
+
306
+ const send = async (force: boolean) => {
307
+ let captured: Record<string, unknown> | undefined;
308
+ const inner: Provider = {
309
+ name: "anthropic",
310
+ async sendMessage(
311
+ _messages: Message[],
312
+ options?: SendMessageOptions,
313
+ ): Promise<ProviderResponse> {
314
+ captured = options?.config as Record<string, unknown> | undefined;
315
+ return makeResponse("anthropic");
316
+ },
317
+ };
318
+ const provider = new RetryProvider(inner);
319
+ await provider.sendMessage(DUMMY_MESSAGES, {
320
+ config: {
321
+ callSite: "inference",
322
+ overrideProfile: "strong",
323
+ ...(force ? { forceOverrideProfile: true } : {}),
324
+ },
325
+ });
326
+ return captured;
327
+ };
328
+
329
+ // Without the flag, the call-site pin (`cheap`) outranks `overrideProfile`.
330
+ expect((await send(false))?.model).toBe("claude-haiku-4-5-20251001");
331
+
332
+ // With the flag, the forced `strong` profile wins over the call-site pin.
333
+ const forced = await send(true);
334
+ expect(forced?.model).toBe("claude-opus-4-8");
335
+ // The routing keys are stripped before the provider wire request.
336
+ expect(forced?.overrideProfile).toBeUndefined();
337
+ expect(forced?.forceOverrideProfile).toBeUndefined();
338
+ });
339
+ });
@@ -46,23 +46,20 @@ import { eq } from "drizzle-orm";
46
46
 
47
47
  import { upsertContactChannel } from "../contacts/contacts-write.js";
48
48
  import type { Conversation } from "../daemon/conversation.js";
49
+ import {
50
+ createCanonicalGuardianDelivery,
51
+ createCanonicalGuardianRequest,
52
+ getCanonicalGuardianRequest,
53
+ } from "../memory/canonical-guardian-store.js";
49
54
  import { getDb } from "../memory/db-connection.js";
50
55
  import { initializeDb } from "../memory/db-init.js";
51
- import {
52
- createApprovalRequest,
53
- getPendingApprovalForRequest,
54
- } from "../memory/guardian-approvals.js";
55
56
  import { messages } from "../memory/schema/conversations.js";
56
57
  import { readSlackMetadata } from "../messaging/providers/slack/message-metadata.js";
57
58
  import * as pendingInteractions from "../runtime/pending-interactions.js";
58
- import {
59
- _clearApprovalPromptTsTrackerForTesting,
60
- trackApprovalPromptTs,
61
- } from "../runtime/routes/approval-prompt-ts-tracker.js";
62
59
  import {
63
60
  isSlackReactionEvent,
64
61
  parseSlackReactionCallbackData,
65
- } from "../runtime/routes/inbound-message-handler.js";
62
+ } from "../runtime/routes/inbound-stages/reaction-intercept.js";
66
63
  import { handleChannelInbound } from "./helpers/channel-test-adapter.js";
67
64
  import { createGuardianBinding } from "./helpers/create-guardian-binding.js";
68
65
 
@@ -84,7 +81,6 @@ function resetState(): void {
84
81
  db.run("DELETE FROM conversations");
85
82
  db.run("DELETE FROM contact_channels");
86
83
  db.run("DELETE FROM contacts");
87
- _clearApprovalPromptTsTrackerForTesting();
88
84
  }
89
85
 
90
86
  function seedActiveMember(): void {
@@ -421,21 +417,35 @@ function seedGuardianForChannel(): void {
421
417
  function seedPendingGuardianApprovalForReaction(
422
418
  requestId: string,
423
419
  conversationId: string,
420
+ reactedTs: string,
424
421
  ): void {
425
- createApprovalRequest({
426
- runId: `run-${requestId}`,
427
- requestId,
422
+ // Canonical tool_approval request keyed by the confirmation requestId — the
423
+ // same record assistant-event-hub creates for every confirmation.
424
+ createCanonicalGuardianRequest({
425
+ id: requestId,
426
+ kind: "tool_approval",
427
+ sourceType: "channel",
428
+ sourceChannel: "slack",
428
429
  conversationId,
429
- channel: "slack",
430
430
  requesterExternalUserId: "requester-user-1",
431
431
  requesterChatId: SLACK_CHANNEL_ID,
432
432
  guardianExternalUserId: GUARDIAN_USER_ID,
433
- guardianChatId: SLACK_CHANNEL_ID,
433
+ guardianPrincipalId: GUARDIAN_USER_ID,
434
434
  toolName: GUARDIAN_REACTION_TOOL,
435
+ status: "pending",
435
436
  expiresAt: Date.now() + 300_000,
436
437
  });
437
438
 
438
- // Register a pending interaction so applyGuardianDecision finds the
439
+ // The delivered approval card request mapping the reaction resolves
440
+ // against (destination_message_id = the reacted Slack ts).
441
+ createCanonicalGuardianDelivery({
442
+ requestId,
443
+ destinationChannel: "slack",
444
+ destinationChatId: SLACK_CHANNEL_ID,
445
+ destinationMessageId: reactedTs,
446
+ });
447
+
448
+ // Register a pending interaction so the tool_approval resolver finds the
439
449
  // requester-side hook to drive `allow`.
440
450
  const handleConfirmationResponse = mock(() => {});
441
451
  const _mockSession = {
@@ -466,18 +476,17 @@ describe("guardian approval-by-reaction integration via handleChannelInbound", (
466
476
  db.run("DELETE FROM conversations");
467
477
  db.run("DELETE FROM contact_channels");
468
478
  db.run("DELETE FROM contacts");
469
- db.run("DELETE FROM channel_guardian_approval_requests");
479
+ db.run("DELETE FROM canonical_guardian_requests");
480
+ db.run("DELETE FROM canonical_guardian_deliveries");
470
481
  pendingInteractions.clear();
471
- _clearApprovalPromptTsTrackerForTesting();
472
482
  msgCounter = 0;
473
483
  });
474
484
 
475
485
  test("guardian reaction on pending approval applies decision and persists no transcript row", async () => {
476
486
  seedGuardianForChannel();
477
487
  const requestId = "req-guardian-react-1";
478
- // Conversation must exist so the approval row's conversation_id FK
479
- // resolves; reuse an inbound seed by sending a no-op message that the
480
- // ACL allows. Easier: insert directly via the DB layer.
488
+ // Back the canonical request and its pending interaction with a real
489
+ // conversation row, inserted directly via the DB layer.
481
490
  const db = getDb();
482
491
  const conversationId = "conv-react-test-1";
483
492
  const now = Date.now();
@@ -491,12 +500,15 @@ describe("guardian approval-by-reaction integration via handleChannelInbound", (
491
500
  )
492
501
  .run(conversationId, now, now);
493
502
 
494
- seedPendingGuardianApprovalForReaction(requestId, conversationId);
495
-
496
503
  const reactedTs = "1700000099.000001";
497
- // Simulate the approval prompt having been delivered on this ts so the
498
- // guardian reaction is scoped to a tracked prompt message.
499
- trackApprovalPromptTs("slack", SLACK_CHANNEL_ID, reactedTs);
504
+ // The approval card was delivered on this ts; its canonical delivery row
505
+ // is how the guardian reaction is scoped to a known approval message.
506
+ seedPendingGuardianApprovalForReaction(
507
+ requestId,
508
+ conversationId,
509
+ reactedTs,
510
+ );
511
+
500
512
  const body = {
501
513
  sourceChannel: "slack",
502
514
  interface: "slack",
@@ -541,11 +553,11 @@ describe("guardian approval-by-reaction integration via handleChannelInbound", (
541
553
 
542
554
  expect(resp.status).toBe(200);
543
555
  expect(json.accepted).toBe(true);
544
- expect(json.approval).toBe("guardian_decision_applied");
556
+ expect(json.canonicalRouter).toBe("canonical_decision_applied");
545
557
  expect(approvalConversationGenerator).not.toHaveBeenCalled();
546
558
 
547
- // The pending approval row is resolved (no longer pending).
548
- expect(getPendingApprovalForRequest(requestId)).toBeNull();
559
+ // The canonical request is resolved (no longer pending).
560
+ expect(getCanonicalGuardianRequest(requestId)?.status).toBe("approved");
549
561
 
550
562
  // No transcript row was written for the reaction itself — resolved
551
563
  // guardian approval reactions have no transcript representation.
@@ -563,3 +575,112 @@ describe("guardian approval-by-reaction integration via handleChannelInbound", (
563
575
  expect(reactionRows.length).toBe(0);
564
576
  });
565
577
  });
578
+
579
+ // ---------------------------------------------------------------------------
580
+ // Reaction access-control regression (LUM-2489)
581
+ // ---------------------------------------------------------------------------
582
+ //
583
+ // A reaction is a passive signal, not an access attempt. Because reactions are
584
+ // dispatched before the ingress pipeline, an unknown user's 👍 must never run
585
+ // ACL (no trusted-contact verification handshake), create a conversation, or
586
+ // write a binding — it is dropped as channel noise. A known contact's reaction
587
+ // is recorded; neither triggers a verification challenge.
588
+
589
+ describe("reaction access control (no verification handshake)", () => {
590
+ const STRANGER_USER_ID = "U_REACTION_STRANGER";
591
+ const CONTACT_USER_ID = "U_REACTION_CONTACT";
592
+ // Guardian's approval channel is a DM, distinct from the public channel the
593
+ // reaction lands in (mirroring production). Reusing the public channel id
594
+ // would let a reactor match the guardian's channel via findContactChannel's
595
+ // externalChatId fallback and mask the bug.
596
+ const GUARDIAN_DM_CHAT = "D_GUARDIAN_DM";
597
+
598
+ function tableCount(table: string): number {
599
+ return (
600
+ getDb().$client.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get() as {
601
+ n: number;
602
+ }
603
+ ).n;
604
+ }
605
+
606
+ beforeEach(() => {
607
+ resetState();
608
+ getDb().run("DELETE FROM channel_verification_sessions");
609
+ // The assistant has a guardian (as in production); the reactors below are
610
+ // different users.
611
+ createGuardianBinding({
612
+ channel: "slack",
613
+ guardianExternalUserId: GUARDIAN_USER_ID,
614
+ guardianDeliveryChatId: GUARDIAN_DM_CHAT,
615
+ guardianPrincipalId: GUARDIAN_USER_ID,
616
+ });
617
+ msgCounter = 0;
618
+ });
619
+
620
+ test("stranger's reaction is dropped — no challenge, session, conversation, or row", async () => {
621
+ let agentDispatched = false;
622
+ const processMessage = async (): Promise<{ messageId: string }> => {
623
+ agentDispatched = true;
624
+ return { messageId: "should-not-be-called" };
625
+ };
626
+
627
+ const req = buildReactionRequest("reaction:thumbsup", {
628
+ actorExternalId: STRANGER_USER_ID,
629
+ actorDisplayName: "Outside Reactor",
630
+ actorUsername: "outsider",
631
+ });
632
+ const resp = await handleChannelInbound(
633
+ req,
634
+ processMessage,
635
+ TEST_BEARER_TOKEN,
636
+ );
637
+ const json = (await resp.json()) as Record<string, unknown>;
638
+
639
+ // Accepted as a passive signal — never denied or turned into a challenge.
640
+ expect(json.accepted).toBe(true);
641
+ expect(json.denied).not.toBe(true);
642
+ expect(json.reason).not.toBe("verification_challenge_sent");
643
+ expect(json.verificationSessionId).toBeUndefined();
644
+ expect(agentDispatched).toBe(false);
645
+
646
+ // No verification handshake, and no side effects: a dropped reaction leaves
647
+ // no transcript row, no conversation, and no binding.
648
+ expect(tableCount("channel_verification_sessions")).toBe(0);
649
+ expect(readPersistedMessages().length).toBe(0);
650
+ expect(tableCount("conversations")).toBe(0);
651
+ expect(tableCount("external_conversation_bindings")).toBe(0);
652
+ });
653
+
654
+ test("known contact's reaction is recorded — no challenge", async () => {
655
+ // A pending contact classifies as `unverified_contact` — a known tier, so
656
+ // its reactions are recorded. On a real message it would be re-challenged,
657
+ // but a reaction must not trigger that.
658
+ upsertContactChannel({
659
+ sourceChannel: "slack",
660
+ externalUserId: CONTACT_USER_ID,
661
+ externalChatId: SLACK_CHANNEL_ID,
662
+ status: "pending",
663
+ policy: "allow",
664
+ displayName: "Pending Contact",
665
+ });
666
+
667
+ const req = buildReactionRequest("reaction:tada", {
668
+ actorExternalId: CONTACT_USER_ID,
669
+ actorDisplayName: "Pending Contact",
670
+ actorUsername: "pending_contact",
671
+ });
672
+ const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
673
+ const json = (await resp.json()) as Record<string, unknown>;
674
+
675
+ expect(json.denied).not.toBe(true);
676
+ expect(json.reason).not.toBe("verification_challenge_sent");
677
+ expect(tableCount("channel_verification_sessions")).toBe(0);
678
+
679
+ const rows = readPersistedMessages();
680
+ expect(rows.length).toBe(1);
681
+ const envelope = JSON.parse(rows[0].metadata!) as Record<string, unknown>;
682
+ const slackMeta = readSlackMetadata(envelope.slackMeta as string);
683
+ expect(slackMeta?.eventKind).toBe("reaction");
684
+ expect(slackMeta?.reaction?.emoji).toBe("tada");
685
+ });
686
+ });