@vellumai/assistant 0.9.0 → 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 (572) hide show
  1. package/ARCHITECTURE.md +18 -34
  2. package/bun.lock +7 -8
  3. package/docs/activation-funnel-telemetry.md +28 -22
  4. package/docs/architecture/security.md +29 -28
  5. package/docs/stt-provider-onboarding.md +3 -5
  6. package/docs/workflows-testing.md +13 -44
  7. package/docs/workflows.md +3 -5
  8. package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +47 -0
  9. package/node_modules/@vellumai/ces-client/src/rpc-client.ts +28 -5
  10. package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
  11. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +97 -0
  12. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
  13. package/node_modules/@vellumai/gateway-client/src/index.ts +32 -6
  14. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +119 -0
  15. package/node_modules/@vellumai/gateway-client/src/types.ts +15 -84
  16. package/openapi.yaml +976 -63
  17. package/package.json +2 -1
  18. package/scripts/sync-llm-catalog.ts +6 -15
  19. package/scripts/sync-web-search-catalog.ts +3 -11
  20. package/src/__tests__/access-request-card-view.test.ts +98 -0
  21. package/src/__tests__/access-request-seed-content-blocks.test.ts +2 -4
  22. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +72 -32
  23. package/src/__tests__/agent-loop-compaction-strip.test.ts +241 -0
  24. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +16 -13
  25. package/src/__tests__/agent-loop-output-hooks.test.ts +69 -0
  26. package/src/__tests__/agent-loop-override-profile.test.ts +25 -0
  27. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -3
  28. package/src/__tests__/app-compiler.test.ts +15 -1
  29. package/src/__tests__/app-dir-path-guard.test.ts +0 -1
  30. package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
  31. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
  32. package/src/__tests__/auth-fallback-events-store.test.ts +6 -14
  33. package/src/__tests__/avatar-identity-sync.test.ts +2 -27
  34. package/src/__tests__/btw-routes.test.ts +6 -8
  35. package/src/__tests__/call-pointer-messages.test.ts +28 -0
  36. package/src/__tests__/cancel-clears-processing.test.ts +89 -0
  37. package/src/__tests__/channel-approval-routes.test.ts +0 -4
  38. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -15
  39. package/src/__tests__/checker.test.ts +0 -3
  40. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +3 -4
  41. package/src/__tests__/compactor-image-manifest-trust.test.ts +21 -1
  42. package/src/__tests__/compactor-summary-call-truncation.test.ts +223 -0
  43. package/src/__tests__/config-loader-backfill.test.ts +268 -27
  44. package/src/__tests__/config-schema.test.ts +35 -0
  45. package/src/__tests__/config-watcher.test.ts +0 -18
  46. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -2
  47. package/src/__tests__/contact-store-user-file.test.ts +0 -6
  48. package/src/__tests__/contacts-tools.test.ts +29 -0
  49. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -0
  50. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  51. package/src/__tests__/conversation-agent-loop.test.ts +58 -0
  52. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  53. package/src/__tests__/conversation-lifecycle.test.ts +7 -9
  54. package/src/__tests__/conversation-load-history-repair.test.ts +101 -0
  55. package/src/__tests__/conversation-routes-guardian-reply.test.ts +15 -12
  56. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -3
  57. package/src/__tests__/conversation-title-service.test.ts +62 -0
  58. package/src/__tests__/credential-broker.test.ts +449 -1
  59. package/src/__tests__/credential-execution-shell-lockdown.test.ts +18 -11
  60. package/src/__tests__/credential-execution-tools.test.ts +0 -1
  61. package/src/__tests__/credential-prompt-route.test.ts +4 -4
  62. package/src/__tests__/credential-routes.test.ts +360 -0
  63. package/src/__tests__/credential-security-invariants.test.ts +4 -13
  64. package/src/__tests__/disk-pressure-policy.test.ts +12 -0
  65. package/src/__tests__/disk-usage.test.ts +65 -0
  66. package/src/__tests__/dynamic-page-surface.test.ts +152 -1
  67. package/src/__tests__/fixtures/credential-security-fixtures.ts +2 -33
  68. package/src/__tests__/gateway-flag-listener.test.ts +110 -1
  69. package/src/__tests__/gateway-only-guard.test.ts +3 -7
  70. package/src/__tests__/guardian-binding-drift-heal.test.ts +1 -1
  71. package/src/__tests__/guardian-card-withdrawal.test.ts +403 -0
  72. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
  73. package/src/__tests__/guardian-grant-minting.test.ts +3 -35
  74. package/src/__tests__/guardian-routing-invariants.test.ts +64 -26
  75. package/src/__tests__/guardian-routing-state.test.ts +0 -1
  76. package/src/__tests__/headless-browser-mode.test.ts +10 -0
  77. package/src/__tests__/headless-browser-navigate.test.ts +8 -3
  78. package/src/__tests__/helpers/create-guardian-binding.ts +0 -1
  79. package/src/__tests__/host-browser-proxy.test.ts +87 -0
  80. package/src/__tests__/identity-routes.test.ts +0 -189
  81. package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
  82. package/src/__tests__/injector-v3-suppression.test.ts +27 -20
  83. package/src/__tests__/internal-telemetry-routes.test.ts +6 -14
  84. package/src/__tests__/invite-redemption-service.test.ts +4 -7
  85. package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
  86. package/src/__tests__/llm-catalog-parity.test.ts +30 -23
  87. package/src/__tests__/llm-resolver.test.ts +70 -24
  88. package/src/__tests__/llm-schema.test.ts +1 -0
  89. package/src/__tests__/managed-profile-guard.test.ts +163 -4
  90. package/src/__tests__/mcp-health-check.test.ts +6 -7
  91. package/src/__tests__/media-stream-server-integration.test.ts +317 -13
  92. package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
  93. package/src/__tests__/onboarding-persona-write.test.ts +1 -1
  94. package/src/__tests__/path-policy.test.ts +34 -0
  95. package/src/__tests__/persona-resolver.test.ts +49 -14
  96. package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
  97. package/src/__tests__/plugin-api-provider.test.ts +24 -0
  98. package/src/__tests__/plugin-tool-contribution.test.ts +6 -3
  99. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +214 -0
  100. package/src/__tests__/provider-send-message-override-profile.test.ts +76 -0
  101. package/src/__tests__/reaction-persistence.test.ts +150 -29
  102. package/src/__tests__/registry.test.ts +2 -7
  103. package/src/__tests__/relay-server.test.ts +285 -0
  104. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  105. package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
  106. package/src/__tests__/schedule-routes.test.ts +0 -30
  107. package/src/__tests__/schedule-tools.test.ts +2 -18
  108. package/src/__tests__/scheduler-reuse-conversation.test.ts +8 -5
  109. package/src/__tests__/skill-execute-input.test.ts +51 -1
  110. package/src/__tests__/skill-runtime-path.test.ts +2 -3
  111. package/src/__tests__/skills.test.ts +51 -0
  112. package/src/__tests__/slack-notification-approval-card.test.ts +176 -0
  113. package/src/__tests__/slack-reaction-canonical-approval.test.ts +285 -0
  114. package/src/__tests__/subagent-tools.test.ts +266 -0
  115. package/src/__tests__/surface-completion-nudge-hook.test.ts +367 -0
  116. package/src/__tests__/task-progress-nudge-hook.test.ts +1 -1
  117. package/src/__tests__/title-generate-hook.test.ts +100 -3
  118. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -29
  119. package/src/__tests__/token-manager.test.ts +519 -0
  120. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +1 -1
  121. package/src/__tests__/tool-audit-listener.test.ts +7 -7
  122. package/src/__tests__/tool-executor-lifecycle-events.test.ts +6 -3
  123. package/src/__tests__/tool-executor.test.ts +0 -79
  124. package/src/__tests__/trusted-contact-approval-notifier.test.ts +4 -2
  125. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +220 -3
  126. package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
  127. package/src/__tests__/trusted-contact-verification.test.ts +8 -10
  128. package/src/__tests__/twilio-routes.test.ts +81 -1
  129. package/src/__tests__/voice-invite-redemption.test.ts +2 -3
  130. package/src/__tests__/weak-open-model.test.ts +30 -0
  131. package/src/__tests__/web-search-catalog-parity.test.ts +6 -25
  132. package/src/__tests__/workspace-greetings.test.ts +152 -0
  133. package/src/__tests__/workspace-migration-105-enable-memory-v3-live-for-new-workspaces.test.ts +149 -0
  134. package/src/__tests__/workspace-migration-108-drop-balanced-economy-profile.test.ts +285 -0
  135. package/src/__tests__/workspace-migration-add-send-diagnostics.test.ts +1 -1
  136. package/src/__tests__/workspace-migration-drop-collect-usage-data.test.ts +118 -0
  137. package/src/__tests__/workspace-migration-drop-send-diagnostics.test.ts +118 -0
  138. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +0 -4
  139. package/src/agent/loop.ts +49 -29
  140. package/src/api/README.md +6 -6
  141. package/src/api/events/tool-result.ts +6 -0
  142. package/src/api/events/workflow-completed.ts +53 -0
  143. package/src/api/events/workflow-leaf-finished.ts +38 -0
  144. package/src/api/events/workflow-leaf-started.ts +35 -0
  145. package/src/api/events/workflow-progress.ts +32 -0
  146. package/src/api/events/workflow-started.ts +31 -0
  147. package/src/api/index.ts +40 -0
  148. package/src/api/responses/conversation-message.ts +28 -4
  149. package/src/api/responses/home.ts +26 -4
  150. package/src/api/responses/workflow-journal.ts +53 -0
  151. package/src/approvals/guardian-card-withdrawal.ts +145 -0
  152. package/src/approvals/guardian-decision-primitive.ts +26 -3
  153. package/src/approvals/guardian-request-resolvers.ts +183 -80
  154. package/src/calls/__tests__/channel-admission-reader.test.ts +132 -0
  155. package/src/calls/__tests__/relay-setup-router.test.ts +350 -0
  156. package/src/calls/call-pointer-messages.ts +10 -4
  157. package/src/calls/channel-admission-reader.ts +104 -0
  158. package/src/calls/guardian-dispatch.ts +17 -45
  159. package/src/calls/media-stream-server.ts +84 -2
  160. package/src/calls/relay-access-wait.ts +1 -1
  161. package/src/calls/relay-server.ts +66 -0
  162. package/src/calls/relay-setup-router.ts +82 -1
  163. package/src/calls/twilio-routes.ts +17 -8
  164. package/src/calls/voice-session-bridge.ts +2 -2
  165. package/src/cli/commands/clients.ts +3 -0
  166. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2-compare-render.test.ts +1 -1
  167. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2.test.ts +8 -7
  168. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v3.test.ts +5 -4
  169. package/src/cli/commands/memory/index.ts +30 -0
  170. package/src/cli/commands/{memory-v2-compare-render.ts → memory/memory-v2-compare-render.ts} +1 -1
  171. package/src/cli/commands/{memory-v2.ts → memory/memory-v2.ts} +6 -15
  172. package/src/cli/commands/{memory-v3.ts → memory/memory-v3.ts} +97 -11
  173. package/src/cli/commands/oauth/status.test.ts +36 -0
  174. package/src/cli/commands/oauth/status.ts +23 -3
  175. package/src/cli/commands/plugins.ts +197 -4
  176. package/src/cli/lib/__tests__/diff-plugin.test.ts +443 -0
  177. package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
  178. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +443 -0
  179. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +111 -0
  180. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +295 -2
  181. package/src/cli/lib/diff-plugin.ts +346 -0
  182. package/src/cli/lib/inspect-plugin.ts +12 -1
  183. package/src/cli/lib/install-from-github.ts +105 -17
  184. package/src/cli/lib/merge-plugin-tree.ts +328 -0
  185. package/src/cli/lib/plugin-fingerprint.ts +14 -0
  186. package/src/cli/lib/plugin-surfaces.ts +104 -0
  187. package/src/cli/lib/upgrade-plugin.ts +298 -10
  188. package/src/cli/program.ts +2 -6
  189. package/src/config/__tests__/sync-gated-profiles.test.ts +368 -0
  190. package/src/config/assistant-feature-flags.ts +22 -7
  191. package/src/config/bundled-skills/contacts/tools/contact-search.ts +0 -1
  192. package/src/config/bundled-skills/messaging/SKILL.md +6 -4
  193. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +9 -8
  194. package/src/config/bundled-skills/subagent/SKILL.md +4 -0
  195. package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
  196. package/src/config/bundled-skills/workflows/SKILL.md +14 -8
  197. package/src/config/bundled-tool-registry.ts +2 -7
  198. package/src/config/call-site-defaults.ts +15 -2
  199. package/src/config/feature-flag-registry.json +46 -31
  200. package/src/config/inference-profile-validation.ts +26 -0
  201. package/src/config/llm-resolver.ts +3 -0
  202. package/src/config/loader.ts +4 -0
  203. package/src/config/memory-v3-gate.ts +11 -0
  204. package/src/config/profile-order.ts +28 -0
  205. package/src/config/schema.ts +8 -6
  206. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  207. package/src/config/schemas/call-site-catalog.ts +7 -0
  208. package/src/config/schemas/channels.ts +11 -0
  209. package/src/config/schemas/elevenlabs.ts +0 -1
  210. package/src/config/schemas/llm.ts +31 -0
  211. package/src/config/schemas/memory-lifecycle.ts +3 -7
  212. package/src/config/schemas/memory-v3.ts +6 -0
  213. package/src/config/schemas/platform.ts +0 -8
  214. package/src/config/schemas/services.ts +18 -0
  215. package/src/config/seed-inference-profiles.ts +109 -44
  216. package/src/config/skills.ts +21 -0
  217. package/src/config/sync-gated-profiles.ts +220 -0
  218. package/src/contacts/contact-store.ts +89 -106
  219. package/src/contacts/contacts-write.ts +5 -22
  220. package/src/contacts/types.ts +0 -1
  221. package/src/context/compactor.ts +88 -54
  222. package/src/context/strip-injections.ts +58 -10
  223. package/src/context/token-estimator.ts +1 -1
  224. package/src/credential-execution/process-manager.ts +55 -14
  225. package/src/credential-execution/prompted-credential.ts +2 -3
  226. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -2
  227. package/src/daemon/config-watcher.ts +0 -4
  228. package/src/daemon/conversation-agent-loop-handlers.ts +2 -0
  229. package/src/daemon/conversation-agent-loop.ts +114 -22
  230. package/src/daemon/conversation-history.ts +1 -1
  231. package/src/daemon/conversation-lifecycle.ts +3 -5
  232. package/src/daemon/conversation-process.ts +13 -5
  233. package/src/daemon/conversation-runtime-assembly.ts +13 -15
  234. package/src/daemon/conversation-slash.ts +2 -23
  235. package/src/daemon/conversation-surfaces.ts +26 -0
  236. package/src/daemon/conversation-tool-setup.ts +27 -14
  237. package/src/daemon/conversation.ts +66 -14
  238. package/src/daemon/disk-pressure-policy.ts +5 -3
  239. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -1
  240. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -1
  241. package/src/daemon/handlers/config-a2a.ts +0 -2
  242. package/src/daemon/handlers/config-channels.ts +15 -16
  243. package/src/daemon/handlers/config-slack-channel.ts +22 -3
  244. package/src/daemon/handlers/conversations.ts +107 -0
  245. package/src/daemon/host-browser-proxy.ts +41 -0
  246. package/src/daemon/lifecycle.ts +55 -27
  247. package/src/daemon/message-provenance.ts +2 -0
  248. package/src/daemon/message-types/contacts.ts +0 -1
  249. package/src/daemon/message-types/conversations.ts +3 -3
  250. package/src/daemon/message-types/sync.ts +0 -1
  251. package/src/daemon/message-types/web-activity.ts +7 -1
  252. package/src/daemon/message-types/workflows.ts +83 -1
  253. package/src/daemon/orphan-reaper.test.ts +0 -19
  254. package/src/daemon/orphan-reaper.ts +2 -24
  255. package/src/daemon/server.ts +0 -10
  256. package/src/daemon/tool-setup-types.ts +4 -0
  257. package/src/daemon/trust-context.ts +1 -1
  258. package/src/events/tool-audit-listener.ts +2 -2
  259. package/src/home/feed-source-enrichment.test.ts +151 -0
  260. package/src/home/feed-source-enrichment.ts +176 -0
  261. package/src/home/relationship-state.ts +2 -4
  262. package/src/instrument.ts +18 -6
  263. package/src/ipc/__tests__/binary-result-ipc.test.ts +81 -0
  264. package/src/ipc/__tests__/clients-list-ipc.test.ts +20 -0
  265. package/src/ipc/assistant-server.ts +37 -4
  266. package/src/ipc/gateway-flag-listener.ts +18 -2
  267. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +5 -16
  268. package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +7 -11
  269. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +37 -7
  270. package/src/memory/__tests__/memory-retrospective-job.test.ts +229 -401
  271. package/src/memory/__tests__/onboarding-events-store.test.ts +7 -7
  272. package/src/memory/auth-fallback-events-store.ts +2 -2
  273. package/src/memory/auto-analysis-enqueue.ts +3 -5
  274. package/src/memory/bookmark-crud.ts +1 -2
  275. package/src/memory/canonical-guardian-store.ts +39 -1
  276. package/src/memory/conversation-crud.ts +9 -4
  277. package/src/memory/conversation-key-store.ts +17 -2
  278. package/src/memory/conversation-title-service.ts +64 -7
  279. package/src/memory/db-init.ts +17 -17
  280. package/src/memory/embedding-backend.ts +38 -1
  281. package/src/memory/embedding-billing-breaker.ts +96 -0
  282. package/src/memory/jobs-store.ts +25 -13
  283. package/src/memory/jobs-worker.ts +54 -1
  284. package/src/memory/lifecycle-events-store.ts +2 -2
  285. package/src/memory/memory-retrospective-constants.ts +4 -4
  286. package/src/memory/memory-retrospective-enqueue.ts +31 -6
  287. package/src/memory/memory-retrospective-job.ts +28 -227
  288. package/src/memory/migrations/129-contact-channels-access-fields.ts +18 -9
  289. package/src/memory/migrations/131-drop-legacy-member-guardian-tables.ts +14 -2
  290. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +10 -0
  291. package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +72 -0
  292. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.test.ts +67 -0
  293. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.ts +25 -0
  294. package/src/memory/migrations/293-workflow-journal-leaf-tokens.ts +32 -0
  295. package/src/memory/migrations/294-drop-external-user-id.ts +31 -0
  296. package/src/memory/migrations/295-drop-approval-prompt-ts-tracker.ts +20 -0
  297. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.test.ts +110 -0
  298. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.ts +68 -0
  299. package/src/memory/migrations/__tests__/131-drop-legacy-member-guardian-tables.test.ts +154 -0
  300. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +31 -0
  301. package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +341 -0
  302. package/src/memory/migrations/__tests__/run-migrations.test.ts +52 -0
  303. package/src/memory/migrations/index.ts +6 -0
  304. package/src/memory/migrations/run-migrations.ts +41 -0
  305. package/src/memory/migrations/validate-migration-state.ts +1 -1
  306. package/src/memory/onboarding-events-store.ts +3 -3
  307. package/src/memory/schema/contacts.ts +0 -5
  308. package/src/memory/skill-loaded-events-store.test.ts +7 -15
  309. package/src/memory/skill-loaded-events-store.ts +2 -2
  310. package/src/memory/tool-executed-events-store.test.ts +7 -7
  311. package/src/memory/turn-trace-store.test.ts +736 -0
  312. package/src/memory/turn-trace-store.ts +364 -0
  313. package/src/memory/v2/__tests__/consolidation-job.test.ts +8 -0
  314. package/src/memory/v2/__tests__/skill-content.test.ts +30 -0
  315. package/src/memory/v2/consolidation-job.ts +2 -2
  316. package/src/memory/v2/skill-content.ts +25 -7
  317. package/src/memory/v2/skill-store.ts +7 -1
  318. package/src/memory/v3-eval/__tests__/eval-packets.test.ts +248 -0
  319. package/src/memory/v3-eval/eval-packets.ts +546 -0
  320. package/src/messaging/providers/slack/adapter.ts +1 -1
  321. package/src/messaging/providers/slack/api.ts +31 -0
  322. package/src/messaging/providers/slack/send.test.ts +114 -2
  323. package/src/messaging/providers/slack/send.ts +30 -7
  324. package/src/messaging/providers/slack/withdraw.test.ts +200 -0
  325. package/src/messaging/providers/slack/withdraw.ts +161 -0
  326. package/src/notifications/AGENTS.md +2 -0
  327. package/src/notifications/access-request-copy.ts +72 -59
  328. package/src/notifications/adapters/shared.ts +29 -0
  329. package/src/notifications/adapters/slack.ts +58 -103
  330. package/src/notifications/adapters/telegram.ts +2 -20
  331. package/src/notifications/approval-card-data.ts +333 -0
  332. package/src/notifications/broadcaster.ts +16 -3
  333. package/src/notifications/canonical-delivery-recorder.ts +139 -0
  334. package/src/notifications/copy-composer.ts +3 -3
  335. package/src/notifications/decision-engine.ts +4 -2
  336. package/src/notifications/destination-resolver.ts +4 -6
  337. package/src/notifications/guardian-question-mode.ts +10 -0
  338. package/src/notifications/home-feed-side-effect.ts +7 -16
  339. package/src/notifications/notification-utils.ts +19 -20
  340. package/src/notifications/signal.ts +79 -43
  341. package/src/notifications/types.ts +98 -121
  342. package/src/oauth/AGENTS.md +5 -24
  343. package/src/permissions/checker.test.ts +51 -0
  344. package/src/permissions/checker.ts +185 -26
  345. package/src/permissions/ipc-risk-types.ts +24 -0
  346. package/src/permissions/question-prompter.test.ts +27 -0
  347. package/src/permissions/question-prompter.ts +4 -0
  348. package/src/platform/client.test.ts +119 -0
  349. package/src/platform/client.ts +66 -0
  350. package/src/platform/consent-cache.test.ts +267 -0
  351. package/src/platform/consent-cache.ts +174 -0
  352. package/src/plugin-api/constants.ts +1 -1
  353. package/src/plugin-api/index.ts +33 -1
  354. package/src/plugin-api/model-profiles.ts +33 -0
  355. package/src/plugin-api/types.ts +50 -2
  356. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +56 -0
  357. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +43 -0
  358. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +137 -0
  359. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +153 -0
  360. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +138 -0
  361. package/src/plugins/defaults/advisor/__tests__/transcript.test.ts +147 -0
  362. package/src/plugins/defaults/advisor/advisor-gate.ts +29 -0
  363. package/src/plugins/defaults/advisor/advisor-state-store.ts +94 -0
  364. package/src/plugins/defaults/advisor/config.ts +21 -0
  365. package/src/plugins/defaults/advisor/consult.ts +93 -0
  366. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +34 -0
  367. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +30 -0
  368. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +19 -0
  369. package/src/plugins/defaults/advisor/package.json +14 -0
  370. package/src/plugins/defaults/advisor/steering.ts +67 -0
  371. package/src/plugins/defaults/advisor/tools/advisor.ts +65 -0
  372. package/src/plugins/defaults/advisor/transcript.ts +76 -0
  373. package/src/plugins/defaults/index.ts +60 -0
  374. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +22 -9
  375. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
  376. package/src/plugins/defaults/memory-retrieval/tail-reinjection-strip.ts +64 -0
  377. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +29 -21
  378. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1 -0
  379. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  380. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +129 -9
  381. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +31 -4
  382. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +77 -2
  383. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +1 -0
  384. package/src/plugins/defaults/memory-v3-shadow/injector.ts +7 -10
  385. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +144 -11
  386. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +32 -20
  387. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +56 -3
  388. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +23 -2
  389. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +276 -0
  390. package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +22 -0
  391. package/src/plugins/defaults/surface-completion-nudge/nudge-state-store.ts +46 -0
  392. package/src/plugins/defaults/surface-completion-nudge/package.json +14 -0
  393. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +3 -13
  394. package/src/plugins/defaults/title-generate/hooks/stop.ts +56 -21
  395. package/src/prompts/persona-resolver.ts +14 -4
  396. package/src/prompts/templates/system-sections.ts +7 -2
  397. package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
  398. package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
  399. package/src/providers/__tests__/retry-callsite.test.ts +176 -0
  400. package/src/providers/atlascloud/client.ts +85 -0
  401. package/src/providers/fetch-provider-catalog.ts +85 -0
  402. package/src/providers/inference/adapter-factory.ts +3 -0
  403. package/src/providers/model-catalog.ts +58 -0
  404. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +33 -0
  405. package/src/providers/openai/chat-completions-provider.ts +7 -0
  406. package/src/providers/openai/responses-provider.ts +10 -0
  407. package/src/providers/provider-send-message.ts +11 -3
  408. package/src/providers/retry.ts +53 -12
  409. package/src/providers/search-provider-catalog.ts +10 -0
  410. package/src/providers/weak-open-model.ts +22 -0
  411. package/src/runtime/AGENTS.md +0 -1
  412. package/src/runtime/__tests__/agent-wake.test.ts +181 -0
  413. package/src/runtime/__tests__/client-health.test.ts +44 -0
  414. package/src/runtime/access-request-helper.ts +21 -53
  415. package/src/runtime/actor-trust-resolver.ts +59 -63
  416. package/src/runtime/agent-wake.ts +52 -0
  417. package/src/runtime/assistant-event-hub.ts +18 -4
  418. package/src/runtime/auth/__tests__/route-policy.test.ts +12 -0
  419. package/src/runtime/auth/require-bound-guardian.ts +1 -4
  420. package/src/runtime/btw-sidechain.ts +3 -6
  421. package/src/runtime/capabilities.test.ts +120 -0
  422. package/src/runtime/capabilities.ts +197 -0
  423. package/src/runtime/channel-approval-types.ts +22 -45
  424. package/src/runtime/channel-invite-transports/telegram.ts +4 -4
  425. package/src/runtime/channel-retry-sweep.ts +1 -0
  426. package/src/runtime/channel-verification-service.ts +3 -3
  427. package/src/runtime/client-health.ts +26 -0
  428. package/src/runtime/confirmation-request-guardian-bridge.ts +38 -29
  429. package/src/runtime/effective-capabilities.test.ts +128 -0
  430. package/src/runtime/effective-capabilities.ts +84 -0
  431. package/src/runtime/guardian-reply-router.ts +106 -21
  432. package/src/runtime/invite-redemption-service.ts +9 -25
  433. package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +123 -0
  434. package/src/runtime/migrations/vbundle-builder.ts +49 -20
  435. package/src/runtime/pending-interactions.ts +15 -0
  436. package/src/runtime/routes/__tests__/client-routes.test.ts +13 -0
  437. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +67 -0
  438. package/src/runtime/routes/__tests__/plugins-routes.test.ts +240 -1
  439. package/src/runtime/routes/app-routes.ts +1 -1
  440. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -2
  441. package/src/runtime/routes/assets/vellum-design-system.css +1959 -0
  442. package/src/runtime/routes/browser-tabs-routes.ts +9 -0
  443. package/src/runtime/routes/btw-routes.ts +1 -27
  444. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +17 -8
  445. package/src/runtime/routes/client-routes.ts +10 -0
  446. package/src/runtime/routes/contact-routes.ts +31 -8
  447. package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
  448. package/src/runtime/routes/conversation-management-routes.ts +80 -1
  449. package/src/runtime/routes/conversation-query-routes.ts +68 -22
  450. package/src/runtime/routes/conversation-routes.ts +39 -14
  451. package/src/runtime/routes/credential-routes.ts +40 -16
  452. package/src/runtime/routes/empty-state-greeting-cache.ts +1 -2
  453. package/src/runtime/routes/events-routes.ts +1 -3
  454. package/src/runtime/routes/guardian-approval-interception.ts +14 -73
  455. package/src/runtime/routes/guardian-approval-prompt.ts +22 -4
  456. package/src/runtime/routes/home-feed-routes.ts +8 -3
  457. package/src/runtime/routes/identity-routes.ts +1 -296
  458. package/src/runtime/routes/inbound-message-handler.ts +214 -228
  459. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +89 -7
  460. package/src/runtime/routes/inbound-stages/admission-policy.test.ts +154 -0
  461. package/src/runtime/routes/inbound-stages/admission-policy.ts +140 -0
  462. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +3 -3
  463. package/src/runtime/routes/inbound-stages/background-dispatch.ts +11 -6
  464. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +1 -2
  465. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +1 -2
  466. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +7 -7
  467. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +47 -28
  468. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +358 -0
  469. package/src/runtime/routes/index.ts +2 -0
  470. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +8 -0
  471. package/src/runtime/routes/integrations/slack/channel.ts +36 -0
  472. package/src/runtime/routes/internal-telemetry-routes.ts +1 -1
  473. package/src/runtime/routes/mcp-auth-routes.ts +233 -41
  474. package/src/runtime/routes/memory-eval-routes.ts +87 -0
  475. package/src/runtime/routes/notification-routes.ts +122 -133
  476. package/src/runtime/routes/platform-routes.ts +2 -2
  477. package/src/runtime/routes/plugins-routes.ts +202 -3
  478. package/src/runtime/routes/schedule-routes.ts +0 -22
  479. package/src/runtime/routes/secret-routes.ts +10 -0
  480. package/src/runtime/routes/surface-action-routes.ts +2 -1
  481. package/src/runtime/routes/tool-call-question-enrichment.test.ts +146 -0
  482. package/src/runtime/routes/tool-call-question-enrichment.ts +66 -0
  483. package/src/runtime/routes/workflow-routes.test.ts +229 -44
  484. package/src/runtime/routes/workflow-routes.ts +131 -29
  485. package/src/runtime/routes/workspace-greetings.ts +55 -0
  486. package/src/runtime/sync/resource-sync-events.ts +1 -11
  487. package/src/runtime/tool-grant-request-helper.ts +18 -16
  488. package/src/runtime/trust-context-resolver.ts +8 -5
  489. package/src/schedule/inference-profile.ts +2 -14
  490. package/src/schedule/schedule-store.ts +1 -1
  491. package/src/schedule/scheduler-types.ts +5 -1
  492. package/src/security/__tests__/provider-key-env-fallback.test.ts +6 -0
  493. package/src/security/secret-patterns.ts +3 -0
  494. package/src/subagent/manager.ts +17 -4
  495. package/src/subagent/types.ts +6 -0
  496. package/src/telemetry/trace-collection-policy.test.ts +28 -0
  497. package/src/telemetry/trace-collection-policy.ts +30 -0
  498. package/src/telemetry/types.ts +89 -0
  499. package/src/telemetry/usage-telemetry-reporter.test.ts +586 -36
  500. package/src/telemetry/usage-telemetry-reporter.ts +148 -41
  501. package/src/tools/AGENTS.md +3 -3
  502. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +31 -0
  503. package/src/tools/browser/browser-execution.ts +30 -19
  504. package/src/tools/document/document-tool.ts +2 -3
  505. package/src/tools/executor.ts +5 -3
  506. package/src/tools/host-terminal/host-shell.ts +5 -4
  507. package/src/tools/memory/register.ts +2 -2
  508. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +360 -0
  509. package/src/tools/network/__tests__/web-search.test.ts +143 -0
  510. package/src/tools/network/web-fetch.ts +372 -1
  511. package/src/tools/network/web-search-error.ts +1 -1
  512. package/src/tools/network/web-search.ts +213 -10
  513. package/src/tools/permission-checker.ts +4 -3
  514. package/src/tools/registry.ts +20 -0
  515. package/src/tools/schedule/create.ts +7 -12
  516. package/src/tools/schedule/update.ts +4 -11
  517. package/src/tools/shared/filesystem/path-policy.ts +39 -13
  518. package/src/tools/side-effects.ts +2 -17
  519. package/src/tools/skills/execute.ts +33 -0
  520. package/src/tools/subagent/spawn.ts +61 -12
  521. package/src/tools/terminal/shell.ts +10 -4
  522. package/src/tools/tool-approval-handler.ts +18 -13
  523. package/src/tools/tool-manifest.ts +0 -2
  524. package/src/tools/types.ts +9 -0
  525. package/src/tools/ui-surface/definitions.ts +64 -3
  526. package/src/tools/verification-control-plane-policy.ts +3 -1
  527. package/src/tools/workflows/run-workflow.test.ts +8 -18
  528. package/src/tools/workflows/run-workflow.ts +1 -0
  529. package/src/util/disk-usage.ts +78 -23
  530. package/src/util/platform.ts +10 -3
  531. package/src/watcher/telemetry.ts +2 -2
  532. package/src/workflows/capabilities.ts +2 -3
  533. package/src/workflows/engine.test.ts +175 -1
  534. package/src/workflows/engine.ts +82 -0
  535. package/src/workflows/journal-store.test.ts +70 -0
  536. package/src/workflows/journal-store.ts +18 -3
  537. package/src/workflows/run-manager.test.ts +171 -28
  538. package/src/workflows/run-manager.ts +66 -24
  539. package/src/workspace/migrations/105-enable-memory-v3-live-for-new-workspaces.ts +63 -0
  540. package/src/workspace/migrations/106-drop-collect-usage-data.ts +47 -0
  541. package/src/workspace/migrations/107-drop-send-diagnostics.ts +47 -0
  542. package/src/workspace/migrations/108-drop-balanced-economy-profile.ts +129 -0
  543. package/src/workspace/migrations/registry.ts +8 -0
  544. package/src/__tests__/app-control-no-global-cgevent.test.ts +0 -98
  545. package/src/__tests__/credential-security-e2e.test.ts +0 -362
  546. package/src/__tests__/credential-vault-unit.test.ts +0 -1528
  547. package/src/__tests__/credential-vault.test.ts +0 -1706
  548. package/src/__tests__/identity-intro-cache.test.ts +0 -315
  549. package/src/__tests__/secret-onetime-send.test.ts +0 -182
  550. package/src/cli/commands/__tests__/task.test.ts +0 -914
  551. package/src/cli/commands/task.ts +0 -771
  552. package/src/config/bundled-skills/personal-page/SKILL.md +0 -57
  553. package/src/config/bundled-skills/personal-page/TOOLS.json +0 -27
  554. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +0 -17
  555. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +0 -22
  556. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +0 -16
  557. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +0 -77
  558. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +0 -57
  559. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +0 -28
  560. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +0 -234
  561. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +0 -48
  562. package/src/config/preloaded-apps/personal-page/src/components/media.ts +0 -16
  563. package/src/config/preloaded-apps/personal-page/src/index.html +0 -20
  564. package/src/config/preloaded-apps/personal-page/src/main.tsx +0 -7
  565. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +0 -82
  566. package/src/config/preloaded-apps/personal-page/src/styles.css +0 -759
  567. package/src/memory/__tests__/preloaded-apps.test.ts +0 -85
  568. package/src/memory/preloaded-apps.ts +0 -116
  569. package/src/notifications/tool-approval-copy.ts +0 -142
  570. package/src/runtime/routes/approval-prompt-ts-tracker.ts +0 -78
  571. package/src/runtime/routes/identity-intro-cache.ts +0 -172
  572. package/src/tools/credentials/vault.ts +0 -712
