@vellumai/assistant 0.8.2 → 0.8.4

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 (503) hide show
  1. package/ARCHITECTURE.md +11 -12
  2. package/docker-entrypoint.sh +13 -2
  3. package/docker-init-apt-root.sh +79 -6
  4. package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
  5. package/openapi.yaml +945 -36
  6. package/package.json +1 -1
  7. package/src/__tests__/agent-loop-exit-reason.test.ts +271 -0
  8. package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
  9. package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
  10. package/src/__tests__/agent-loop.test.ts +88 -3
  11. package/src/__tests__/anthropic-provider.test.ts +272 -0
  12. package/src/__tests__/approval-cascade.test.ts +1 -1
  13. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
  14. package/src/__tests__/channel-delivery-store.test.ts +193 -0
  15. package/src/__tests__/channel-reply-delivery.test.ts +284 -5
  16. package/src/__tests__/channel-retry-sweep.test.ts +274 -1
  17. package/src/__tests__/compaction-events.test.ts +1 -1
  18. package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
  19. package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
  20. package/src/__tests__/config-get-vision-flag.test.ts +136 -0
  21. package/src/__tests__/config-loader-backfill.test.ts +115 -18
  22. package/src/__tests__/config-watcher.test.ts +1 -1
  23. package/src/__tests__/context-token-estimator.test.ts +112 -57
  24. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  25. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +54 -3
  26. package/src/__tests__/conversation-agent-loop-overflow.test.ts +31 -6
  27. package/src/__tests__/conversation-agent-loop.test.ts +77 -3
  28. package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
  29. package/src/__tests__/conversation-clean-command.test.ts +137 -0
  30. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
  31. package/src/__tests__/conversation-fork-crud.test.ts +161 -0
  32. package/src/__tests__/conversation-lifecycle.test.ts +1 -1
  33. package/src/__tests__/conversation-load-cleaned-at.test.ts +279 -0
  34. package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
  35. package/src/__tests__/conversation-media-retry.test.ts +19 -8
  36. package/src/__tests__/conversation-pairing.test.ts +2 -2
  37. package/src/__tests__/conversation-process-callsite.test.ts +1 -1
  38. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
  39. package/src/__tests__/conversation-queue.test.ts +1 -1
  40. package/src/__tests__/conversation-runtime-assembly.test.ts +290 -85
  41. package/src/__tests__/conversation-seed-composer.test.ts +66 -4
  42. package/src/__tests__/conversation-slash-commands.test.ts +36 -8
  43. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  44. package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
  45. package/src/__tests__/conversation-speed-override.test.ts +1 -1
  46. package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
  47. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
  48. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  49. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  50. package/src/__tests__/credential-security-invariants.test.ts +6 -0
  51. package/src/__tests__/cu-unified-flow.test.ts +10 -1
  52. package/src/__tests__/date-context.test.ts +45 -0
  53. package/src/__tests__/dm-backfill.test.ts +64 -0
  54. package/src/__tests__/dm-persistence.test.ts +33 -0
  55. package/src/__tests__/document-find-replace.test.ts +501 -0
  56. package/src/__tests__/external-plugin-loader.test.ts +91 -19
  57. package/src/__tests__/first-greeting.test.ts +23 -2
  58. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
  59. package/src/__tests__/guardian-dispatch.test.ts +1 -0
  60. package/src/__tests__/headless-browser-navigate.test.ts +172 -0
  61. package/src/__tests__/heartbeat-service.test.ts +24 -164
  62. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  63. package/src/__tests__/host-app-control-proxy.test.ts +241 -0
  64. package/src/__tests__/host-bash-proxy.test.ts +6 -0
  65. package/src/__tests__/host-browser-proxy.test.ts +10 -0
  66. package/src/__tests__/host-cu-proxy.test.ts +8 -1
  67. package/src/__tests__/host-file-proxy.test.ts +8 -1
  68. package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
  69. package/src/__tests__/host-transfer-proxy.test.ts +8 -1
  70. package/src/__tests__/identity-routes.test.ts +57 -0
  71. package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
  72. package/src/__tests__/injector-background-turn.test.ts +153 -0
  73. package/src/__tests__/injector-chain.test.ts +7 -0
  74. package/src/__tests__/injector-document-comments.test.ts +378 -0
  75. package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
  76. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
  77. package/src/__tests__/list-messages-attachments.test.ts +21 -17
  78. package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
  79. package/src/__tests__/list-messages-page-latest.test.ts +130 -14
  80. package/src/__tests__/list-messages-tool-merge.test.ts +17 -16
  81. package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
  82. package/src/__tests__/llm-catalog-parity.test.ts +3 -0
  83. package/src/__tests__/llm-context-normalization.test.ts +0 -2
  84. package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
  85. package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
  86. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
  87. package/src/__tests__/llm-resolver.test.ts +340 -3
  88. package/src/__tests__/log-export-routes.test.ts +99 -2
  89. package/src/__tests__/managed-profile-guard.test.ts +10 -0
  90. package/src/__tests__/message-queue-steer.test.ts +114 -0
  91. package/src/__tests__/notification-decision-fallback.test.ts +0 -91
  92. package/src/__tests__/notification-decision-strategy.test.ts +14 -31
  93. package/src/__tests__/notification-deep-link.test.ts +15 -0
  94. package/src/__tests__/notification-guardian-path.test.ts +1 -2
  95. package/src/__tests__/notification-platform-adapter.test.ts +5 -4
  96. package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
  97. package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
  98. package/src/__tests__/openai-provider.test.ts +323 -3
  99. package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
  100. package/src/__tests__/openai-responses-provider.test.ts +4 -4
  101. package/src/__tests__/openrouter-provider-only.test.ts +51 -3
  102. package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
  103. package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
  104. package/src/__tests__/pending-interactions-resolved-event.test.ts +190 -0
  105. package/src/__tests__/platform-proxy-context.test.ts +6 -1
  106. package/src/__tests__/platform.test.ts +0 -3
  107. package/src/__tests__/plugin-source-watcher.test.ts +302 -0
  108. package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
  109. package/src/__tests__/plugin-types.test.ts +2 -2
  110. package/src/__tests__/process-message-background-slack.test.ts +1 -51
  111. package/src/__tests__/process-message-display-content.test.ts +21 -16
  112. package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
  113. package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
  114. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
  115. package/src/__tests__/server-history-render.test.ts +83 -4
  116. package/src/__tests__/steer-tool-repair.test.ts +249 -0
  117. package/src/__tests__/system-prompt.test.ts +57 -101
  118. package/src/__tests__/terminal-tools.test.ts +11 -1
  119. package/src/__tests__/thinking-block-replay.test.ts +113 -0
  120. package/src/__tests__/thread-backfill.test.ts +370 -22
  121. package/src/__tests__/tool-executor.test.ts +90 -1
  122. package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
  123. package/src/__tests__/twilio-routes.test.ts +1 -1
  124. package/src/__tests__/web-fetch.test.ts +2 -2
  125. package/src/__tests__/workspace-git-service.test.ts +88 -5
  126. package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
  127. package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
  128. package/src/a2a/__tests__/agent-card.test.ts +98 -0
  129. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
  130. package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
  131. package/src/a2a/__tests__/task-store.test.ts +246 -0
  132. package/src/a2a/agent-card.ts +58 -0
  133. package/src/a2a/feature-gate.ts +8 -0
  134. package/src/a2a/protocol-constants.ts +21 -0
  135. package/src/a2a/protocol-errors.ts +50 -0
  136. package/src/a2a/protocol-types.ts +162 -0
  137. package/src/a2a/task-store.ts +168 -0
  138. package/src/agent/attachments.ts +1 -0
  139. package/src/agent/loop.ts +208 -22
  140. package/src/background-wake/next-wake.test.ts +289 -0
  141. package/src/background-wake/next-wake.ts +172 -0
  142. package/src/browser/operations.ts +15 -0
  143. package/src/channels/config.ts +9 -0
  144. package/src/channels/types.ts +14 -0
  145. package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
  146. package/src/cli/commands/__tests__/memory-v2.test.ts +9 -12
  147. package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
  148. package/src/cli/commands/__tests__/schedules.test.ts +469 -0
  149. package/src/cli/commands/conversations.ts +128 -1
  150. package/src/cli/commands/inference-providers.ts +147 -1
  151. package/src/cli/commands/memory-v2.ts +308 -0
  152. package/src/cli/commands/notifications.ts +89 -37
  153. package/src/cli/commands/plugins.ts +67 -0
  154. package/src/cli/commands/schedules.ts +297 -5
  155. package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
  156. package/src/cli/lib/install-from-github.ts +8 -9
  157. package/src/cli/lib/search-plugins.ts +163 -0
  158. package/src/cli/program.ts +14 -0
  159. package/src/cli/utils/conversation-id.ts +17 -5
  160. package/src/config/assistant-feature-flags.ts +24 -54
  161. package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
  162. package/src/config/bundled-skills/document-editor/SKILL.md +115 -0
  163. package/src/config/bundled-skills/document-editor/TOOLS.json +240 -0
  164. package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
  165. package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
  166. package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
  167. package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
  168. package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
  169. package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
  170. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  171. package/src/config/bundled-skills/schedule/SKILL.md +8 -0
  172. package/src/config/bundled-tool-registry.ts +22 -12
  173. package/src/config/call-site-defaults.ts +124 -0
  174. package/src/config/feature-flag-registry.json +111 -23
  175. package/src/config/llm-resolver.ts +66 -1
  176. package/src/config/schema.ts +2 -0
  177. package/src/config/schemas/__tests__/memory-v2.test.ts +7 -3
  178. package/src/config/schemas/call-site-catalog.ts +21 -0
  179. package/src/config/schemas/channels.ts +9 -0
  180. package/src/config/schemas/conversations.ts +10 -0
  181. package/src/config/schemas/heartbeat.ts +14 -0
  182. package/src/config/schemas/llm.ts +4 -3
  183. package/src/config/schemas/memory-retrospective.ts +1 -1
  184. package/src/config/schemas/memory-v2.ts +51 -4
  185. package/src/config/schemas/memory.ts +3 -1
  186. package/src/config/seed-inference-profiles.ts +99 -29
  187. package/src/context/compactor.ts +80 -13
  188. package/src/context/token-estimator.ts +72 -31
  189. package/src/context/window-manager.ts +25 -0
  190. package/src/credential-health/credential-health-service.ts +34 -19
  191. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
  192. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
  193. package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
  194. package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
  195. package/src/daemon/conversation-agent-loop-handlers.ts +231 -23
  196. package/src/daemon/conversation-agent-loop.ts +252 -56
  197. package/src/daemon/conversation-lifecycle.ts +142 -116
  198. package/src/daemon/conversation-messaging.ts +3 -0
  199. package/src/daemon/conversation-process.ts +273 -0
  200. package/src/daemon/conversation-queue-manager.ts +14 -0
  201. package/src/daemon/conversation-runtime-assembly.ts +144 -75
  202. package/src/daemon/conversation-slash.ts +37 -5
  203. package/src/daemon/conversation-surfaces.ts +45 -2
  204. package/src/daemon/conversation-tool-setup.ts +7 -0
  205. package/src/daemon/conversation.ts +42 -12
  206. package/src/daemon/date-context.ts +40 -0
  207. package/src/daemon/first-greeting.ts +10 -0
  208. package/src/daemon/guardian-action-generators.ts +1 -125
  209. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
  210. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
  211. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
  212. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
  213. package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
  214. package/src/daemon/handlers/config-a2a.ts +449 -0
  215. package/src/daemon/handlers/config-model.test.ts +1 -0
  216. package/src/daemon/handlers/conversations.ts +80 -0
  217. package/src/daemon/handlers/shared.ts +92 -29
  218. package/src/daemon/host-app-control-proxy.ts +69 -18
  219. package/src/daemon/host-bash-proxy.ts +1 -1
  220. package/src/daemon/host-cu-proxy.ts +1 -1
  221. package/src/daemon/host-file-proxy.ts +1 -1
  222. package/src/daemon/host-proxy-preactivation.ts +85 -18
  223. package/src/daemon/host-transfer-proxy.ts +1 -1
  224. package/src/daemon/lifecycle.ts +67 -65
  225. package/src/daemon/memory-v2-startup.ts +49 -13
  226. package/src/daemon/message-protocol.ts +4 -0
  227. package/src/daemon/message-types/conversations.ts +8 -0
  228. package/src/daemon/message-types/document-comments.ts +50 -0
  229. package/src/daemon/message-types/messages.ts +68 -1
  230. package/src/daemon/message-types/notifications.ts +21 -0
  231. package/src/daemon/message-types/surfaces.ts +3 -1
  232. package/src/daemon/message-types/web-activity.ts +57 -0
  233. package/src/daemon/pkb-reminder-builder.test.ts +10 -53
  234. package/src/daemon/pkb-reminder-builder.ts +4 -19
  235. package/src/daemon/plugin-source-watcher.ts +135 -3
  236. package/src/daemon/process-message.ts +72 -12
  237. package/src/daemon/query-complexity-router.ts +75 -0
  238. package/src/daemon/skill-memory-refresh.ts +5 -1
  239. package/src/daemon/trust-context.ts +6 -0
  240. package/src/daemon/wake-target-adapter.ts +2 -0
  241. package/src/documents/document-comments-store.test.ts +338 -0
  242. package/src/documents/document-comments-store.ts +237 -0
  243. package/src/documents/document-store.ts +202 -0
  244. package/src/export/__tests__/transcript-formatter.test.ts +121 -0
  245. package/src/export/transcript-formatter.ts +54 -20
  246. package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -1
  247. package/src/heartbeat/heartbeat-service.ts +35 -191
  248. package/src/home/__tests__/feed-types.test.ts +40 -0
  249. package/src/home/__tests__/suggested-prompts.test.ts +33 -2
  250. package/src/home/feed-types.ts +20 -3
  251. package/src/home/home-content-refresh.ts +52 -0
  252. package/src/home/home-greeting-cache.ts +69 -0
  253. package/src/home/home-greeting.ts +94 -0
  254. package/src/home/suggested-prompts.ts +177 -9
  255. package/src/ipc/cli-client.ts +147 -45
  256. package/src/memory/__tests__/conversation-queries.test.ts +220 -0
  257. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
  258. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
  259. package/src/memory/__tests__/memory-retrospective-job.test.ts +407 -10
  260. package/src/memory/conversation-crud.ts +133 -43
  261. package/src/memory/conversation-queries.ts +87 -1
  262. package/src/memory/conversation-title-service.ts +26 -4
  263. package/src/memory/db-init.ts +22 -0
  264. package/src/memory/delivery-crud.ts +41 -0
  265. package/src/memory/delivery-status.ts +141 -15
  266. package/src/memory/external-conversation-store.ts +32 -1
  267. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
  268. package/src/memory/graph/conversation-graph-memory.ts +18 -6
  269. package/src/memory/graph/tools.ts +6 -37
  270. package/src/memory/invite-store.ts +53 -0
  271. package/src/memory/jobs-worker.ts +21 -1
  272. package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
  273. package/src/memory/llm-request-log-store.ts +92 -1
  274. package/src/memory/memory-retrospective-constants.ts +28 -0
  275. package/src/memory/memory-retrospective-enqueue.ts +4 -22
  276. package/src/memory/memory-retrospective-job.ts +438 -21
  277. package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
  278. package/src/memory/memory-v2-activation-log-store.ts +26 -8
  279. package/src/memory/migrations/100-core-tables.ts +1 -0
  280. package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
  281. package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
  282. package/src/memory/migrations/251-a2a-tasks.ts +49 -0
  283. package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
  284. package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
  285. package/src/memory/migrations/253-document-comments.ts +47 -0
  286. package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
  287. package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
  288. package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
  289. package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
  290. package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
  291. package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
  292. package/src/memory/migrations/index.ts +20 -0
  293. package/src/memory/migrations/registry.ts +33 -0
  294. package/src/memory/onboarding-events-store.ts +7 -0
  295. package/src/memory/schema/a2a.ts +15 -0
  296. package/src/memory/schema/calls.ts +1 -0
  297. package/src/memory/schema/conversations.ts +3 -0
  298. package/src/memory/schema/index.ts +1 -0
  299. package/src/memory/schema/inference.ts +2 -0
  300. package/src/memory/schema/infrastructure.ts +2 -0
  301. package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
  302. package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
  303. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
  304. package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
  305. package/src/memory/v2/__tests__/injection.test.ts +221 -17
  306. package/src/memory/v2/__tests__/page-index.test.ts +365 -1
  307. package/src/memory/v2/__tests__/router.test.ts +489 -1
  308. package/src/memory/v2/__tests__/static-context.test.ts +12 -1
  309. package/src/memory/v2/activation-store.ts +14 -16
  310. package/src/memory/v2/cli-command-content.ts +19 -0
  311. package/src/memory/v2/cli-command-store.ts +304 -0
  312. package/src/memory/v2/consolidation-job.ts +14 -0
  313. package/src/memory/v2/frontmatter-sweep.ts +7 -1
  314. package/src/memory/v2/injection-events.ts +101 -0
  315. package/src/memory/v2/injection.ts +69 -29
  316. package/src/memory/v2/page-index.ts +246 -19
  317. package/src/memory/v2/page-store.ts +18 -0
  318. package/src/memory/v2/router.ts +209 -55
  319. package/src/memory/v2/static-context.ts +4 -4
  320. package/src/memory/v2/types.ts +23 -0
  321. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
  322. package/src/messaging/providers/a2a/deliver.ts +156 -0
  323. package/src/messaging/providers/gmail/client.ts +9 -2
  324. package/src/messaging/providers/index.ts +18 -3
  325. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
  326. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
  327. package/src/messaging/providers/slack/adapter.ts +178 -25
  328. package/src/messaging/providers/slack/api.test.ts +54 -0
  329. package/src/messaging/providers/slack/api.ts +119 -3
  330. package/src/messaging/providers/slack/client.ts +12 -0
  331. package/src/messaging/providers/slack/deep-link.ts +20 -1
  332. package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
  333. package/src/messaging/providers/slack/message-metadata.ts +156 -0
  334. package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
  335. package/src/messaging/providers/slack/render-transcript.ts +176 -49
  336. package/src/messaging/providers/slack/send.test.ts +77 -0
  337. package/src/messaging/providers/slack/send.ts +8 -2
  338. package/src/messaging/providers/slack/types.ts +14 -0
  339. package/src/notifications/__tests__/broadcaster.test.ts +203 -0
  340. package/src/notifications/__tests__/decision-engine.test.ts +283 -0
  341. package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
  342. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +5 -1
  343. package/src/notifications/__tests__/home-feed-side-effect.test.ts +521 -36
  344. package/src/notifications/adapters/macos.ts +12 -2
  345. package/src/notifications/broadcaster.ts +29 -4
  346. package/src/notifications/conversation-seed-composer.ts +14 -2
  347. package/src/notifications/copy-composer.ts +17 -64
  348. package/src/notifications/decision-engine.ts +111 -44
  349. package/src/notifications/deferred-emit.ts +135 -0
  350. package/src/notifications/deterministic-checks.ts +96 -0
  351. package/src/notifications/emit-signal.ts +10 -1
  352. package/src/notifications/home-feed-side-effect.ts +136 -27
  353. package/src/notifications/signal.ts +0 -4
  354. package/src/notifications/types.ts +8 -0
  355. package/src/oauth/connect-orchestrator.ts +3 -0
  356. package/src/oauth/credential-token-resolver.ts +2 -0
  357. package/src/oauth/manual-token-connection.ts +19 -0
  358. package/src/oauth/oauth-store.ts +12 -0
  359. package/src/oauth/platform-connection.test.ts +43 -3
  360. package/src/oauth/platform-connection.ts +13 -4
  361. package/src/oauth/seed-providers.ts +22 -0
  362. package/src/permissions/prompter.ts +5 -2
  363. package/src/permissions/secret-prompter.ts +4 -1
  364. package/src/plugins/defaults/injectors.ts +118 -26
  365. package/src/plugins/external-plugin-loader.ts +82 -10
  366. package/src/plugins/types.ts +16 -7
  367. package/src/prompts/__tests__/system-prompt.test.ts +44 -45
  368. package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
  369. package/src/prompts/normalize-onboarding.ts +40 -0
  370. package/src/prompts/sections.ts +32 -14
  371. package/src/prompts/system-prompt.ts +105 -76
  372. package/src/prompts/template-detection.ts +37 -0
  373. package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
  374. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  375. package/src/prompts/templates/VOICE.md +3 -0
  376. package/src/prompts/templates/system-sections.ts +51 -10
  377. package/src/providers/__tests__/inference.test.ts +2 -0
  378. package/src/providers/anthropic/client.ts +132 -5
  379. package/src/providers/call-site-routing.ts +24 -6
  380. package/src/providers/connection-resolution.ts +63 -13
  381. package/src/providers/fireworks/client.ts +20 -2
  382. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
  383. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
  384. package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
  385. package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
  386. package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
  387. package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
  388. package/src/providers/inference/adapter-factory.ts +24 -21
  389. package/src/providers/inference/auth.ts +15 -3
  390. package/src/providers/inference/backfill.ts +14 -1
  391. package/src/providers/inference/codex-token-refresh.ts +128 -0
  392. package/src/providers/inference/connections.ts +85 -5
  393. package/src/providers/inference/resolve-auth.ts +50 -5
  394. package/src/providers/model-catalog.ts +244 -242
  395. package/src/providers/model-intents.ts +3 -3
  396. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
  397. package/src/providers/openai/chat-completions-provider.ts +215 -25
  398. package/src/providers/openai/responses-provider.ts +9 -3
  399. package/src/providers/openrouter/client.ts +46 -4
  400. package/src/providers/platform-proxy/constants.ts +3 -4
  401. package/src/providers/provider-catalog-visibility.ts +3 -1
  402. package/src/providers/provider-send-message.ts +27 -12
  403. package/src/providers/registry.ts +30 -1
  404. package/src/providers/types.ts +25 -0
  405. package/src/runtime/__tests__/agent-wake.test.ts +214 -0
  406. package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
  407. package/src/runtime/agent-wake.ts +212 -57
  408. package/src/runtime/auth/route-policy.ts +20 -3
  409. package/src/runtime/background-job-runner.ts +26 -0
  410. package/src/runtime/channel-reply-delivery.ts +182 -47
  411. package/src/runtime/channel-retry-sweep.ts +141 -16
  412. package/src/runtime/http-server.ts +7 -16
  413. package/src/runtime/http-types.ts +7 -51
  414. package/src/runtime/pending-interactions.ts +51 -8
  415. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
  416. package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
  417. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +121 -5
  418. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
  419. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
  420. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
  421. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +271 -0
  422. package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
  423. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
  424. package/src/runtime/routes/approval-routes.ts +4 -1
  425. package/src/runtime/routes/channel-availability-routes.ts +5 -0
  426. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
  427. package/src/runtime/routes/consolidation-routes.ts +100 -0
  428. package/src/runtime/routes/content-source-routes.ts +78 -0
  429. package/src/runtime/routes/conversation-cli-routes.ts +146 -1
  430. package/src/runtime/routes/conversation-query-routes.ts +130 -12
  431. package/src/runtime/routes/conversation-routes.ts +288 -76
  432. package/src/runtime/routes/document-comments-routes.ts +287 -0
  433. package/src/runtime/routes/documents-routes.ts +33 -0
  434. package/src/runtime/routes/home-feed-routes.ts +6 -3
  435. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  436. package/src/runtime/routes/host-browser-routes.ts +8 -1
  437. package/src/runtime/routes/identity-routes.ts +21 -0
  438. package/src/runtime/routes/inbound-message-handler.ts +288 -58
  439. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
  440. package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
  441. package/src/runtime/routes/index.ts +14 -4
  442. package/src/runtime/routes/inference-provider-connection-routes.ts +192 -3
  443. package/src/runtime/routes/integrations/a2a.ts +294 -0
  444. package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
  445. package/src/runtime/routes/log-export-routes.ts +39 -0
  446. package/src/runtime/routes/memory-v2-routes.ts +217 -0
  447. package/src/runtime/routes/notification-routes.ts +19 -2
  448. package/src/runtime/routes/question-routes.ts +4 -1
  449. package/src/runtime/routes/sanity-routes.ts +159 -0
  450. package/src/runtime/routes/slack-channel-routes.ts +187 -0
  451. package/src/runtime/routes/subagents-routes.ts +41 -0
  452. package/src/runtime/services/conversation-serializer.ts +30 -4
  453. package/src/schedule/integration-status.ts +3 -1
  454. package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
  455. package/src/security/oauth2-device-code.ts +307 -0
  456. package/src/security/oauth2.ts +26 -9
  457. package/src/security/secure-keys.ts +5 -0
  458. package/src/skills/catalog-install.ts +6 -2
  459. package/src/subagent/manager.ts +2 -0
  460. package/src/tools/browser/__tests__/pinned-tabs.test.ts +80 -0
  461. package/src/tools/browser/browser-execution.ts +93 -0
  462. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
  463. package/src/tools/browser/cdp-client/__tests__/types.test.ts +1 -0
  464. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +10 -0
  465. package/src/tools/browser/cdp-client/extension-cdp-client.ts +15 -1
  466. package/src/tools/browser/cdp-client/factory.ts +87 -3
  467. package/src/tools/browser/cdp-client/local-cdp-client.ts +9 -0
  468. package/src/tools/browser/cdp-client/types.ts +36 -0
  469. package/src/tools/browser/pinned-tabs.ts +90 -0
  470. package/src/tools/document/document-comment-tool.test.ts +379 -0
  471. package/src/tools/document/document-comment-tool.ts +156 -0
  472. package/src/tools/document/document-tool.ts +128 -2
  473. package/src/tools/memory/register.ts +1 -9
  474. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
  475. package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
  476. package/src/tools/network/domain-normalize.ts +17 -0
  477. package/src/tools/network/web-fetch.ts +213 -64
  478. package/src/tools/network/web-search.ts +191 -66
  479. package/src/tools/registry.ts +2 -2
  480. package/src/tools/terminal/safe-env.ts +3 -2
  481. package/src/tools/tool-approval-handler.ts +19 -12
  482. package/src/tools/types.ts +41 -2
  483. package/src/tools/ui-surface/definitions.ts +3 -1
  484. package/src/types/onboarding-context.ts +4 -0
  485. package/src/util/__tests__/favicon.test.ts +84 -0
  486. package/src/util/favicon.ts +40 -0
  487. package/src/util/platform.ts +0 -5
  488. package/src/workspace/git-service.ts +75 -4
  489. package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
  490. package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
  491. package/src/workspace/migrations/registry.ts +4 -0
  492. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
  493. package/src/config/bundled-skills/document/SKILL.md +0 -54
  494. package/src/config/bundled-skills/document/TOOLS.json +0 -106
  495. package/src/daemon/seed-files.ts +0 -18
  496. package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
  497. package/src/runtime/guardian-action-conversation-turn.ts +0 -99
  498. package/src/runtime/routes/interface-routes.ts +0 -43
  499. /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
  500. /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
  501. /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
  502. /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
  503. /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
