@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,287 @@
1
+ /**
2
+ * Tests that the in-flight activity status text emitted for `web_search`
3
+ * and `web_fetch` tool starts surfaces per-call detail (the query / domain)
4
+ * instead of a generic placeholder. Covers both the Anthropic native
5
+ * `server_tool_start` path and the non-native `tool_use` path.
6
+ */
7
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
8
+
9
+ // ── Mock platform (must precede imports that read it) ─────────────────────────
10
+ mock.module("../../util/logger.js", () => ({
11
+ getLogger: () =>
12
+ new Proxy({} as Record<string, unknown>, {
13
+ get: () => () => {},
14
+ }),
15
+ }));
16
+
17
+ mock.module("../../config/loader.js", () => ({
18
+ getConfig: () => ({
19
+ skills: {
20
+ entries: {},
21
+ load: { extraDirs: [], watch: false, watchDebounceMs: 0 },
22
+ install: { nodeManager: "npm" },
23
+ allowBundled: null,
24
+ remoteProviders: {
25
+ skillssh: { enabled: true },
26
+ clawhub: { enabled: true },
27
+ },
28
+ remotePolicy: {
29
+ blockSuspicious: true,
30
+ blockMalware: true,
31
+ maxSkillsShRisk: "medium",
32
+ },
33
+ },
34
+ }),
35
+ loadConfig: () => ({}),
36
+ }));
37
+
38
+ mock.module("../../memory/conversation-crud.js", () => ({
39
+ addMessage: () => ({ id: "mock-msg-id" }),
40
+ getMessageById: () => null,
41
+ updateMessageContent: () => {},
42
+ provenanceFromTrustContext: () => ({}),
43
+ }));
44
+
45
+ mock.module("../../memory/llm-request-log-store.js", () => ({
46
+ recordRequestLog: () => {},
47
+ backfillMessageIdOnLogs: () => {},
48
+ }));
49
+
50
+ // ── Imports (after mocks) ─────────────────────────────────────────────────────
51
+ import type {
52
+ EventHandlerDeps,
53
+ EventHandlerState,
54
+ } from "../conversation-agent-loop-handlers.js";
55
+ import {
56
+ createEventHandlerState,
57
+ dispatchAgentEvent,
58
+ formatFetchStatusText,
59
+ formatSearchStatusText,
60
+ } from "../conversation-agent-loop-handlers.js";
61
+ import type { ServerMessage } from "../message-protocol.js";
62
+
63
+ // ── Helpers ───────────────────────────────────────────────────────────────────
64
+
65
+ interface ActivityStateCapture {
66
+ phase: string;
67
+ reason: string;
68
+ scope: string;
69
+ reqId: string;
70
+ statusText?: string;
71
+ }
72
+
73
+ function createCollectorDeps(): {
74
+ deps: EventHandlerDeps;
75
+ activityStates: ActivityStateCapture[];
76
+ } {
77
+ const activityStates: ActivityStateCapture[] = [];
78
+ const deps = {
79
+ ctx: {
80
+ conversationId: "conv-status-text",
81
+ provider: { name: "anthropic" },
82
+ traceEmitter: { emit: () => {} },
83
+ streamThinking: false,
84
+ emitActivityState: (
85
+ phase: string,
86
+ reason: string,
87
+ scope: string,
88
+ reqId: string,
89
+ statusText?: string,
90
+ ) => {
91
+ activityStates.push({ phase, reason, scope, reqId, statusText });
92
+ },
93
+ markWorkspaceTopLevelDirty: () => {},
94
+ currentTurnSurfaces: [],
95
+ } as unknown as EventHandlerDeps["ctx"],
96
+ onEvent: (_msg: ServerMessage) => {},
97
+ reqId: "req-status-text",
98
+ isFirstMessage: false,
99
+ shouldGenerateTitle: false,
100
+ rlog: new Proxy({} as Record<string, unknown>, {
101
+ get: () => () => {},
102
+ }) as unknown as EventHandlerDeps["rlog"],
103
+ turnChannelContext: {
104
+ userMessageChannel: "vellum",
105
+ assistantMessageChannel: "vellum",
106
+ } as EventHandlerDeps["turnChannelContext"],
107
+ turnInterfaceContext: {
108
+ userMessageInterface: "macos",
109
+ assistantMessageInterface: "macos",
110
+ } as EventHandlerDeps["turnInterfaceContext"],
111
+ } as EventHandlerDeps;
112
+ return { deps, activityStates };
113
+ }
114
+
115
+ // ── Pure helper tests ─────────────────────────────────────────────────────────
116
+
117
+ describe("formatSearchStatusText", () => {
118
+ test("surfaces the query in quotes", () => {
119
+ expect(formatSearchStatusText("web_search", "foo")).toBe(
120
+ 'Searching "foo"',
121
+ );
122
+ });
123
+
124
+ test("truncates queries longer than 60 chars with an ellipsis", () => {
125
+ const longQuery = "a".repeat(120);
126
+ const result = formatSearchStatusText("web_search", longQuery);
127
+ // 57 chars + "..." = 60 char body, wrapped in 'Searching "..."'
128
+ expect(result).toBe(`Searching "${"a".repeat(57)}..."`);
129
+ });
130
+
131
+ test("preserves 60-char queries as-is (no truncation at the boundary)", () => {
132
+ const sixty = "a".repeat(60);
133
+ expect(formatSearchStatusText("web_search", sixty)).toBe(
134
+ `Searching "${sixty}"`,
135
+ );
136
+ });
137
+
138
+ test("falls back to the generic phrasing when the query is empty", () => {
139
+ expect(formatSearchStatusText("web_search", "")).toBe("Searching the web");
140
+ expect(formatSearchStatusText("web_search", " ")).toBe(
141
+ "Searching the web",
142
+ );
143
+ });
144
+
145
+ test("emits 'Running <tool>' for non-web_search tools", () => {
146
+ expect(formatSearchStatusText("other_tool", "x")).toBe(
147
+ "Running other_tool",
148
+ );
149
+ });
150
+ });
151
+
152
+ describe("formatFetchStatusText", () => {
153
+ test("returns the domain when given a parseable URL", () => {
154
+ expect(formatFetchStatusText("https://example.com/path")).toBe(
155
+ "Reading example.com",
156
+ );
157
+ });
158
+
159
+ test("lowercases the host", () => {
160
+ expect(formatFetchStatusText("https://EXAMPLE.COM/x")).toBe(
161
+ "Reading example.com",
162
+ );
163
+ });
164
+
165
+ test("falls back when the URL is unparseable or missing", () => {
166
+ expect(formatFetchStatusText("not a url")).toBe("Reading a page");
167
+ expect(formatFetchStatusText(undefined)).toBe("Reading a page");
168
+ expect(formatFetchStatusText(42)).toBe("Reading a page");
169
+ });
170
+ });
171
+
172
+ // ── Dispatch-path tests ───────────────────────────────────────────────────────
173
+
174
+ describe("server_tool_start status text", () => {
175
+ let state: EventHandlerState;
176
+
177
+ beforeEach(() => {
178
+ state = createEventHandlerState();
179
+ });
180
+
181
+ test("native web_search emits 'Searching \"<query>\"'", async () => {
182
+ const { deps, activityStates } = createCollectorDeps();
183
+
184
+ await dispatchAgentEvent(state, deps, {
185
+ type: "server_tool_start",
186
+ name: "web_search",
187
+ toolUseId: "tu_native_q",
188
+ input: { query: "foo" },
189
+ });
190
+
191
+ const toolStart = activityStates.find((s) => s.reason === "tool_use_start");
192
+ expect(toolStart?.statusText).toBe('Searching "foo"');
193
+ });
194
+
195
+ test("native web_search with long query truncates", async () => {
196
+ const { deps, activityStates } = createCollectorDeps();
197
+ const longQuery = "a".repeat(120);
198
+
199
+ await dispatchAgentEvent(state, deps, {
200
+ type: "server_tool_start",
201
+ name: "web_search",
202
+ toolUseId: "tu_native_long",
203
+ input: { query: longQuery },
204
+ });
205
+
206
+ const toolStart = activityStates.find((s) => s.reason === "tool_use_start");
207
+ expect(toolStart?.statusText).toBe(`Searching "${"a".repeat(57)}..."`);
208
+ });
209
+
210
+ test("native web_search with missing query falls back to the generic phrasing", async () => {
211
+ const { deps, activityStates } = createCollectorDeps();
212
+
213
+ await dispatchAgentEvent(state, deps, {
214
+ type: "server_tool_start",
215
+ name: "web_search",
216
+ toolUseId: "tu_native_empty",
217
+ input: {},
218
+ });
219
+
220
+ const toolStart = activityStates.find((s) => s.reason === "tool_use_start");
221
+ expect(toolStart?.statusText).toBe("Searching the web");
222
+ });
223
+ });
224
+
225
+ describe("tool_use status text (non-native)", () => {
226
+ let state: EventHandlerState;
227
+
228
+ beforeEach(() => {
229
+ state = createEventHandlerState();
230
+ });
231
+
232
+ test("web_search emits 'Searching \"<query>\"'", async () => {
233
+ const { deps, activityStates } = createCollectorDeps();
234
+
235
+ await dispatchAgentEvent(state, deps, {
236
+ type: "tool_use",
237
+ id: "tu_ws",
238
+ name: "web_search",
239
+ input: { query: "bar" },
240
+ });
241
+
242
+ const toolStart = activityStates.find((s) => s.reason === "tool_use_start");
243
+ expect(toolStart?.statusText).toBe('Searching "bar"');
244
+ });
245
+
246
+ test("web_fetch emits 'Reading <domain>'", async () => {
247
+ const { deps, activityStates } = createCollectorDeps();
248
+
249
+ await dispatchAgentEvent(state, deps, {
250
+ type: "tool_use",
251
+ id: "tu_wf",
252
+ name: "web_fetch",
253
+ input: { url: "https://www.nytimes.com/article" },
254
+ });
255
+
256
+ const toolStart = activityStates.find((s) => s.reason === "tool_use_start");
257
+ expect(toolStart?.statusText).toBe("Reading www.nytimes.com");
258
+ });
259
+
260
+ test("web_fetch with malformed url falls back to a generic phrase", async () => {
261
+ const { deps, activityStates } = createCollectorDeps();
262
+
263
+ await dispatchAgentEvent(state, deps, {
264
+ type: "tool_use",
265
+ id: "tu_wf_bad",
266
+ name: "web_fetch",
267
+ input: { url: "not-a-url" },
268
+ });
269
+
270
+ const toolStart = activityStates.find((s) => s.reason === "tool_use_start");
271
+ expect(toolStart?.statusText).toBe("Reading a page");
272
+ });
273
+
274
+ test("unrelated tools keep the existing 'Running <friendly>' fallback", async () => {
275
+ const { deps, activityStates } = createCollectorDeps();
276
+
277
+ await dispatchAgentEvent(state, deps, {
278
+ type: "tool_use",
279
+ id: "tu_bash",
280
+ name: "bash",
281
+ input: { command: "ls" },
282
+ });
283
+
284
+ const toolStart = activityStates.find((s) => s.reason === "tool_use_start");
285
+ expect(toolStart?.statusText).toBe("Running command");
286
+ });
287
+ });
@@ -13,6 +13,7 @@ import type {
13
13
  TurnChannelContext,
14
14
  TurnInterfaceContext,
15
15
  } from "../channels/types.js";
