@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
@@ -5,13 +5,16 @@ import {
5
5
  } from "node:https";
6
6
  import { Readable } from "node:stream";
7
7
 
8
+ import type { WebFetchMetadata } from "../../daemon/message-types/web-activity.js";
8
9
  import { RiskLevel } from "../../permissions/types.js";
9
10
  import type { ToolDefinition } from "../../providers/types.js";
10
11
  import { wrapUntrustedContent } from "../../security/untrusted-content.js";
12
+ import { faviconUrlForDomain } from "../../util/favicon.js";
11
13
  import { getLogger } from "../../util/logger.js";
12
14
  import { safeStringSlice } from "../../util/unicode.js";
13
15
  import { registerTool } from "../registry.js";
14
16
  import type { Tool, ToolContext, ToolExecutionResult } from "../types.js";
17
+ import { extractDomain } from "./domain-normalize.js";
15
18
  import {
16
19
  buildHostHeader,
17
20
  isIPv4,
@@ -351,6 +354,26 @@ function extractFirstMatch(
351
354
  return value || undefined;
352
355
  }
353
356
 
357
+ const MAX_TITLE_CHARS = 200;
358
+
359
+ /**
360
+ * Parse the first HTML `<title>` element from a response body.
361
+ *
362
+ * Used to populate {@link WebFetchMetadata.title}. Returns `undefined`
363
+ * when no `<title>` is present. The result is HTML-entity-decoded and
364
+ * capped at {@link MAX_TITLE_CHARS} characters so client UIs never have
365
+ * to truncate.
366
+ */
367
+ function parseHtmlTitle(html: string): string | undefined {
368
+ // Bound the search to the first 200KB to avoid scanning huge bodies.
369
+ const searchRegion = safeStringSlice(html, 0, 200_000);
370
+ const match = /<title[^>]*>([^<]+)<\/title>/i.exec(searchRegion);
371
+ if (!match) return undefined;
372
+ const decoded = decodeHtmlEntities(match[1]).trim();
373
+ if (!decoded) return undefined;
374
+ return safeStringSlice(decoded, 0, MAX_TITLE_CHARS);
375
+ }
376
+
354
377
  function extractHtmlMetadata(html: string): {
355
378
  title?: string;
356
379
  description?: string;
@@ -518,15 +541,62 @@ export async function executeWebFetch(
518
541
  input: Record<string, unknown>,
519
542
  options?: ExecuteWebFetchOptions,
520
543
  ): Promise<ToolExecutionResult> {
521
- const parsedUrl = parseUrl(input.url);
522
- if (!parsedUrl) {
544
+ const startedAt = Date.now();
545
+
546
+ /**
547
+ * Build a {@link ToolExecutionResult} for an early-exit error path (bad
548
+ * input, blocked target, timeout, bad content-type, HTTP error, ...).
549
+ * Always attaches structured {@link WebFetchMetadata} so client UIs can
550
+ * still render failed visits.
551
+ */
552
+ const buildErrorResult = (
553
+ errorMessage: string,
554
+ meta: {
555
+ url: string;
556
+ finalUrl?: string;
557
+ status?: number;
558
+ contentType?: string;
559
+ redirectCount?: number;
560
+ },
561
+ ): ToolExecutionResult => {
562
+ const safeUrl = sanitizeUrlStringForOutput(meta.url);
563
+ const safeFinalUrl = meta.finalUrl
564
+ ? sanitizeUrlStringForOutput(meta.finalUrl)
565
+ : safeUrl;
566
+ const domain = extractDomain(safeFinalUrl);
523
567
  return {
524
- content: "Error: url is required and must be a valid HTTP(S) URL",
568
+ content: errorMessage,
525
569
  isError: true,
570
+ activityMetadata: {
571
+ webFetch: {
572
+ url: safeUrl,
573
+ finalUrl: safeFinalUrl,
574
+ status: meta.status ?? 0,
575
+ contentType: meta.contentType,
576
+ byteCount: 0,
577
+ charCount: 0,
578
+ truncated: false,
579
+ domain,
580
+ faviconUrl: faviconUrlForDomain(domain),
581
+ redirectCount: meta.redirectCount ?? 0,
582
+ durationMs: Date.now() - startedAt,
583
+ errorMessage,
584
+ },
585
+ },
526
586
  };
587
+ };
588
+
589
+ const parsedUrl = parseUrl(input.url);
590
+ if (!parsedUrl) {
591
+ return buildErrorResult(
592
+ "Error: url is required and must be a valid HTTP(S) URL",
593
+ { url: typeof input.url === "string" ? input.url : "" },
594
+ );
527
595
  }
528
596
  if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
529
- return { content: "Error: url must use http or https", isError: true };
597
+ return buildErrorResult("Error: url must use http or https", {
598
+ url: parsedUrl.href,
599
+ });
530
600
  }
531
601
 
532
602
  const allowPrivateNetwork = input.allow_private_network === true;
@@ -534,10 +604,10 @@ export async function executeWebFetch(
534
604
  const requestExecutor = options?.requestExecutor ?? defaultRequestExecutor;
535
605
 
536
606
  if (!allowPrivateNetwork && isPrivateOrLocalHost(parsedUrl.hostname)) {
537
- return {
538
- content: `Error: Refusing to fetch local/private network target (${parsedUrl.hostname}). Set allow_private_network=true if you explicitly need it.`,
539
- isError: true,
540
- };
607
+ return buildErrorResult(
608
+ `Error: Refusing to fetch local/private network target (${parsedUrl.hostname}). Set allow_private_network=true if you explicitly need it.`,
609
+ { url: parsedUrl.href },
610
+ );
541
611
  }
542
612
  const timeoutSeconds = clampInteger(
543
613
  input.timeout_seconds,
@@ -576,6 +646,9 @@ export async function executeWebFetch(
576
646
  }
577
647
  }
578
648
 
649
+ let currentUrl = new URL(requestedUrl);
650
+ let redirectCount = 0;
651
+
579
652
  try {
580
653
  log.debug(
581
654
  { url: safeRequestedUrl, timeoutSeconds, maxChars, startIndex, rawMode },
@@ -591,8 +664,6 @@ export async function executeWebFetch(
591
664
  "VellumAssistant/1.0 (+https://vellum.ai)",
592
665
  };
593
666
 
594
- let currentUrl = new URL(requestedUrl);
595
- let redirectCount = 0;
596
667
  let response: Response | null = null;
597
668
  let currentResolvedAddresses: string[] | undefined;
598
669
 
@@ -606,16 +677,16 @@ export async function executeWebFetch(
606
677
  controller.signal,
607
678
  );
608
679
  if (resolution.blockedAddress) {
609
- return {
610
- content: `Error: Refusing to fetch target (${currentUrl.hostname}) because it resolves to local/private network address ${resolution.blockedAddress}. Set allow_private_network=true if you explicitly need it.`,
611
- isError: true,
612
- };
680
+ return buildErrorResult(
681
+ `Error: Refusing to fetch target (${currentUrl.hostname}) because it resolves to local/private network address ${resolution.blockedAddress}. Set allow_private_network=true if you explicitly need it.`,
682
+ { url: requestedUrl, finalUrl: currentUrl.href, redirectCount },
683
+ );
613
684
  }
614
685
  if (resolution.addresses.length === 0) {
615
- return {
616
- content: `Error: Unable to resolve host "${currentUrl.hostname}" while fetching ${safeRequestedUrl}`,
617
- isError: true,
618
- };
686
+ return buildErrorResult(
687
+ `Error: Unable to resolve host "${currentUrl.hostname}" while fetching ${safeRequestedUrl}`,
688
+ { url: requestedUrl, finalUrl: currentUrl.href, redirectCount },
689
+ );
619
690
  }
620
691
  currentResolvedAddresses = resolution.addresses;
621
692
  }
@@ -650,10 +721,10 @@ export async function executeWebFetch(
650
721
  currentResolvedAddresses = undefined;
651
722
 
652
723
  if (!response) {
653
- return {
654
- content: "Error: Web fetch failed: no response returned",
655
- isError: true,
656
- };
724
+ return buildErrorResult(
725
+ "Error: Web fetch failed: no response returned",
726
+ { url: requestedUrl, finalUrl: currentUrl.href, redirectCount },
727
+ );
657
728
  }
658
729
 
659
730
  const location = response.headers.get("location");
@@ -662,10 +733,15 @@ export async function executeWebFetch(
662
733
  if (!isRedirect) break;
663
734
 
664
735
  if (redirectCount >= MAX_REDIRECTS) {
665
- return {
666
- content: `Error: Too many redirects (>${MAX_REDIRECTS}) while fetching ${safeRequestedUrl}`,
667
- isError: true,
668
- };
736
+ return buildErrorResult(
737
+ `Error: Too many redirects (>${MAX_REDIRECTS}) while fetching ${safeRequestedUrl}`,
738
+ {
739
+ url: requestedUrl,
740
+ finalUrl: currentUrl.href,
741
+ status: response.status,
742
+ redirectCount,
743
+ },
744
+ );
669
745
  }
670
746
 
671
747
  let nextUrl: URL;
@@ -677,24 +753,39 @@ export async function executeWebFetch(
677
753
  currentUrl,
678
754
  );
679
755
  const safeCurrentUrl = sanitizeUrlForOutput(currentUrl);
680
- return {
681
- content: `Error: Invalid redirect location "${safeLocation}" received from ${safeCurrentUrl}`,
682
- isError: true,
683
- };
756
+ return buildErrorResult(
757
+ `Error: Invalid redirect location "${safeLocation}" received from ${safeCurrentUrl}`,
758
+ {
759
+ url: requestedUrl,
760
+ finalUrl: currentUrl.href,
761
+ status: response.status,
762
+ redirectCount,
763
+ },
764
+ );
684
765
  }
685
766
 
686
767
  if (nextUrl.protocol !== "http:" && nextUrl.protocol !== "https:") {
687
- return {
688
- content: `Error: Refusing redirect to unsupported protocol "${nextUrl.protocol}"`,
689
- isError: true,
690
- };
768
+ return buildErrorResult(
769
+ `Error: Refusing redirect to unsupported protocol "${nextUrl.protocol}"`,
770
+ {
771
+ url: requestedUrl,
772
+ finalUrl: currentUrl.href,
773
+ status: response.status,
774
+ redirectCount,
775
+ },
776
+ );
691
777
  }
692
778
 
693
779
  if (!allowPrivateNetwork && isPrivateOrLocalHost(nextUrl.hostname)) {
694
- return {
695
- content: `Error: Refusing redirect to local/private network target (${nextUrl.hostname}). Set allow_private_network=true if you explicitly need it.`,
696
- isError: true,
697
- };
780
+ return buildErrorResult(
781
+ `Error: Refusing redirect to local/private network target (${nextUrl.hostname}). Set allow_private_network=true if you explicitly need it.`,
782
+ {
783
+ url: requestedUrl,
784
+ finalUrl: currentUrl.href,
785
+ status: response.status,
786
+ redirectCount,
787
+ },
788
+ );
698
789
  }
699
790
  if (!allowPrivateNetwork) {
700
791
  const resolution = await withAbortSignal(
@@ -706,17 +797,27 @@ export async function executeWebFetch(
706
797
  controller.signal,
707
798
  );
708
799
  if (resolution.blockedAddress) {
709
- return {
710
- content: `Error: Refusing redirect to target (${nextUrl.hostname}) because it resolves to local/private network address ${resolution.blockedAddress}. Set allow_private_network=true if you explicitly need it.`,
711
- isError: true,
712
- };
800
+ return buildErrorResult(
801
+ `Error: Refusing redirect to target (${nextUrl.hostname}) because it resolves to local/private network address ${resolution.blockedAddress}. Set allow_private_network=true if you explicitly need it.`,
802
+ {
803
+ url: requestedUrl,
804
+ finalUrl: currentUrl.href,
805
+ status: response.status,
806
+ redirectCount,
807
+ },
808
+ );
713
809
  }
714
810
  if (resolution.addresses.length === 0) {
715
811
  const safeCurrentUrl = sanitizeUrlForOutput(currentUrl);
716
- return {
717
- content: `Error: Unable to resolve redirect host "${nextUrl.hostname}" from ${safeCurrentUrl}`,
718
- isError: true,
719
- };
812
+ return buildErrorResult(
813
+ `Error: Unable to resolve redirect host "${nextUrl.hostname}" from ${safeCurrentUrl}`,
814
+ {
815
+ url: requestedUrl,
816
+ finalUrl: currentUrl.href,
817
+ status: response.status,
818
+ redirectCount,
819
+ },
820
+ );
720
821
  }
721
822
  currentResolvedAddresses = resolution.addresses;
722
823
  }
@@ -726,18 +827,25 @@ export async function executeWebFetch(
726
827
  }
727
828
 
728
829
  if (!response) {
729
- return {
730
- content: "Error: Web fetch failed: no response returned",
731
- isError: true,
732
- };
830
+ return buildErrorResult("Error: Web fetch failed: no response returned", {
831
+ url: requestedUrl,
832
+ finalUrl: currentUrl.href,
833
+ redirectCount,
834
+ });
733
835
  }
734
836
 
735
837
  const contentType = response.headers.get("content-type") ?? "";
736
838
  if (!isTextLikeContentType(contentType)) {
737
- return {
738
- content: `Error: Unsupported content type "${contentType || "unknown"}". web_fetch only supports text-like responses.`,
739
- isError: true,
740
- };
839
+ return buildErrorResult(
840
+ `Error: Unsupported content type "${contentType || "unknown"}". web_fetch only supports text-like responses.`,
841
+ {
842
+ url: requestedUrl,
843
+ finalUrl: currentUrl.href,
844
+ status: response.status,
845
+ contentType,
846
+ redirectCount,
847
+ },
848
+ );
741
849
  }
742
850
 
743
851
  const body = await readResponseText(response, MAX_DOWNLOAD_BYTES);
@@ -778,9 +886,20 @@ export async function executeWebFetch(
778
886
  `start_index (${startIndex}) exceeded available content length (${processed.length}).`,
779
887
  );
780
888
  }
781
- if (html && !rawMode && processed.length < 200) {
889
+ // Detect likely JS-rendered SPAs: text is absolutely tiny, or a non-trivial
890
+ // HTML payload compresses to almost nothing (a shell page whose meaningful
891
+ // content is painted after fetch + document.body rewrite).
892
+ const lowAbsolute = processed.length < 200;
893
+ const lowRatio =
894
+ body.bytesRead >= 10_000 && processed.length / body.bytesRead < 0.05;
895
+ const mayRequireJavaScript = html && !rawMode && (lowAbsolute || lowRatio);
896
+ if (mayRequireJavaScript) {
897
+ const pct =
898
+ body.bytesRead > 0
899
+ ? ((processed.length / body.bytesRead) * 100).toFixed(1)
900
+ : "0";
782
901
  notices.push(
783
- `Extracted text content is very short (${processed.length} characters). The page may require JavaScript rendering for full content.`,
902
+ `Extracted only ${processed.length} chars of text from ${body.bytesRead} bytes of HTML (${pct}%). Content may be JavaScript-rendered the static fetch likely missed dynamically injected content.`,
784
903
  );
785
904
  }
786
905
 
@@ -803,11 +922,32 @@ export async function executeWebFetch(
803
922
  markdownTokens,
804
923
  });
805
924
 
925
+ const truncated = body.truncated || safeEnd < processed.length;
926
+ const parsedTitle = html ? parseHtmlTitle(body.text) : undefined;
927
+ const finalDomain = extractDomain(currentUrl.href);
928
+ const meta: WebFetchMetadata = {
929
+ url: safeRequestedUrl,
930
+ finalUrl: sanitizeUrlForOutput(currentUrl),
931
+ status: response.status,
932
+ contentType: contentType || undefined,
933
+ byteCount: body.bytesRead,
934
+ charCount: sliced.length,
935
+ truncated,
936
+ title: parsedTitle,
937
+ domain: finalDomain,
938
+ faviconUrl: faviconUrlForDomain(finalDomain),
939
+ redirectCount,
940
+ durationMs: Date.now() - startedAt,
941
+ mayRequireJavaScript: mayRequireJavaScript || undefined,
942
+ };
943
+
806
944
  if (!response.ok) {
945
+ const errorMessage = `Error: HTTP ${response.status}`;
807
946
  return {
808
- content: `Error: HTTP ${response.status}\n\n${content}`,
947
+ content: `${errorMessage}\n\n${content}`,
809
948
  isError: true,
810
949
  status: notices.length > 0 ? notices.join("\n") : undefined,
950
+ activityMetadata: { webFetch: { ...meta, errorMessage } },
811
951
  };
812
952
  }
813
953
 
@@ -815,21 +955,30 @@ export async function executeWebFetch(
815
955
  content,
816
956
  isError: false,
817
957
  status: notices.length > 0 ? notices.join("\n") : undefined,
958
+ activityMetadata: { webFetch: meta },
818
959
  };
819
960
  } catch (err) {
820
961
  if (err instanceof Error && err.name === "AbortError") {
821
962
  if (externalSignal?.aborted) {
822
- return { content: "Error: web fetch was cancelled", isError: true };
963
+ return buildErrorResult("Error: web fetch was cancelled", {
964
+ url: requestedUrl,
965
+ finalUrl: currentUrl.href,
966
+ redirectCount,
967
+ });
823
968
  }
824
- return {
825
- content: `Error: web fetch timed out after ${timeoutSeconds}s`,
826
- isError: true,
827
- };
969
+ return buildErrorResult(
970
+ `Error: web fetch timed out after ${timeoutSeconds}s`,
971
+ { url: requestedUrl, finalUrl: currentUrl.href, redirectCount },
972
+ );
828
973
  }
829
974
 
830
975
  const msg = err instanceof Error ? err.message : String(err);
831
976
  log.error({ err, url: safeRequestedUrl }, "Web fetch failed");
832
- return { content: `Error: Web fetch failed: ${msg}`, isError: true };
977
+ return buildErrorResult(`Error: Web fetch failed: ${msg}`, {
978
+ url: requestedUrl,
979
+ finalUrl: currentUrl.href,
980
+ redirectCount,
981
+ });
833
982
  } finally {
834
983
  clearTimeout(timeoutHandle);
835
984
  externalSignal?.removeEventListener("abort", onExternalAbort);
@@ -839,7 +988,7 @@ export async function executeWebFetch(
839
988
  class WebFetchTool implements Tool {
840
989
  name = "web_fetch";
841
990
  description =
842
- "Fetch a webpage and return LLM-friendly extracted text with metadata. Use this after web_search when you need to read a specific result.";
991
+ "Fetch a webpage and return LLM-friendly extracted text with metadata. Use this after web_search when you need to read a specific result. To find pages on a site without guessing slugs, fetch /sitemap.xml first — it has ground-truth paths and works even when pages are JS-rendered.";
843
992
  category = "network";
844
993
  defaultRiskLevel = RiskLevel.Low;
845
994