@@ -20,12 +20,19 @@ mock.module("./api.js", () => ({
20
20
  callSlackApiForm: async () => ({}),
21
21
  completeSlackUpload: async () => {},
22
22
  SlackApiError: class SlackApiError extends Error {
23
- slackError?: string;
23
+ readonly slackError: string | undefined;
24
+
25
+ constructor(slackError: string | undefined) {
26
+ super(slackError ?? "unknown");
27
+ this.slackError = slackError;
28
+ }
24
29
  },
25
30
  uploadToSlackUrl: async () => {},
26
31
  }));
27
32
 
28
- import { sendSlackAssistantThreadStatus } from "./send.js";
33
+ const { SlackApiError } = await import("./api.js");
34
+ const { sendSlackAssistantThreadStatus, sendSlackReply } =
35
+ await import("./send.js");
29
36
 
30
37
  describe("sendSlackAssistantThreadStatus", () => {
31
38
  beforeEach(() => {
@@ -75,3 +82,108 @@ describe("sendSlackAssistantThreadStatus", () => {
75
82
  });
76
83
  });
77
84
  });
85
+
86
+ describe("sendSlackReply update path", () => {
87
+ const messageTs = "1700000000.000100";
88
+ const blocks = [
89
+ { type: "section", text: { type: "mrkdwn", text: "Final reply" } },
90
+ ];
91
+
92
+ beforeEach(() => {
93
+ callSlackApiMock.mockReset();
94
+ callSlackApiMock.mockImplementation(async () => ({ ok: true }));
95
+ });
96
+
97
+ test("retries chat.update without blocks on invalid_blocks instead of posting a duplicate", async () => {
98
+ callSlackApiMock
99
+ .mockImplementationOnce(async () => {
100
+ throw new SlackApiError("invalid_blocks");
101
+ })
102
+ .mockImplementationOnce(async () => ({ ok: true, ts: messageTs }));
103
+
104
+ const result = await sendSlackReply("C123", "Final reply", {
105
+ messageTs,
106
+ threadTs: "1700000000.000001",
107
+ blocks,
108
+ });
109
+
110
+ expect(result).toEqual({ ok: true, ts: messageTs });
111
+ // Two chat.update calls (with then without blocks); never chat.postMessage,
112
+ // so the placeholder is edited in place rather than duplicated.
113
+ expect(callSlackApiMock).toHaveBeenCalledTimes(2);
114
+ expect(callSlackApiMock).toHaveBeenNthCalledWith(1, "chat.update", {
115
+ channel: "C123",
116
+ text: "Final reply",
117
+ ts: messageTs,
118
+ blocks,
119
+ });
120
+ expect(callSlackApiMock).toHaveBeenNthCalledWith(2, "chat.update", {
121
+ channel: "C123",
122
+ text: "Final reply",
123
+ ts: messageTs,
124
+ });
125
+ const postMessageCalls = callSlackApiMock.mock.calls.filter(
126
+ (call) => call[0] === "chat.postMessage",
127
+ );
128
+ expect(postMessageCalls).toHaveLength(0);
129
+ });
130
+
131
+ test("falls back to chat.postMessage only after the no-block update retry also fails", async () => {
132
+ callSlackApiMock
133
+ .mockImplementationOnce(async () => {
134
+ throw new SlackApiError("invalid_blocks");
135
+ })
136
+ .mockImplementationOnce(async () => {
137
+ throw new SlackApiError("message_not_found");
138
+ })
139
+ .mockImplementationOnce(async () => ({
140
+ ok: true,
141
+ ts: "1700000000.000200",
142
+ }));
143
+
144
+ const result = await sendSlackReply("C123", "Final reply", {
145
+ messageTs,
146
+ threadTs: "1700000000.000001",
147
+ blocks,
148
+ });
149
+
150
+ expect(result).toEqual({ ok: true, ts: "1700000000.000200" });
151
+ expect(callSlackApiMock).toHaveBeenCalledTimes(3);
152
+ expect(callSlackApiMock.mock.calls[0]?.[0]).toBe("chat.update");
153
+ expect(callSlackApiMock.mock.calls[1]?.[0]).toBe("chat.update");
154
+ // The post fallback drops the rejected blocks.
155
+ expect(callSlackApiMock).toHaveBeenNthCalledWith(3, "chat.postMessage", {
156
+ channel: "C123",
157
+ text: "Final reply",
158
+ thread_ts: "1700000000.000001",
159
+ });
160
+ });
161
+
162
+ test("non-invalid_blocks update failure still falls back to chat.postMessage", async () => {
163
+ callSlackApiMock
164
+ .mockImplementationOnce(async () => {
165
+ throw new SlackApiError("internal_error");
166
+ })
167
+ .mockImplementationOnce(async () => ({
168
+ ok: true,
169
+ ts: "1700000000.000200",
170
+ }));
171
+
172
+ const result = await sendSlackReply("C123", "Final reply", {
173
+ messageTs,
174
+ threadTs: "1700000000.000001",
175
+ blocks,
176
+ });
177
+
178
+ expect(result).toEqual({ ok: true, ts: "1700000000.000200" });
179
+ // One failed chat.update, then a single chat.postMessage (no extra retry).
180
+ expect(callSlackApiMock).toHaveBeenCalledTimes(2);
181
+ expect(callSlackApiMock.mock.calls[0]?.[0]).toBe("chat.update");
182
+ expect(callSlackApiMock).toHaveBeenNthCalledWith(2, "chat.postMessage", {
183
+ channel: "C123",
184
+ text: "Final reply",
185
+ thread_ts: "1700000000.000001",
186
+ blocks,
187
+ });
188
+ });
189
+ });
@@ -165,15 +165,38 @@ export async function sendSlackReply(
165
165
  );