16
+ import { getConfig } from "../config/loader.js";
16
17
  import { recordEstimate } from "../context/estimator-calibration.js";
17
18
  import { getCalibrationProviderKey } from "../context/token-estimator.js";
18
19
  import {
@@ -23,12 +24,15 @@ import {
23
24
  } from "../memory/conversation-crud.js";
24
25
  import {
25
26
  backfillMessageIdOnLogs,
27
+ buildProviderErrorResponsePayload,
26
28
  recordRequestLog,
29
+ setAgentLoopExitReasonOnLatestLog,
27
30
  } from "../memory/llm-request-log-store.js";
28
31
  import { backfillMemoryRecallLogMessageId } from "../memory/memory-recall-log-store.js";
29
32
  import { backfillMemoryV2ActivationMessageId } from "../memory/memory-v2-activation-log-store.js";
30
33
  import { getThreadTs } from "../memory/slack-thread-store.js";
31
34
  import {
35
+ formatSlackTimezoneLabel,
32
36
  type SlackMessageMetadata,
33
37
  writeSlackMetadata,
34
38
  } from "../messaging/providers/slack/message-metadata.js";
@@ -44,7 +48,9 @@ import type {
44
48
  import type { ContentBlock, ImageContent } from "../providers/types.js";
45
49
  import { isContextOverflowError } from "../providers/types.js";
46
50
  import { redactSecrets } from "../security/secret-scanner.js";
51
+ import { extractDomain } from "../tools/network/domain-normalize.js";
47
52
  import { ProviderError } from "../util/errors.js";
53
+ import { faviconUrlForDomain } from "../util/favicon.js";
48
54
  import { getLogger } from "../util/logger.js";
49
55
  import type { DirectiveRequest } from "./assistant-attachments.js";
50
56
  import {
@@ -58,7 +64,12 @@ import {
58
64
  isContextTooLarge,
59
65
  } from "./conversation-error.js";
60
66
  import { isProviderOrderingError } from "./conversation-slash.js";
67
+ import { resolveTurnTimezoneContext } from "./date-context.js";
61
68
  import type { ServerMessage } from "./message-protocol.js";
69
+ import type {
70
+ WebSearchMetadata,
71
+ WebSearchResultItem,
72
+ } from "./message-types/web-activity.js";
62
73
 
63
74
  const log = getLogger("agent-loop-handlers");
64
75
 
@@ -197,6 +208,10 @@ export interface EventHandlerState {
197
208
  currentTurnToolUseIds: string[];
198
209
  /** Wall-clock time (ms since epoch) when the agent loop turn started, used as the display timestamp for assistant messages. */
199
210
  turnStartedAt: number;
211
+ /** Wall-clock start time of native server tool calls, keyed by tool_use_id. */
212
+ readonly serverToolStartedAt: Map<string, number>;
213
+ /** Original input from server_tool_start, keyed by tool_use_id, so the complete handler can read the query. */
214
+ readonly serverToolInputs: Map<string, Record<string, unknown>>;
200
215
  }
201
216
 
202
217
  /** Immutable context shared across event handlers within a single agent loop run. */
@@ -255,6 +270,8 @@ export function createEventHandlerState(): EventHandlerState {
255
270
  toolRiskOutcomes: new Map(),
256
271
  currentTurnToolUseIds: [],
257
272
  turnStartedAt: Date.now(),
273
+ serverToolStartedAt: new Map(),
274
+ serverToolInputs: new Map(),
258
275
  };
259
276
  }
260
277
 
@@ -322,6 +339,68 @@ function friendlyToolName(name: string): string {
322
339
  return TOOL_FRIENDLY_NAMES[name] ?? name.replace(/_/g, " ");
323
340
  }
324
341
 
342
+ /**
343
+ * Status text shown to the client while a web search is in flight.
344
+ * Surfaces the actual query so the loading state is meaningful instead of
345
+ * a generic "Searching the web". Used for both Anthropic native server-tool
346
+ * starts and non-native `web_search` tool starts.
347
+ */
348
+ export function formatSearchStatusText(
349
+ toolName: string,
350
+ query: string,
351
+ ): string {
352
+ if (toolName !== "web_search") return `Running ${toolName}`;
353
+ const trimmed = query.trim();
354
+ if (!trimmed) return "Searching the web";
355
+ const truncated =
356
+ trimmed.length > 60 ? trimmed.slice(0, 57) + "..." : trimmed;
357
+ return `Searching "${truncated}"`;
358
+ }
359
+
360
+ /**
361
+ * Status text shown to the client while a web fetch is in flight.
362
+ * Surfaces the domain so users can tell what page is being read.
363
+ */
364
+ export function formatFetchStatusText(url: unknown): string {
365
+ if (typeof url !== "string") return "Reading a page";
366
+ const domain = extractDomain(url);
367
+ if (!domain) return "Reading a page";
368
+ return `Reading ${domain}`;
369
+ }
370
+
371
+ function computeToolUseStatusText(
372
+ name: string,
373
+ input: Record<string, unknown>,
374
+ ): string {
375
+ if (name === "web_search") {
376
+ const query = typeof input.query === "string" ? input.query : "";
377
+ return formatSearchStatusText("web_search", query);
378
+ }
379
+ if (name === "web_fetch") {
380
+ return formatFetchStatusText(input.url);
381
+ }
382
+ if (
383
+ name === "skill_execute" &&
384
+ typeof input.activity === "string" &&
385
+ input.activity.length > 0
386
+ ) {
387
+ return input.activity;
388
+ }
389
+ return `Running ${friendlyToolName(name)}`;
390
+ }
391
+
392
+ function resolveAssistantReplyTimestampTimezone(
393
+ ctx: AgentLoopConversationContext,
394
+ ): string {
395
+ const config = getConfig();
396
+ return resolveTurnTimezoneContext({
397
+ configuredUserTimeZone: config.ui?.userTimezone ?? null,
398
+ clientTimezone: ctx.clientTimezone ?? null,
399
+ detectedTimezone: config.ui?.detectedTimezone ?? null,
400
+ hostTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
401
+ }).effectiveTimezone;
402
+ }
403
+
325
404
  // ── Individual Handlers ──────────────────────────────────────────────
326
405
 
327
406
  function handleTextDelta(
@@ -403,12 +482,7 @@ export function handleToolUse(
403
482
  state.toolCallTimestamps.set(event.id, { startedAt: Date.now() });
404
483
  state.currentToolUseId = event.id;
405
484
  state.currentTurnToolUseIds.push(event.id);
406
- const statusText =
407
- event.name === "skill_execute" &&
408
- typeof event.input.activity === "string" &&
409
- event.input.activity.length > 0
410
- ? event.input.activity
411
- : `Running ${friendlyToolName(event.name)}`;
485
+ const statusText = computeToolUseStatusText(event.name, event.input);
412
486
  deps.ctx.emitActivityState(
413
487
  "tool_running",
414
488
  "tool_use_start",
@@ -675,6 +749,7 @@ export function handleToolResult(
675
749
  approvalMode: event.approvalMode,
676
750
  approvalReason: event.approvalReason,
677
751
  riskThreshold: event.riskThreshold,
752
+ activityMetadata: event.activityMetadata,
678
753
  });
679
754
  }
680
755
 
@@ -983,11 +1058,20 @@ export async function handleMessageComplete(
983
1058
  const channelId = deps.ctx.trustContext?.requesterChatId;
984
1059
  if (channelId) {
985
1060
  const threadTs = getThreadTs(deps.ctx.conversationId);
1061
+ const timestampTimezone = resolveAssistantReplyTimestampTimezone(
1062
+ deps.ctx,
1063
+ );
1064
+ const timestampTimezoneLabel = formatSlackTimezoneLabel(
1065
+ timestampTimezone,
1066
+ { nowMs: state.turnStartedAt },
1067
+ );
986
1068
  const partialSlackMeta: Partial<SlackMessageMetadata> = {
987
1069
  source: "slack",
988
1070
  eventKind: "message",
989
1071
  channelId,
990
1072
  ...(threadTs ? { threadTs } : {}),
1073
+ timestampTimezone,
1074
+ ...(timestampTimezoneLabel ? { timestampTimezoneLabel } : {}),
991
1075
  };
992
1076
  assistantChannelMetadata.slackMeta = writeSlackMetadata(
993
1077
  // `channelTs` is filled in by the post-send reconciliation step in
@@ -1166,6 +1250,52 @@ function handleUsage(
1166
1250
  state.llmCallStartedEmitted = false;
1167
1251
  }
1168
1252
 
1253
+ /**
1254
+ * Persist a provider-rejected LLM call as an `llm_request_logs` row.
1255
+ *
1256
+ * Mirrors `handleUsage`'s recording side-effect for the failure path: the
1257
+ * loop only reaches the success branch (and emits `usage`) when the
1258
+ * provider returns a response, so without this handler a rejected call
1259
+ * leaves nothing in the inspector — only a pino line saying "The AI
1260
+ * provider rejected the request." The row's `messageId` is left null
1261
+ * here and linked via one of two backfill paths, depending on how the
1262
+ * turn unwinds:
1263
+ *
1264
+ * - Multi-call turn where a later call also produces a real assistant
1265
+ * response: `handleMessageComplete` -> `backfillMessageIdOnLogs`
1266
+ * sweeps this row with the rest, same as a successful-call row.
1267
+ * - Pure provider-failure turn (no real assistant response): the
1268
+ * synthetic error-message branch in `conversation-agent-loop.ts`
1269
+ * persists a stand-in assistant message and calls
1270
+ * `backfillMessageIdOnLogs` itself, since `message_complete` is
1271
+ * never emitted on that path. Closing the orphan window inside the
1272
+ * same synchronous turn prevents a later turn's sweep from wrong-
1273
+ * attaching this row to an unrelated assistant message.
1274
+ *
1275
+ * Failures inside the recording itself are logged and swallowed — this
1276
+ * mirrors `handleUsage`'s non-fatal stance so a DB hiccup never escalates
1277
+ * a provider rejection into a dispatcher-level throw.
1278
+ */
1279
+ function handleProviderError(
1280
+ deps: EventHandlerDeps,
1281
+ event: Extract<AgentEvent, { type: "provider_error" }>,
1282
+ ): void {
1283
+ try {
1284
+ recordRequestLog(
1285
+ deps.ctx.conversationId,
1286
+ JSON.stringify(event.rawRequest),
1287
+ JSON.stringify(buildProviderErrorResponsePayload(event.error)),
1288
+ undefined,
1289
+ event.actualProvider,
1290
+ );
1291
+ } catch (err) {
1292
+ deps.rlog.warn(
1293
+ { err },
1294
+ "Failed to persist provider-error LLM request log (non-fatal)",
1295
+ );
1296
+ }
1297
+ }
1298
+
1169
1299
  // ── Dispatcher ───────────────────────────────────────────────────────
1170
1300
 
1171
1301
  /** Routes an AgentEvent to the appropriate handler. */
@@ -1198,10 +1328,9 @@ export async function dispatchAgentEvent(
1198
1328
  handleToolResult(state, deps, event);
1199
1329
  break;
1200
1330
  case "server_tool_start": {
1201
- const friendlyNames: Record<string, string> = {
1202
- web_search: "Searching the web",
1203
- };
1204
- const statusText = friendlyNames[event.name] ?? `Running ${event.name}`;
1331
+ const query =
1332
+ typeof event.input.query === "string" ? event.input.query : "";
1333
+ const statusText = formatSearchStatusText(event.name, query);
1205
1334
  deps.ctx.emitActivityState(
1206
1335
  "tool_running",
1207
1336
  "tool_use_start",
@@ -1209,6 +1338,8 @@ export async function dispatchAgentEvent(
1209
1338
  deps.reqId,
1210
1339
  statusText,
1211
1340
  );
1341
+ state.serverToolStartedAt.set(event.toolUseId, Date.now());
1342
+ state.serverToolInputs.set(event.toolUseId, event.input);
1212
1343
  deps.onEvent({
1213
1344
  type: "tool_use_start",
1214
1345
  toolName: event.name,
@@ -1227,19 +1358,65 @@ export async function dispatchAgentEvent(
1227
1358
  "Thinking",
1228
1359
  );
1229
1360
 
1230
- // Format web search results into a human-readable string for the client.
1231
- let resultText = "";
1232
- if (Array.isArray(event.content) && event.content.length > 0) {
1233
- resultText = (event.content as unknown[])
1234
- .filter(
1235
- (r): r is { type: string; title: string; url: string } =>
1236
- typeof r === "object" &&
1237
- r != null &&
1238
- (r as { type?: string }).type === "web_search_result",
1239
- )
1240
- .map((r) => `${r.title}\n${r.url}`)
1241
- .join("\n\n");
1242
- }
1361
+ // Prefer `resolvedInput` (Anthropic's accumulated server-tool input,
1362
+ // populated on content_block_stop) over the input captured at
1363
+ // server_tool_start, which is `{}` on Anthropic until the deltas land.
1364
+ const inputForCall =
1365
+ event.resolvedInput ?? state.serverToolInputs.get(event.toolUseId);
1366
+ const query =
1367
+ typeof inputForCall?.query === "string" ? inputForCall.query : "";
1368
+ const startedAt =
1369
+ state.serverToolStartedAt.get(event.toolUseId) ?? Date.now();
1370
+ const durationMs = Date.now() - startedAt;
1371
+ state.serverToolStartedAt.delete(event.toolUseId);
1372
+ state.serverToolInputs.delete(event.toolUseId);
1373
+
1374
+ const rawBlocks = Array.isArray(event.content) ? event.content : [];
1375
+ const results: WebSearchResultItem[] = rawBlocks
1376
+ .filter(
1377
+ (r): r is { type: string; title: string; url: string } =>
1378
+ typeof r === "object" &&
1379
+ r != null &&
1380
+ (r as { type?: string }).type === "web_search_result",
1381
+ )
1382
+ .map((r, i) => {
1383
+ const domain = extractDomain(r.url);
1384
+ return {
1385
+ rank: i + 1,
1386
+ title: r.title,
1387
+ url: r.url,
1388
+ domain,
1389
+ faviconUrl: faviconUrlForDomain(domain),
1390
+ // snippet intentionally absent — Anthropic native content is encrypted/opaque
1391
+ };
1392
+ });
1393
+
1394
+ // Only Anthropic produces structured `web_search_tool_result` blocks
1395
+ // that map cleanly onto `WebSearchMetadata` (provider-tagged
1396
+ // "anthropic-native"). Other providers (e.g. OpenAI's responses
1397
+ // `web_search_call`) share this event channel but their results are
1398
+ // woven into the text stream — emitting "anthropic-native" metadata
1399
+ // for them would mis-label the provider and ship empty results.
1400
+ const isAnthropicNative = deps.ctx.provider.name === "anthropic";
1401
+
1402
+ const errorMessage = event.isError
1403
+ ? (event.errorMessage ?? event.errorCode ?? "Search failed")
1404
+ : undefined;
1405
+
1406
+ const metadata: WebSearchMetadata | undefined = isAnthropicNative
1407
+ ? {
1408
+ query,
1409
+ provider: "anthropic-native",
1410
+ resultCount: results.length,
1411
+ durationMs,
1412
+ results,
1413
+ ...(errorMessage ? { errorMessage } : {}),
1414
+ }
1415
+ : undefined;
1416
+
1417
+ const resultText = results
1418
+ .map((r) => `${r.title}\n${r.url}`)
1419
+ .join("\n\n");
1243
1420
 
1244
1421
  deps.onEvent({
1245
1422
  type: "tool_result",
@@ -1248,18 +1425,49 @@ export async function dispatchAgentEvent(
1248
1425
  isError: event.isError,
1249
1426
  conversationId: deps.ctx.conversationId,
1250
1427
  toolUseId: event.toolUseId,
1428
+ ...(metadata ? { activityMetadata: { webSearch: metadata } } : {}),
1251
1429
  });
1252
1430
  break;
1253
1431
  }
1254
1432
  case "error":
1255
1433
  handleError(state, deps, event);
1256
1434
  break;
1435
+ case "provider_error":
1436
+ handleProviderError(deps, event);
1437
+ break;
1257
1438
  case "message_complete":
1258
1439
  await handleMessageComplete(state, deps, event);
1259
1440
  break;
1260
1441
  case "usage":
1261
1442
  handleUsage(state, deps, event);
1262
1443
  break;
1444
+ case "agent_loop_exit":
1445
+ // Stamp the exit reason onto the most-recent llm_request_logs
1446
+ // row for this conversation. The final `usage` event of the run
1447
+ // lands its row immediately before this event arrives (in the
1448
+ // normal-dispatch path; the wake path handles ordering
1449
+ // explicitly via `pendingExitReason`).
1450
+ //
1451
+ // Wrapped in try/catch so a DB hiccup here can't tear down the
1452
+ // surrounding dispatch — the outer try/catch already swallows
1453
+ // errors, but logging here gives the diagnosis hook a clear
1454
+ // attribution to the exit handler specifically.
1455
+ try {
1456
+ setAgentLoopExitReasonOnLatestLog(
1457
+ deps.ctx.conversationId,
1458
+ event.reason,
1459
+ );
1460
+ } catch (err) {
1461
+ log.warn(
1462
+ {
1463
+ err,
1464
+ conversationId: deps.ctx.conversationId,
1465
+ reason: event.reason,
1466
+ },
1467
+ "Failed to persist agent_loop_exit_reason (non-fatal)",
1468
+ );
1469
+ }
1470
+ break;
1263
1471
  }
1264
1472
  } catch (err) {
1265
1473
  log.error(