@@ -0,0 +1,156 @@
1
+ /**
2
+ * A2A direct delivery adapter.
3
+ *
4
+ * Completes an A2A task with response artifacts and optionally POSTs the
5
+ * completed task to the requester's push notification URL.
6
+ */
7
+
8
+ import type {
9
+ ChannelDeliveryResult,
10
+ ChannelReplyPayload,
11
+ } from "@vellumai/gateway-client";
12
+
13
+ import {
14
+ A2A_CONTENT_TYPE,
15
+ A2A_VERSION,
16
+ A2A_VERSION_HEADER,
17
+ } from "../../../a2a/protocol-constants.js";
18
+ import type { Part } from "../../../a2a/protocol-types.js";
19
+ import * as taskStore from "../../../a2a/task-store.js";
20
+ import { getLogger } from "../../../util/logger.js";
21
+ import {
22
+ computeRetryDelay,
23
+ isRetryableStatus,
24
+ sleep,
25
+ } from "../../../util/retry.js";
26
+
27
+ const log = getLogger("a2a-deliver");
28
+
29
+ const MAX_RETRIES = 3;
30
+ const PUSH_TIMEOUT_MS = 15_000;
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Helpers
34
+ // ---------------------------------------------------------------------------
35
+
36
+ /** Extract the `taskId` query parameter from a callback URL. */
37
+ function parseTaskId(callbackUrl: string): string | null {
38
+ try {
39
+ return new URL(callbackUrl).searchParams.get("taskId");
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ /** Build A2A parts from a channel reply payload. */
46
+ function buildParts(payload: ChannelReplyPayload): Part[] {
47
+ const parts: Part[] = [];
48
+
49
+ if (payload.text) {
50
+ parts.push({ kind: "text", text: payload.text });
51
+ }
52
+
53
+ if (payload.attachments) {
54
+ for (const att of payload.attachments) {
55
+ parts.push({
56
+ kind: "file",
57
+ filename: att.filename,
58
+ media_type: att.mimeType,
59
+ url: att.data,
60
+ });
61
+ }
62
+ }
63
+
64
+ return parts;
65
+ }
66
+
67
+ /** POST the completed task to the requester's push URL with retry. */
68
+ async function pushNotification(
69
+ pushUrl: string,
70
+ taskJson: unknown,
71
+ ): Promise<void> {
72
+ let lastError: Error | null = null;
73
+
74
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
75
+ if (attempt > 0) {
76
+ await sleep(computeRetryDelay(attempt - 1));
77
+ }
78
+
79
+ try {
80
+ const response = await fetch(pushUrl, {
81
+ method: "POST",
82
+ headers: {
83
+ "Content-Type": A2A_CONTENT_TYPE,
84
+ [A2A_VERSION_HEADER]: A2A_VERSION,
85
+ },
86
+ body: JSON.stringify(taskJson),
87
+ signal: AbortSignal.timeout(PUSH_TIMEOUT_MS),
88
+ });
89
+
90
+ if (response.ok) return;
91
+
92
+ const body = await response.text().catch(() => "");
93
+ lastError = new Error(
94
+ `Push notification failed with status ${response.status}: ${body}`,
95
+ );
96
+
97
+ if (!isRetryableStatus(response.status)) {
98
+ break;
99
+ }
100
+ } catch (err) {
101
+ lastError = err instanceof Error ? err : new Error(String(err));
102
+ }
103
+ }
104
+
105
+ // Push failure is logged but doesn't propagate
106
+ log.warn(
107
+ { pushUrl, error: lastError?.message },
108
+ "A2A push notification failed after retries",
109
+ );
110
+ }
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Public API
114
+ // ---------------------------------------------------------------------------
115
+
116
+ /** Deliver an assistant reply as an A2A task completion. */
117
+ export async function deliverA2AReply(
118
+ callbackUrl: string,
119
+ payload: ChannelReplyPayload,
120
+ ): Promise<ChannelDeliveryResult> {
121
+ const taskId = parseTaskId(callbackUrl);
122
+ if (!taskId) {
123
+ return { ok: false };
124
+ }
125
+
126
+ const parts = buildParts(payload);
127
+ if (parts.length === 0) {
128
+ log.debug({ taskId }, "No content to deliver; skipping A2A completion");
129
+ return { ok: true };
130
+ }
131
+
132
+ let completedTask;
133
+ try {
134
+ completedTask = taskStore.completeWithArtifacts(taskId, [
135
+ { artifact_id: crypto.randomUUID(), parts },
136
+ ]);
137
+ } catch (err) {
138
+ const message = err instanceof Error ? err.message : String(err);
139
+ log.error({ taskId, error: message }, "Failed to complete A2A task");
140
+ return { ok: false };
141
+ }
142
+
143
+ // Push notification — fire-and-forget
144
+ const pushUrl = taskStore.getPushUrl(taskId);
145
+ if (pushUrl) {
146
+ pushNotification(pushUrl, completedTask).catch((err) => {
147
+ log.error(
148
+ { taskId, pushUrl, error: String(err) },
149
+ "Unexpected push notification error",
150
+ );
151
+ });
152
+ }
153
+
154
+ log.info({ taskId }, "A2A reply delivered");
155
+ return { ok: true };
156
+ }
@@ -422,13 +422,16 @@ async function executeBatchCall(
422
422
  /** Max concurrent individual getMessage requests (matches batch concurrency) */
423
423
  const INDIVIDUAL_CONCURRENCY = BATCH_CONCURRENCY;
424
424
 
425
+ /** Delay between waves of individual fetches to avoid rate-limit storms (ms). */
426
+ const INTER_WAVE_DELAY_MS = 500;
427
+
425
428
  /**
426
429
  * Fetch all messages individually using getMessage (no batch endpoint).
427
430
  * Used as a fallback when the batch API is unavailable (e.g. platform connections
428
431
  * that cannot expose raw tokens for the multipart batch endpoint).
429
432
  *
430
- * Processes messages in waves of INDIVIDUAL_CONCURRENCY to avoid unbounded
431
- * parallelism that would trigger 429s on high-volume paths like senderDigest.
433
+ * Processes messages in waves of INDIVIDUAL_CONCURRENCY with a brief inter-wave
434
+ * delay to avoid triggering upstream rate limits on high-volume paths.
432
435
  */
433
436
  async function fetchMessagesIndividually(
434
437
  connection: OAuthConnection,
@@ -441,6 +444,10 @@ async function fetchMessagesIndividually(
441
444
  const results: GmailMessage[] = [];
442
445
  for (let i = 0; i < messageIds.length; i += INDIVIDUAL_CONCURRENCY) {
443
446
  signal?.throwIfAborted();
447
+ // Delay between waves (skip before the first wave)
448
+ if (i > 0) {
449
+ await signalAwareSleep(INTER_WAVE_DELAY_MS, signal);
450
+ }
444
451
  const wave = messageIds.slice(i, i + INDIVIDUAL_CONCURRENCY);
445
452
  const waveResults = await Promise.all(
446
453
  wave.map((id) =>
@@ -5,7 +5,7 @@
5
5
  * matcher and send logic here. The gateway-client consults
6
6
  * `isDirectDelivery()` before falling back to the HTTP proxy path.
7
7
  *
8
- * Currently supported: WhatsApp, Telegram, Slack.
8
+ * Currently supported: WhatsApp, Telegram, Slack, A2A.
9
9
  */
10
10
 
11
11
  import type {
@@ -15,6 +15,7 @@ import type {
15
15
  import { ChannelDeliveryError } from "@vellumai/gateway-client/http-delivery";
16
16
 
17
17
  import { getLogger } from "../../util/logger.js";
18
+ import { deliverA2AReply } from "./a2a/deliver.js";
18
19
  import {
19
20
  sendSlackAssistantThreadStatus,
20
21
  sendSlackAttachments,
@@ -59,6 +60,10 @@ function isSlackCallback(callbackUrl: string): boolean {
59
60
  }
60
61
  }
61
62
 
63
+ function isA2ACallback(callbackUrl: string): boolean {
64
+ return matchesPathname(callbackUrl, "/deliver/a2a");
65
+ }
66
+
62
67
  function parseSlackCallbackParams(callbackUrl: string): {
63
68
  channel?: string;
64
69
  threadTs?: string;
@@ -169,8 +174,14 @@ async function deliverSlack(
169
174
  channel,
170
175
  threadTs: statusThreadTs,
171
176
  status,
177
+ loadingMessages,
172
178
  } = payload.assistantThreadStatus;
173
- await sendSlackAssistantThreadStatus(channel, statusThreadTs, status);
179
+ await sendSlackAssistantThreadStatus(
180
+ channel,
181
+ statusThreadTs,
182
+ status,
183
+ loadingMessages,
184
+ );
174
185
  return { ok: true };
175
186
  }
176
187
 
@@ -233,7 +244,8 @@ export function isDirectDelivery(callbackUrl: string): boolean {
233
244
  return (
234
245
  isWhatsAppCallback(callbackUrl) ||
235
246
  isTelegramCallback(callbackUrl) ||
236
- isSlackCallback(callbackUrl)
247
+ isSlackCallback(callbackUrl) ||
248
+ isA2ACallback(callbackUrl)
237
249
  );
238
250
  }
239
251
 
@@ -254,6 +266,9 @@ export async function deliverDirect(
254
266
  if (isSlackCallback(callbackUrl)) {
255
267
  return deliverSlack(callbackUrl, payload);
256
268
  }
269
+ if (isA2ACallback(callbackUrl)) {
270
+ return deliverA2AReply(callbackUrl, payload);
271
+ }
257
272
 
258
273
  // Defensive — isDirectDelivery should have returned false.
259
274
  throw new Error(
@@ -21,7 +21,7 @@ mock.module("../../../../oauth/oauth-store.js", () => ({
21
21
  isProviderConnected: async () => false,
22
22
  }));
23
23
 
24
- const findContactChannelMock = mock(() => undefined);
24
+ const findContactChannelMock = mock((): unknown => undefined);
25
25
  const upsertContactChannelMock = mock(() => {});
26
26
  mock.module("../../../../contacts/contact-store.js", () => ({
27
27
  findContactChannel: findContactChannelMock,
@@ -30,9 +30,13 @@ mock.module("../../../../contacts/contacts-write.js", () => ({
30
30
  upsertContactChannel: upsertContactChannelMock,
31
31
  }));
32
32
 
33
- import { slackProvider } from "../adapter.js";
33
+ import {
34
+ __resetSlackUserInfoCacheForTests,
35
+ slackProvider,
36
+ } from "../adapter.js";
34
37
 
35
38
  const originalFetch = globalThis.fetch;
39
+ let userInfoCalls: string[] = [];
36
40
 
37
41
  function installFetchStub() {
38
42
  globalThis.fetch = (async (
@@ -60,6 +64,73 @@ function fakeSlackResponse(url: string): Record<string, unknown> {
60
64
  const method = parsed.pathname.split("/").at(-1);
61
65
 
62
66
  if (method === "conversations.history") {
67
+ if (parsed.searchParams.get("channel") === "C_USERINFO_FAIL") {
68
+ return {
69
+ ok: true,
70
+ has_more: false,
71
+ messages: [
72
+ {
73
+ type: "message",
74
+ ts: "1700000006.000700",
75
+ user: "UMISSING",
76
+ text: "Fallback sender message",
77
+ },
78
+ ],
79
+ };
80
+ }
81
+
82
+ if (parsed.searchParams.get("channel") === "C_USERINFO_RETRY") {
83
+ return {
84
+ ok: true,
85
+ has_more: false,
86
+ messages: [
87
+ {
88
+ type: "message",
89
+ ts: "1700000007.000800",
90
+ user: "URETRY",
91
+ text: "Retry sender message",
92
+ },
93
+ ],
94
+ };
95
+ }
96
+
97
+ if (parsed.searchParams.get("channel") === "C_TIMEZONE_CACHE") {
98
+ return {
99
+ ok: true,
100
+ has_more: false,
101
+ messages: [
102
+ {
103
+ type: "message",
104
+ ts: "1700000004.000500",
105
+ user: "USENDER",
106
+ text: "First timezone-bearing message",
107
+ },
108
+ {
109
+ type: "message",
110
+ ts: "1700000005.000600",
111
+ user: "USENDER",
112
+ text: "Second timezone-bearing message",
113
+ },
114
+ ],
115
+ };
116
+ }
117
+
118
+ if (parsed.searchParams.get("channel") === "C_BOT_HISTORY") {
119
+ return {
120
+ ok: true,
121
+ has_more: false,
122
+ messages: [
123
+ {
124
+ type: "message",
125
+ subtype: "bot_message",
126
+ ts: "1700000003.000400",
127
+ bot_id: "B_ASSISTANT",
128
+ text: "Bot-authored history",
129
+ },
130
+ ],
131
+ };
132
+ }
133
+
63
134
  return {
64
135
  ok: true,
65
136
  has_more: false,
@@ -117,7 +188,9 @@ function fakeSlackResponse(url: string): Record<string, unknown> {
117
188
  }
118
189
 
119
190
  if (method === "users.info") {
120
- return fakeUserInfoResponse(parsed.searchParams.get("user") ?? "");
191
+ const userId = parsed.searchParams.get("user") ?? "";
192
+ userInfoCalls.push(userId);
193
+ return fakeUserInfoResponse(userId);
121
194
  }
122
195
 
123
196
  return { ok: true };
@@ -141,6 +214,9 @@ function fakeUserInfoResponse(userId: string): Record<string, unknown> {
141
214
  user: {
142
215
  id: "USENDER",
143
216
  name: "sender",
217
+ tz: "America/New_York",
218
+ tz_label: "Eastern Time",
219
+ tz_offset: -18000,
144
220
  profile: { display_name: "Sender" },
145
221
  },
146
222
  };
@@ -157,11 +233,93 @@ function fakeUserInfoResponse(userId: string): Record<string, unknown> {
157
233
  };
158
234
  }
159
235
 
236
+ if (userId === "URETRY") {
237
+ const retryCalls = userInfoCalls.filter((call) => call === userId).length;
238
+ if (retryCalls === 1) {
239
+ return { ok: false, error: "temporarily_unavailable" };
240
+ }
241
+ return {
242
+ ok: true,
243
+ user: {
244
+ id: "URETRY",
245
+ name: "retry_sender",
246
+ tz: "America/Chicago",
247
+ tz_label: "Central Time",
248
+ tz_offset: -21600,
249
+ profile: { display_name: "Retry Sender" },
250
+ },
251
+ };
252
+ }
253
+
160
254
  return { ok: false, error: "user_not_found" };
161
255
  }
162
256
 
257
+ function makeOAuthConnection(
258
+ id: string,
259
+ accountInfo: string,
260
+ displayName: string,
261
+ timezone: string,
262
+ timezoneLabel: string,
263
+ timezoneOffsetSeconds: number,
264
+ ): OAuthConnection {
265
+ return {
266
+ id,
267
+ provider: "slack",
268
+ accountInfo,
269
+ request: async (req) => {
270
+ if (req.path === "/conversations.history") {
271
+ return {
272
+ status: 200,
273
+ headers: {},
274
+ body: {
275
+ ok: true,
276
+ has_more: false,
277
+ messages: [
278
+ {
279
+ type: "message",
280
+ ts: "1700000008.000900",
281
+ user: "USAME",
282
+ text: "Account scoped sender",
283
+ },
284
+ ],
285
+ },
286
+ };
287
+ }
288
+ if (req.path === "/users.info") {
289
+ const userId = req.query?.user;
290
+ userInfoCalls.push(`${id}:${userId}`);
291
+ return {
292
+ status: 200,
293
+ headers: {},
294
+ body: {
295
+ ok: true,
296
+ user: {
297
+ id: "USAME",
298
+ name: displayName.toLowerCase().replaceAll(" ", "_"),
299
+ tz: timezone,
300
+ tz_label: timezoneLabel,
301
+ tz_offset: timezoneOffsetSeconds,
302
+ profile: { display_name: displayName },
303
+ },
304
+ },
305
+ };
306
+ }
307
+ return {
308
+ status: 200,
309
+ headers: {},
310
+ body: { ok: true },
311
+ };
312
+ },
313
+ withToken: async <T>(_fn: (token: string) => Promise<T>): Promise<T> => {
314
+ throw new Error("withToken was not expected");
315
+ },
316
+ };
317
+ }
318
+
163
319
  describe("Slack adapter mention rendering", () => {
164
320
  beforeEach(async () => {
321
+ __resetSlackUserInfoCacheForTests();
322
+ userInfoCalls = [];
165
323
  getSecureKeyAsyncMock.mockReset();
166
324
  getSecureKeyAsyncMock.mockImplementation(async (key: string) => {
167
325
  if (key === credentialKey("slack_channel", "bot_token")) {
@@ -190,6 +348,174 @@ describe("Slack adapter mention rendering", () => {
190
348
  expect(messages[0].reactions).toEqual([{ name: "eyes", count: 1 }]);
191
349
  });
192
350
 
351
+ test("getHistory preserves bot ids for bot-authored Slack messages", async () => {
352
+ const messages = await slackProvider.getHistory(undefined, "C_BOT_HISTORY");
353
+
354
+ expect(messages).toHaveLength(1);
355
+ expect(messages[0].sender).toEqual({ id: "B_ASSISTANT", name: "unknown" });
356
+ expect(messages[0].metadata).toEqual({
357
+ isBot: true,
358
+ slackBotId: "B_ASSISTANT",
359
+ });
360
+ });
361
+
362
+ test("getHistory caches Slack user info and maps timezone metadata", async () => {
363
+ const messages = await slackProvider.getHistory(
364
+ undefined,
365
+ "C_TIMEZONE_CACHE",
366
+ );
367
+
368
+ expect(messages).toHaveLength(2);
369
+ expect(userInfoCalls.filter((userId) => userId === "USENDER")).toHaveLength(
370
+ 1,
371
+ );
372
+ expect(messages.map((message) => message.sender)).toEqual([
373
+ { id: "USENDER", name: "Sender" },
374
+ { id: "USENDER", name: "Sender" },
375
+ ]);
376
+ expect(messages.map((message) => message.metadata)).toEqual([
377
+ {
378
+ actorTimezone: "America/New_York",
379
+ actorTimezoneLabel: "Eastern Time",
380
+ actorTimezoneOffsetSeconds: -18000,
381
+ },
382
+ {
383
+ actorTimezone: "America/New_York",
384
+ actorTimezoneLabel: "Eastern Time",
385
+ actorTimezoneOffsetSeconds: -18000,
386
+ },
387
+ ]);
388
+ });
389
+
390
+ test("getHistory prefers contact display names while still fetching Slack timezone facts", async () => {
391
+ findContactChannelMock.mockImplementationOnce(() => ({
392
+ contact: { displayName: "Saved Sender" },
393
+ }));
394
+
395
+ const messages = await slackProvider.getHistory(
396
+ undefined,
397
+ "C_TIMEZONE_CACHE",
398
+ );
399
+
400
+ expect(userInfoCalls.filter((userId) => userId === "USENDER")).toHaveLength(
401
+ 1,
402
+ );
403
+ expect(messages[0].sender).toEqual({
404
+ id: "USENDER",
405
+ name: "Saved Sender",
406
+ });
407
+ expect(messages[0].metadata).toEqual({
408
+ actorTimezone: "America/New_York",
409
+ actorTimezoneLabel: "Eastern Time",
410
+ actorTimezoneOffsetSeconds: -18000,
411
+ });
412
+ });
413
+
414
+ test("getHistory caches fallback user info for permanent users.info failures", async () => {
415
+ const firstMessages = await slackProvider.getHistory(
416
+ undefined,
417
+ "C_USERINFO_FAIL",
418
+ );
419
+ const secondMessages = await slackProvider.getHistory(
420
+ undefined,
421
+ "C_USERINFO_FAIL",
422
+ );
423
+
424
+ expect(firstMessages).toHaveLength(1);
425
+ expect(secondMessages).toHaveLength(1);
426
+ expect(
427
+ userInfoCalls.filter((userId) => userId === "UMISSING"),
428
+ ).toHaveLength(1);
429
+ expect(firstMessages[0].sender).toEqual({
430
+ id: "UMISSING",
431
+ name: "UMISSING",
432
+ });
433
+ expect(secondMessages[0].sender).toEqual({
434
+ id: "UMISSING",
435
+ name: "UMISSING",
436
+ });
437
+ expect(firstMessages[0].metadata).toBeUndefined();
438
+ expect(secondMessages[0].metadata).toBeUndefined();
439
+ });
440
+
441
+ test("getHistory does not cache fallback user info after transient users.info failures", async () => {
442
+ const firstMessages = await slackProvider.getHistory(
443
+ undefined,
444
+ "C_USERINFO_RETRY",
445
+ );
446
+ const secondMessages = await slackProvider.getHistory(
447
+ undefined,
448
+ "C_USERINFO_RETRY",
449
+ );
450
+
451
+ expect(userInfoCalls.filter((userId) => userId === "URETRY")).toHaveLength(
452
+ 2,
453
+ );
454
+ expect(firstMessages[0].sender).toEqual({
455
+ id: "URETRY",
456
+ name: "URETRY",
457
+ });
458
+ expect(firstMessages[0].metadata).toBeUndefined();
459
+ expect(secondMessages[0].sender).toEqual({
460
+ id: "URETRY",
461
+ name: "Retry Sender",
462
+ });
463
+ expect(secondMessages[0].metadata).toEqual({
464
+ actorTimezone: "America/Chicago",
465
+ actorTimezoneLabel: "Central Time",
466
+ actorTimezoneOffsetSeconds: -21600,
467
+ });
468
+ });
469
+
470
+ test("getHistory scopes Slack user info cache by OAuth connection", async () => {
471
+ const workspaceA = makeOAuthConnection(
472
+ "conn-workspace-a",
473
+ "workspace-a",
474
+ "Workspace A Sender",
475
+ "America/Los_Angeles",
476
+ "Pacific Time",
477
+ -28800,
478
+ );
479
+ const workspaceB = makeOAuthConnection(
480
+ "conn-workspace-b",
481
+ "workspace-b",
482
+ "Workspace B Sender",
483
+ "Europe/London",
484
+ "Greenwich Mean Time",
485
+ 0,
486
+ );
487
+
488
+ const messagesA = await slackProvider.getHistory(
489
+ workspaceA,
490
+ "C_ACCOUNT_SCOPE",
491
+ );
492
+ const messagesB = await slackProvider.getHistory(
493
+ workspaceB,
494
+ "C_ACCOUNT_SCOPE",
495
+ );
496
+
497
+ expect(userInfoCalls).toContain("conn-workspace-a:USAME");
498
+ expect(userInfoCalls).toContain("conn-workspace-b:USAME");
499
+ expect(messagesA[0].sender).toEqual({
500
+ id: "USAME",
501
+ name: "Workspace A Sender",
502
+ });
503
+ expect(messagesA[0].metadata).toEqual({
504
+ actorTimezone: "America/Los_Angeles",
505
+ actorTimezoneLabel: "Pacific Time",
506
+ actorTimezoneOffsetSeconds: -28800,
507
+ });
508
+ expect(messagesB[0].sender).toEqual({
509
+ id: "USAME",
510
+ name: "Workspace B Sender",
511
+ });
512
+ expect(messagesB[0].metadata).toEqual({
513
+ actorTimezone: "Europe/London",
514
+ actorTimezoneLabel: "Greenwich Mean Time",
515
+ actorTimezoneOffsetSeconds: 0,
516
+ });
517
+ });
518
+
193
519
  test("getThreadReplies renders Slack user mentions for model-facing text without changing sender identity", async () => {
194
520
  const messages = await slackProvider.getThreadReplies!(
195
521
  undefined,
@@ -49,7 +49,11 @@ mock.module("../../../../contacts/contacts-write.js", () => ({
49
49
  upsertContactChannel: () => {},
50
50
  }));
51
51
 
52
- import { slackProvider, withSlackBotToken } from "../adapter.js";
52
+ import {
53
+ resolveSlackBotUserId,
54
+ slackProvider,
55
+ withSlackBotToken,
56
+ } from "../adapter.js";
53
57
 
54
58
  // ── fetch capture ───────────────────────────────────────────────────────────
55
59
 
@@ -104,6 +108,16 @@ function fakeSlackResponse(url: string): Record<string, unknown> {
104
108
  if (url.includes("/chat.postMessage")) {
105
109
  return { ok: true, ts: "1700000000.000100", channel: "C123" };
106
110
  }
111
+ if (url.includes("/bots.info")) {
112
+ const botId = new URL(url).searchParams.get("bot");
113
+ return {
114
+ ok: true,
115
+ bot: {
116
+ id: botId,
117
+ user_id: botId === "B_ASSISTANT" ? "U_BOT" : "U_OTHER_BOT",
118
+ },
119
+ };
120
+ }
107
121
  // Default envelope for any other method the adapter might call.
108
122
  return { ok: true };
109
123
  }
@@ -319,4 +333,23 @@ describe("Slack adapter token routing", () => {
319
333
  account: "workspace-b",
320
334
  });
321
335
  });
336
+
337
+ test("bot id resolver maps Slack bot ids through the bot token", async () => {
338
+ const resolved = await slackProvider.resolveConnection!();
339
+ expect(resolved).toBeUndefined();
340
+
341
+ const userId = await resolveSlackBotUserId(undefined, "B_ASSISTANT");
342
+ const repeatedUserId = await resolveSlackBotUserId(
343
+ undefined,
344
+ "B_ASSISTANT",
345
+ );
346
+
347
+ expect(userId).toBe("U_BOT");
348
+ expect(repeatedUserId).toBe("U_BOT");
349
+ const botsInfoCalls = captured.filter((c) => c.url.includes("/bots.info"));
350
+ expect(botsInfoCalls).toHaveLength(2);
351
+ for (const call of botsInfoCalls) {
352
+ expect(call.authorization).toBe(`Bearer ${BOT_TOKEN}`);
353
+ }
354
+ });
322
355
  });