166
166
  return { ok: true, ts: result.ts };
167
167
  } catch (err) {
168
- if (err instanceof SlackApiError && err.slackError === "invalid_blocks") {
169
- log.warn(
170
- { chatId, messageTs: options!.messageTs },
171
- "chat.update returned invalid_blocks; falling back to chat.postMessage without blocks",
172
- );
173
- delete slackBody.blocks;
168
+ if (
169
+ err instanceof SlackApiError &&
170
+ err.slackError === "invalid_blocks" &&
171
+ Array.isArray(updateBody.blocks) &&
172
+ updateBody.blocks.length > 0
173
+ ) {
174
+ // `invalid_blocks` means Slack rejected the Block Kit payload, not the
175
+ // target message — the message still exists. Retry the update without
176
+ // blocks so it is edited in place. Posting a fresh message here would
177
+ // leave the original alongside a duplicate ("ghost") reply.
178
+ const retryBody: Record<string, unknown> = {
179
+ channel: chatId,
180
+ text,
181
+ ts: options!.messageTs,
182
+ };
183
+ try {
184
+ const retryResult = await callSlackApi("chat.update", retryBody);
185
+ log.info(
186
+ { chatId, messageTs: options!.messageTs },
187
+ "Slack message updated without blocks after invalid_blocks",
188
+ );
189
+ return { ok: true, ts: retryResult.ts };
190
+ } catch (retryErr) {
191
+ log.warn(
192
+ { err: retryErr, chatId, messageTs: options!.messageTs },
193
+ "Slack chat.update without blocks failed, falling back to chat.postMessage",
194
+ );
195
+ delete slackBody.blocks;
196
+ }
174
197
  } else {
175
198
  log.warn(
176
- { chatId, messageTs: options!.messageTs },
199
+ { err, chatId, messageTs: options!.messageTs },
177
200
  "Slack chat.update failed, falling back to chat.postMessage",
178
201
  );
179
202
  }
@@ -0,0 +1,200 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ mock.module("../../../util/logger.js", () => ({
4
+ getLogger: () =>
5
+ new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
6
+ truncateForLog: (value: string) => value,
7
+ }));
8
+
9
+ const getSlackMessageBlocks = mock(
10
+ async (_channel: string, _ts: string): Promise<unknown[] | null> => null,
11
+ );
12
+ const callSlackApi = mock(
13
+ async (_method: string, _body: Record<string, unknown>) => ({
14
+ ok: true,
15
+ ts: "1.0",
16
+ }),
17
+ );
18
+ mock.module("./api.js", () => ({ getSlackMessageBlocks, callSlackApi }));
19
+
20
+ import {
21
+ stripApprovalActionBlocks,
22
+ withdrawSlackApprovalCard,
23
+ } from "./withdraw.js";
24
+
25
+ describe("stripApprovalActionBlocks", () => {
26
+ test("strips the actions array from a native card block, keeping content", () => {
27
+ const blocks = [
28
+ {
29
+ type: "card",
30
+ title: { type: "plain_text", text: "Access Request" },
31
+ body: { type: "mrkdwn", text: "Alice wants in" },
32
+ actions: [{ type: "button", action_id: "apr:r1:approve_once" }],
33
+ },
34
+ ];
35
+ const result = stripApprovalActionBlocks(blocks) as Array<
36
+ Record<string, unknown>
37
+ >;
38
+ expect(result).toHaveLength(1);
39
+ expect(result[0].type).toBe("card");
40
+ expect(result[0].title).toEqual({
41
+ type: "plain_text",
42
+ text: "Access Request",
43
+ });
44
+ expect(result[0].body).toEqual({ type: "mrkdwn", text: "Alice wants in" });
45
+ expect("actions" in result[0]).toBe(false);
46
+ });
47
+
48
+ test("drops standalone actions blocks and preserves everything else", () => {
49
+ const blocks = [
50
+ { type: "section", text: { type: "mrkdwn", text: "Context" } },
51
+ {
52
+ type: "actions",
53
+ elements: [{ type: "button", action_id: "apr:r1:reject" }],
54
+ },
55
+ {
56
+ type: "context",
57
+ elements: [{ type: "mrkdwn", text: "Source: Slack" }],
58
+ },
59
+ ];
60
+ const result = stripApprovalActionBlocks(blocks) as Array<
61
+ Record<string, unknown>
62
+ >;
63
+ expect(result.map((b) => b.type)).toEqual(["section", "context"]);
64
+ });
65
+
66
+ test("tolerates non-object entries", () => {
67
+ const result = stripApprovalActionBlocks([
68
+ null,
69
+ "weird",
70
+ { type: "section" },
71
+ ]);
72
+ expect(result).toHaveLength(3);
73
+ });
74
+ });
75
+
76
+ describe("withdrawSlackApprovalCard", () => {
77
+ beforeEach(() => {
78
+ getSlackMessageBlocks.mockReset();
79
+ callSlackApi.mockReset();
80
+ callSlackApi.mockImplementation(async () => ({ ok: true, ts: "1.0" }));
81
+ });
82
+
83
+ test("edits in place, preserving content, removing buttons, appending status", async () => {
84
+ getSlackMessageBlocks.mockImplementationOnce(async () => [
85
+ {
86
+ type: "card",
87
+ title: { type: "plain_text", text: "Access Request" },
88
+ body: { type: "mrkdwn", text: "Alice wants in" },
89
+ actions: [{ type: "button", action_id: "apr:r1:approve_once" }],
90
+ },
91
+ {
92
+ type: "context",
93
+ elements: [{ type: "mrkdwn", text: "Source: Slack" }],
94
+ },
95
+ ]);
96
+
97
+ await withdrawSlackApprovalCard({
98
+ channel: "C1",
99
+ messageTs: "1700000000.0001",
100
+ status: "approved",
101
+ decidedByExternalUserId: "U-guardian",
102
+ decidedAtMs: 1_700_000_000_000,
103
+ });
104
+
105
+ expect(callSlackApi).toHaveBeenCalledTimes(1);
106
+ const [method, body] = callSlackApi.mock.calls[0];
107
+ expect(method).toBe("chat.update");
108
+ expect(body.channel).toBe("C1");
109
+ expect(body.ts).toBe("1700000000.0001");
110
+
111
+ const blocks = body.blocks as Array<Record<string, unknown>>;
112
+ // original card (no actions) + original context + appended status context
113
+ expect(blocks).toHaveLength(3);
114
+ expect(blocks[0].type).toBe("card");
115
+ expect("actions" in blocks[0]).toBe(false);
116
+ expect(JSON.stringify(blocks)).not.toContain("apr:");
117
+
118
+ // status line carries the outcome, decider mention, and a date token
119
+ const statusText = JSON.stringify(blocks[2]);
120
+ expect(statusText).toContain("Approved");
121
+ expect(statusText).toContain("<@U-guardian>");
122
+ expect(statusText).toContain("<!date^1700000000^");
123
+ });
124
+
125
+ test("falls back to a status-only edit when the original blocks can't be read", async () => {
126
+ getSlackMessageBlocks.mockImplementationOnce(async () => null);
127
+
128
+ await withdrawSlackApprovalCard({
129
+ channel: "C1",
130
+ messageTs: "1.0",
131
+ status: "denied",
132
+ });
133
+
134
+ expect(callSlackApi).toHaveBeenCalledTimes(1);
135
+ const [method, body] = callSlackApi.mock.calls[0];
136
+ expect(method).toBe("chat.update");
137
+ const blocks = body.blocks as Array<Record<string, unknown>>;
138
+ expect(blocks).toHaveLength(1);
139
+ expect(blocks[0].type).toBe("section");
140
+ expect(JSON.stringify(blocks)).toContain("Denied");
141
+ });
142
+
143
+ test("falls back when the fetch throws (e.g. missing history scope)", async () => {
144
+ getSlackMessageBlocks.mockImplementationOnce(async () => {
145
+ throw new Error("missing_scope");
146
+ });
147
+
148
+ await withdrawSlackApprovalCard({
149
+ channel: "C1",
150
+ messageTs: "1.0",
151
+ status: "approved",
152
+ });
153
+
154
+ expect(callSlackApi).toHaveBeenCalledTimes(1);
155
+ expect(callSlackApi.mock.calls[0][0]).toBe("chat.update");
156
+ });
157
+
158
+ test("retries with a status-only edit when preserved blocks are rejected", async () => {
159
+ getSlackMessageBlocks.mockImplementationOnce(async () => [
160
+ {
161
+ type: "card",
162
+ body: { type: "mrkdwn", text: "Alice wants in" },
163
+ actions: [{ type: "button", action_id: "apr:r1:approve_once" }],
164
+ },
165
+ ]);
166
+ // First chat.update (preserved blocks) is rejected; second must still edit.
167
+ callSlackApi.mockImplementationOnce(async () => {
168
+ throw new Error("invalid_blocks");
169
+ });
170
+
171
+ await withdrawSlackApprovalCard({
172
+ channel: "C1",
173
+ messageTs: "1.0",
174
+ status: "approved",
175
+ });
176
+
177
+ expect(callSlackApi).toHaveBeenCalledTimes(2);
178
+ // both attempts are edits — never a new message
179
+ expect(callSlackApi.mock.calls.every((c) => c[0] === "chat.update")).toBe(
180
+ true,
181
+ );
182
+ const retryBlocks = callSlackApi.mock.calls[1][1].blocks as Array<
183
+ Record<string, unknown>
184
+ >;
185
+ expect(retryBlocks).toHaveLength(1);
186
+ expect(retryBlocks[0].type).toBe("section");
187
+ });
188
+
189
+ test("never posts a new message (withdrawal is edit-only)", async () => {
190
+ getSlackMessageBlocks.mockImplementationOnce(async () => null);
191
+ await withdrawSlackApprovalCard({
192
+ channel: "C1",
193
+ messageTs: "1.0",
194
+ status: "approved",
195
+ });
196
+ expect(
197
+ callSlackApi.mock.calls.some((c) => c[0] === "chat.postMessage"),
198
+ ).toBe(false);
199
+ });
200
+ });
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Withdraw a Slack approval card when its underlying guardian request resolves.
3
+ *
4
+ * "Withdraw" preserves the card's information and removes only the live
5
+ * affordances: the original message blocks are fetched, the action buttons are
6
+ * stripped, and a resolved-status line (outcome, decider, time) is appended,
7
+ * then the message is edited in place. This keeps an in-channel audit trail
8
+ * rather than collapsing the card to a bare status.
9
+ *
10
+ * This is strictly an in-place edit (`chat.update`). It never posts a new
11
+ * message: the whole point is to drop the buttons on the *existing* card, so a
12
+ * "post instead" fallback would duplicate the card and leave its buttons live.
13
+ * When the original blocks can't be read (missing `*:history` scope, threaded
14
+ * reply, deleted message) or don't round-trip through `chat.update`, it falls
15
+ * back to editing the message down to a status-only block — still an edit.
16
+ */
17
+
18
+ import { getLogger } from "../../../util/logger.js";
19
+ import { callSlackApi, getSlackMessageBlocks } from "./api.js";
20
+
21
+ const log = getLogger("slack-withdraw");
22
+
23
+ const STATUS_GLYPH: Record<string, string> = {
24
+ approved: ":white_check_mark:",
25
+ denied: ":x:",
26
+ expired: ":hourglass_done:",
27
+ cancelled: ":no_entry_sign:",
28
+ };
29
+
30
+ const STATUS_WORD: Record<string, string> = {
31
+ approved: "Approved",
32
+ denied: "Denied",
33
+ expired: "Expired",
34
+ cancelled: "Cancelled",
35
+ };
36
+
37
+ export interface WithdrawSlackApprovalCardParams {
38
+ /** Channel/DM id the approval message lives in. */
39
+ channel: string;
40
+ /** Timestamp (`ts`) of the approval message to edit. */
41
+ messageTs: string;
42
+ /** Terminal status of the request (e.g. "approved", "denied", "expired"). */
43
+ status: string;
44
+ /** Slack user id of the decider, when the decision came from Slack. */
45
+ decidedByExternalUserId?: string;
46
+ /** Decision time (epoch ms) for the audit line. */
47
+ decidedAtMs?: number;
48
+ }
49
+
50
+ /**
51
+ * Build the resolved-status line shown in place of the action buttons.
52
+ * Uses Slack's `<!date>` token so the time renders in each viewer's timezone.
53
+ */
54
+ function buildStatusText(params: WithdrawSlackApprovalCardParams): string {
55
+ const glyph = STATUS_GLYPH[params.status] ?? "";
56
+ const word = STATUS_WORD[params.status] ?? "Resolved";
57
+ let line = glyph ? `${glyph} *${word}*` : `*${word}*`;
58
+ if (params.decidedByExternalUserId) {
59
+ line += ` by <@${params.decidedByExternalUserId}>`;
60
+ }
61
+ if (params.decidedAtMs && Number.isFinite(params.decidedAtMs)) {
62
+ const seconds = Math.floor(params.decidedAtMs / 1000);
63
+ const fallback = new Date(params.decidedAtMs).toISOString();
64
+ line += ` · <!date^${seconds}^{date_short_pretty} at {time}|${fallback}>`;
65
+ }
66
+ return line;
67
+ }
68
+
69
+ /**
70
+ * Remove interactive affordances from a fetched message's blocks: drop
71
+ * standalone `actions` rows and strip the `actions` array from native `card`
72
+ * blocks, leaving all informational content intact.
73
+ */
74
+ export function stripApprovalActionBlocks(blocks: unknown[]): unknown[] {
75
+ const result: unknown[] = [];
76
+ for (const block of blocks) {
77
+ if (!block || typeof block !== "object") {
78
+ result.push(block);
79
+ continue;
80
+ }
81
+ const b = block as Record<string, unknown>;
82
+ if (b.type === "actions") continue;
83
+ if (b.type === "card" && "actions" in b) {
84
+ const { actions: _removed, ...rest } = b;
85
+ result.push(rest);
86
+ continue;
87
+ }
88
+ result.push(block);
89
+ }
90
+ return result;
91
+ }
92
+
93
+ /** Edit a message in place. Throws on Slack API failure. */
94
+ async function editMessage(
95
+ channel: string,
96
+ ts: string,
97
+ text: string,
98
+ blocks: unknown[],
99
+ ): Promise<void> {
100
+ await callSlackApi("chat.update", { channel, ts, text, blocks });
101
+ }
102
+
103
+ /**
104
+ * Edit a Slack approval message in place to its resolved state, preserving the
105
+ * card content and dropping its buttons. Throws only if even the status-only
106
+ * edit fails, which the caller treats as non-fatal.
107
+ */
108
+ export async function withdrawSlackApprovalCard(
109
+ params: WithdrawSlackApprovalCardParams,
110
+ ): Promise<void> {
111
+ const statusText = buildStatusText(params);
112
+ const statusOnlyBlocks = [
113
+ { type: "section", text: { type: "mrkdwn", text: statusText } },
114
+ ];
115
+
116
+ let preserved: unknown[] | null = null;
117
+ try {
118
+ const original = await getSlackMessageBlocks(
119
+ params.channel,
120
+ params.messageTs,
121
+ );
122
+ if (original && original.length > 0) {
123
+ preserved = [
124
+ ...stripApprovalActionBlocks(original),
125
+ { type: "context", elements: [{ type: "mrkdwn", text: statusText }] },
126
+ ];
127
+ }
128
+ } catch (err) {
129
+ log.warn(
130
+ { err, channel: params.channel, messageTs: params.messageTs },
131
+ "Could not read original Slack approval card; collapsing to status only",
132
+ );
133
+ }
134
+
135
+ // Try to preserve the card content; if those blocks are rejected (e.g. a
136
+ // block type that doesn't round-trip through chat.update), fall back to a
137
+ // status-only edit. Both paths are edits — never a new message.
138
+ if (preserved) {
139
+ try {
140
+ await editMessage(
141
+ params.channel,
142
+ params.messageTs,
143
+ statusText,
144
+ preserved,
145
+ );
146
+ return;
147
+ } catch (err) {
148
+ log.warn(
149
+ { err, channel: params.channel, messageTs: params.messageTs },
150
+ "Editing Slack card with preserved blocks failed; retrying status-only",
151
+ );
152
+ }
153
+ }
154
+
155
+ await editMessage(
156
+ params.channel,
157
+ params.messageTs,
158
+ statusText,
159
+ statusOnlyBlocks,
160
+ );
161
+ }
@@ -3,3 +3,5 @@
3
3
  All notification producers **MUST** go through `emitNotificationSignal()` in `notifications/emit-signal.ts`. Do not bypass the pipeline by broadcasting events directly -- the pipeline handles event persistence, deduplication, decision routing, and delivery audit.
4
4
 
5
5
  When a notification flow creates a server-side conversation (e.g. guardian question conversations, task run conversations), the conversation and initial message **MUST** be persisted before the conversation-created event is emitted. This ensures the macOS/iOS client can immediately fetch the conversation contents when it receives the event.
6
+
7
+ Guardian-request producers (access requests, tool approvals, tool-grant escalations, voice questions, trusted-contact confirmations) **MUST** record approval-card deliveries through `recordApprovalCardDelivery` / `recordGuardianRequestDeliveries` in `canonical-delivery-recorder.ts` -- never by calling `createCanonicalGuardianDelivery` directly. That sink is the single place the card-to-request addressing convention lives (conversation id for the in-app card; chat + channel-native `ts` for channel cards), so the path that writes a delivery row cannot drift from the paths that read it back to withdraw a card or resolve an emoji reaction -- the drift that previously fanned this logic into four divergent copies. (Tests may call the store helper directly for fixtures.)