@vellumai/assistant 0.8.7 → 0.8.8

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 (387) hide show
  1. package/Dockerfile +20 -4
  2. package/docker-entrypoint.sh +4 -2
  3. package/docker-init-apt-root.sh +3 -1
  4. package/docker-kata-apt-env.sh +3 -1
  5. package/docker-kata-runtime-family.sh +12 -0
  6. package/docs/architecture/memory.md +1 -1
  7. package/docs/plugins.md +75 -79
  8. package/examples/plugins/echo/README.md +6 -12
  9. package/examples/plugins/echo/register.ts +0 -41
  10. package/node_modules/@vellumai/skill-host-contracts/src/server-message.ts +3 -3
  11. package/openapi.yaml +3381 -348
  12. package/package.json +1 -1
  13. package/scripts/generate-openapi.ts +68 -41
  14. package/src/__tests__/agent-loop-exit-reason.test.ts +34 -39
  15. package/src/__tests__/agent-loop-provider-error-recording.test.ts +1 -1
  16. package/src/__tests__/agent-loop.test.ts +37 -87
  17. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  18. package/src/__tests__/annotate-activity-metadata.test.ts +262 -0
  19. package/src/__tests__/annotate-risk-options.test.ts +2 -3
  20. package/src/__tests__/anthropic-provider.test.ts +95 -2
  21. package/src/__tests__/assistant-event-hub.test.ts +25 -0
  22. package/src/__tests__/assistant-events-sse-shed.test.ts +8 -0
  23. package/src/__tests__/{conversation-stream-state.test.ts → assistant-stream-state.test.ts} +252 -91
  24. package/src/__tests__/auth-fallback-events-store.test.ts +116 -0
  25. package/src/__tests__/background-workers-disk-pressure.test.ts +6 -0
  26. package/src/__tests__/btw-routes.test.ts +62 -3
  27. package/src/__tests__/build-persisted-content.test.ts +184 -0
  28. package/src/__tests__/catalog-files.test.ts +1 -1
  29. package/src/__tests__/clawhub-files.test.ts +1 -1
  30. package/src/__tests__/compaction-pipeline.test.ts +1 -1
  31. package/src/__tests__/compaction.benchmark.test.ts +0 -30
  32. package/src/__tests__/config-watcher.test.ts +1 -1
  33. package/src/__tests__/conversation-abort-tool-results.test.ts +57 -19
  34. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +6 -2
  35. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +10 -4
  36. package/src/__tests__/conversation-agent-loop-overflow.test.ts +313 -1136
  37. package/src/__tests__/conversation-agent-loop.test.ts +596 -1616
  38. package/src/__tests__/conversation-analysis-routes.test.ts +6 -0
  39. package/src/__tests__/conversation-history-web-search.test.ts +11 -1
  40. package/src/__tests__/conversation-pairing.test.ts +4 -31
  41. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +6 -0
  42. package/src/__tests__/conversation-provider-retry-repair.test.ts +26 -5
  43. package/src/__tests__/conversation-queue.test.ts +2 -0
  44. package/src/__tests__/conversation-routes-disk-view.test.ts +3 -0
  45. package/src/__tests__/conversation-routes-slash-commands.test.ts +6 -5
  46. package/src/__tests__/conversation-runtime-assembly.test.ts +170 -229
  47. package/src/__tests__/conversation-runtime-workspace.test.ts +3 -24
  48. package/src/__tests__/conversation-slash-commands.test.ts +8 -42
  49. package/src/__tests__/conversation-slash-queue.test.ts +6 -1
  50. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +84 -0
  51. package/src/__tests__/conversation-sync-tags.test.ts +27 -15
  52. package/src/__tests__/conversation-title-service.test.ts +135 -2
  53. package/src/__tests__/conversation-workspace-injection.test.ts +6 -1
  54. package/src/__tests__/cross-provider-web-search.test.ts +214 -1
  55. package/src/__tests__/db-schedule-syntax-migration.test.ts +5 -0
  56. package/src/__tests__/dm-persistence.test.ts +5 -1
  57. package/src/__tests__/empty-response-hook.test.ts +304 -0
  58. package/src/__tests__/feature-flag-test-helpers.ts +2 -2
  59. package/src/__tests__/gemini-image-service.test.ts +13 -0
  60. package/src/__tests__/helpers/mock-provider.ts +110 -0
  61. package/src/__tests__/helpers/native-web-search-harness.ts +129 -0
  62. package/src/__tests__/history-repair-hook.test.ts +1 -0
  63. package/src/__tests__/identity-intro-cache.test.ts +12 -100
  64. package/src/__tests__/identity-routes.test.ts +248 -7
  65. package/src/__tests__/inbound-slack-persistence.test.ts +5 -1
  66. package/src/__tests__/injector-background-turn.test.ts +2 -8
  67. package/src/__tests__/injector-chain.test.ts +106 -270
  68. package/src/__tests__/injector-disk-pressure.test.ts +3 -12
  69. package/src/__tests__/injector-document-comments.test.ts +2 -2
  70. package/src/__tests__/injector-pkb-v2-silenced.test.ts +30 -22
  71. package/src/__tests__/injector-v3-suppression.test.ts +31 -37
  72. package/src/__tests__/internal-telemetry-routes.test.ts +109 -0
  73. package/src/__tests__/list-messages-page-latest.test.ts +60 -0
  74. package/src/__tests__/list-messages-tool-merge.test.ts +20 -0
  75. package/src/__tests__/llm-usage-store.test.ts +223 -1
  76. package/src/__tests__/memory-retrieval-hook.test.ts +297 -0
  77. package/src/__tests__/memory-v2-static-injector.test.ts +103 -35
  78. package/src/__tests__/native-web-search.test.ts +191 -0
  79. package/src/__tests__/onboarding-template-contract.test.ts +2 -0
  80. package/src/__tests__/openai-image-service.test.ts +17 -0
  81. package/src/__tests__/openai-provider.test.ts +31 -1
  82. package/src/__tests__/persist-unsendable-image.test.ts +215 -0
  83. package/src/__tests__/persistence-secret-redaction.test.ts +1 -0
  84. package/src/__tests__/pipeline-runner.test.ts +29 -39
  85. package/src/__tests__/pkb-autoinject.test.ts +2 -5
  86. package/src/__tests__/plugin-bootstrap.test.ts +13 -28
  87. package/src/__tests__/plugin-registry.test.ts +0 -27
  88. package/src/__tests__/plugin-types.test.ts +2 -125
  89. package/src/__tests__/process-message-display-content.test.ts +6 -2
  90. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +5 -1
  91. package/src/__tests__/resolve-trust-class.test.ts +4 -4
  92. package/src/__tests__/runtime-events-sse-reconnect.test.ts +60 -23
  93. package/src/__tests__/schedule-routes.test.ts +603 -2
  94. package/src/__tests__/schedule-store.test.ts +41 -0
  95. package/src/__tests__/schedule-tools.test.ts +35 -0
  96. package/src/__tests__/server-history-render.test.ts +314 -1
  97. package/src/__tests__/skillssh-files.test.ts +1 -1
  98. package/src/__tests__/system-prompt.test.ts +20 -0
  99. package/src/__tests__/task-scheduler.test.ts +162 -1
  100. package/src/__tests__/terminal-tools.test.ts +6 -1
  101. package/src/__tests__/title-generate-hook.test.ts +319 -0
  102. package/src/__tests__/tool-error-hook.test.ts +278 -0
  103. package/src/__tests__/tool-preview-lifecycle.test.ts +468 -5
  104. package/src/__tests__/tool-result-metadata-plumbing.test.ts +1 -0
  105. package/src/__tests__/tool-result-truncate-hook.test.ts +127 -0
  106. package/src/__tests__/tool-result-truncation.test.ts +0 -2
  107. package/src/__tests__/ui-choice-copy-surfaces.test.ts +254 -0
  108. package/src/__tests__/ui-work-result-surface.test.ts +159 -0
  109. package/src/__tests__/usage-routes.test.ts +285 -1
  110. package/src/__tests__/user-plugin-loader.test.ts +2 -2
  111. package/src/__tests__/voice-session-bridge.test.ts +6 -3
  112. package/src/__tests__/web-search-backend-failure.test.ts +166 -0
  113. package/src/agent/loop.ts +346 -442
  114. package/src/api/events/assistant-thinking-delta.ts +33 -0
  115. package/src/api/events/tool-output-chunk.ts +45 -0
  116. package/src/api/events/tool-use-preview-start.ts +32 -0
  117. package/src/api/events/trace-event.ts +69 -0
  118. package/src/api/index.ts +48 -13
  119. package/src/api/responses/conversation-message.ts +368 -0
  120. package/src/avatar/__tests__/avatar-store.test.ts +34 -29
  121. package/src/cli/commands/__tests__/notifications.test.ts +58 -14
  122. package/src/cli/commands/notifications.ts +112 -60
  123. package/src/config/assistant-feature-flags.ts +22 -11
  124. package/src/config/bundled-skills/app-builder/SKILL.md +3 -20
  125. package/src/config/bundled-skills/app-builder/references/examples/README.md +17 -0
  126. package/src/config/bundled-skills/app-builder/references/examples/expense-tracker.md +515 -0
  127. package/src/config/bundled-skills/app-builder/references/examples/focus-timer.md +342 -0
  128. package/src/config/bundled-skills/app-builder/references/examples/habit-tracker.md +490 -0
  129. package/src/config/bundled-skills/document-editor/SKILL.md +1 -1
  130. package/src/config/bundled-skills/messaging/SKILL.md +0 -7
  131. package/src/config/feature-flag-cache.ts +3 -3
  132. package/src/config/feature-flag-registry.json +35 -3
  133. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  134. package/src/config/schemas/__tests__/memory-v3.test.ts +25 -0
  135. package/src/config/schemas/llm.ts +1 -0
  136. package/src/config/schemas/memory-v2.ts +8 -0
  137. package/src/config/schemas/memory-v3.ts +8 -0
  138. package/src/config/schemas/platform.ts +8 -0
  139. package/src/config/seed-inference-profiles.ts +2 -2
  140. package/src/config/skills.ts +13 -0
  141. package/src/context/compactor.ts +1 -1
  142. package/src/context/strip-injections.ts +122 -0
  143. package/src/context/token-estimator.ts +23 -0
  144. package/src/context/tool-result-truncation.ts +0 -23
  145. package/src/context/window-manager.ts +3 -6
  146. package/src/credential-execution/executable-discovery.ts +16 -0
  147. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +6 -0
  148. package/src/daemon/__tests__/inference-profile-notification.test.ts +153 -0
  149. package/src/daemon/__tests__/native-web-search-metadata.test.ts +10 -8
  150. package/src/daemon/assistant-attachments.ts +1 -1
  151. package/src/daemon/config-watcher.ts +2 -2
  152. package/src/daemon/context-overflow-reducer.ts +0 -1
  153. package/src/daemon/conversation-agent-loop-handlers.ts +605 -153
  154. package/src/daemon/conversation-agent-loop.ts +281 -760
  155. package/src/daemon/conversation-history.ts +5 -4
  156. package/src/daemon/conversation-lifecycle.ts +3 -4
  157. package/src/daemon/conversation-messaging.ts +7 -6
  158. package/src/daemon/conversation-process.ts +11 -16
  159. package/src/daemon/conversation-runtime-assembly.ts +130 -347
  160. package/src/daemon/conversation-slash.ts +6 -25
  161. package/src/daemon/conversation-surfaces.ts +222 -4
  162. package/src/daemon/conversation-tool-setup.ts +2 -29
  163. package/src/daemon/conversation.ts +32 -14
  164. package/src/daemon/external-plugins-bootstrap.ts +9 -10
  165. package/src/daemon/handlers/config-a2a.ts +51 -36
  166. package/src/daemon/handlers/config-slack-channel.ts +20 -14
  167. package/src/daemon/handlers/config-telegram.ts +16 -2
  168. package/src/daemon/handlers/shared.ts +156 -84
  169. package/src/daemon/handlers/skills.ts +39 -10
  170. package/src/daemon/lifecycle.ts +4 -0
  171. package/src/daemon/message-types/apps.ts +1 -29
  172. package/src/daemon/message-types/messages.ts +9 -57
  173. package/src/daemon/message-types/skills.ts +2 -0
  174. package/src/daemon/message-types/surfaces.ts +136 -3
  175. package/src/daemon/now-scratchpad.ts +21 -0
  176. package/src/daemon/orphan-reaper.test.ts +210 -0
  177. package/src/daemon/orphan-reaper.ts +240 -0
  178. package/src/daemon/persist-unsendable-image.ts +117 -0
  179. package/src/daemon/process-message.ts +1 -3
  180. package/src/daemon/trace-emitter.ts +6 -4
  181. package/src/daemon/trust-context.ts +19 -0
  182. package/src/daemon/wake-target-adapter.ts +3 -1
  183. package/src/home/home-greeting-cache.ts +24 -1
  184. package/src/ipc/gateway-client.test.ts +2 -2
  185. package/src/ipc/gateway-client.ts +3 -3
  186. package/src/media/gemini-image-service.ts +15 -0
  187. package/src/media/openai-image-service.ts +14 -0
  188. package/src/media/types.ts +34 -0
  189. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +56 -0
  190. package/src/memory/auth-fallback-events-store.ts +94 -0
  191. package/src/memory/conversation-title-service.ts +65 -41
  192. package/src/memory/db-init.ts +4 -0
  193. package/src/memory/graph/__tests__/conversation-graph-memory-registry.test.ts +119 -0
  194. package/src/memory/graph/conversation-graph-memory.ts +65 -0
  195. package/src/memory/jobs-store.ts +33 -0
  196. package/src/memory/jobs-worker.ts +31 -4
  197. package/src/memory/llm-usage-store.ts +224 -50
  198. package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +6 -5
  199. package/src/memory/migrations/270-schedule-source-conversation.ts +13 -0
  200. package/src/memory/migrations/271-create-auth-fallback-events.ts +21 -0
  201. package/src/memory/migrations/index.ts +2 -0
  202. package/src/memory/pkb/autoinject.ts +61 -0
  203. package/src/memory/pkb/context.ts +50 -0
  204. package/src/memory/pkb/types.ts +14 -0
  205. package/src/memory/schedule-attribution-sql.ts +104 -0
  206. package/src/memory/schema/infrastructure.ts +16 -0
  207. package/src/memory/usage-grouped-buckets.ts +6 -1
  208. package/src/memory/v2/__tests__/consolidation-job.test.ts +1 -1
  209. package/src/memory/v2/consolidation-job.ts +1 -1
  210. package/src/memory/v3/__tests__/health.test.ts +16 -0
  211. package/src/memory/v3/__tests__/orchestrate.test.ts +45 -9
  212. package/src/memory/v3/__tests__/provider-blocks.test.ts +13 -0
  213. package/src/memory/v3/__tests__/router.test.ts +101 -29
  214. package/src/memory/v3/__tests__/selector.test.ts +93 -27
  215. package/src/memory/v3/__tests__/shadow-plugin.test.ts +23 -5
  216. package/src/memory/v3/health.ts +0 -0
  217. package/src/memory/v3/llm-retry.ts +32 -0
  218. package/src/memory/v3/orchestrate.ts +26 -14
  219. package/src/memory/v3/provider-blocks.ts +15 -5
  220. package/src/memory/v3/router.ts +48 -42
  221. package/src/memory/v3/selector.ts +57 -42
  222. package/src/memory/v3/shadow-plugin.ts +47 -15
  223. package/src/memory/v3/types.ts +8 -0
  224. package/src/notifications/conversation-pairing.ts +8 -15
  225. package/src/notifications/decision-engine.ts +6 -3
  226. package/src/notifications/home-feed-side-effect.ts +12 -1
  227. package/src/permissions/prompter.ts +4 -0
  228. package/src/plugin-api/constants.ts +4 -0
  229. package/src/plugin-api/index.ts +8 -1
  230. package/src/plugin-api/types.ts +151 -1
  231. package/src/plugins/defaults/empty-response/hooks/stop.ts +126 -0
  232. package/src/plugins/defaults/empty-response/register.ts +8 -13
  233. package/src/plugins/defaults/index.ts +1 -15
  234. package/src/plugins/defaults/injectors/register.ts +243 -74
  235. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +91 -0
  236. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +216 -0
  237. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +35 -0
  238. package/src/plugins/defaults/title-generate/hooks/stop.ts +75 -0
  239. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +35 -0
  240. package/src/plugins/defaults/title-generate/package.json +1 -1
  241. package/src/plugins/defaults/title-generate/register.ts +18 -18
  242. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +118 -0
  243. package/src/plugins/defaults/tool-error/package.json +1 -1
  244. package/src/plugins/defaults/tool-error/register.ts +9 -21
  245. package/src/plugins/defaults/tool-result-truncate/hooks/post-tool-use.ts +32 -0
  246. package/src/plugins/defaults/tool-result-truncate/register.ts +10 -21
  247. package/src/plugins/defaults/tool-result-truncate/terminal.ts +37 -18
  248. package/src/plugins/pipeline.ts +6 -18
  249. package/src/plugins/registry.ts +8 -25
  250. package/src/plugins/types.ts +43 -474
  251. package/src/proactive-artifact/aux-message-injector.ts +3 -3
  252. package/src/proactive-artifact/job.test.ts +7 -12
  253. package/src/prompts/__tests__/system-prompt.test.ts +36 -0
  254. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +62 -0
  255. package/src/prompts/templates/BOOTSTRAP.md +2 -2
  256. package/src/prompts/templates/system-sections.ts +15 -0
  257. package/src/providers/anthropic/client.ts +37 -29
  258. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +112 -0
  259. package/src/providers/openai/chat-completions-provider.ts +44 -0
  260. package/src/providers/openrouter/client.ts +1 -0
  261. package/src/providers/placeholder-sentinels.ts +35 -0
  262. package/src/runtime/__tests__/agent-wake.test.ts +5 -1
  263. package/src/runtime/agent-wake.ts +2 -2
  264. package/src/runtime/assistant-event-hub.ts +36 -6
  265. package/src/runtime/{conversation-stream-state.ts → assistant-stream-state.ts} +132 -58
  266. package/src/runtime/http-router.ts +16 -21
  267. package/src/runtime/http-types.ts +16 -70
  268. package/src/runtime/pending-interactions.ts +1 -0
  269. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +265 -2
  270. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +31 -1
  271. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +6 -2
  272. package/src/runtime/routes/__tests__/tts-routes.test.ts +6 -2
  273. package/src/runtime/routes/app-management-routes.ts +6 -117
  274. package/src/runtime/routes/app-routes.ts +13 -15
  275. package/src/runtime/routes/attachment-routes.ts +26 -15
  276. package/src/runtime/routes/avatar-routes.ts +26 -0
  277. package/src/runtime/routes/btw-routes.ts +29 -23
  278. package/src/runtime/routes/consolidation-routes.ts +120 -20
  279. package/src/runtime/routes/conversation-query-routes.ts +2 -0
  280. package/src/runtime/routes/conversation-routes.ts +358 -184
  281. package/src/runtime/routes/documents-routes.ts +4 -0
  282. package/src/runtime/routes/domain-routes.ts +51 -37
  283. package/src/runtime/routes/epoch-millis-range.ts +34 -0
  284. package/src/runtime/routes/events-routes.ts +28 -34
  285. package/src/runtime/routes/gateway-log-routes.ts +26 -4
  286. package/src/runtime/routes/heartbeat-routes.ts +32 -12
  287. package/src/runtime/routes/identity-intro-cache.ts +11 -34
  288. package/src/runtime/routes/identity-routes.ts +208 -17
  289. package/src/runtime/routes/image-generation-routes.ts +40 -2
  290. package/src/runtime/routes/index.ts +2 -0
  291. package/src/runtime/routes/integrations/a2a.ts +12 -10
  292. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +16 -0
  293. package/src/runtime/routes/integrations/slack/channel.ts +4 -0
  294. package/src/runtime/routes/integrations/slack/share.ts +27 -6
  295. package/src/runtime/routes/integrations/telegram.ts +6 -0
  296. package/src/runtime/routes/integrations/twilio.ts +42 -0
  297. package/src/runtime/routes/internal-telemetry-routes.ts +88 -0
  298. package/src/runtime/routes/log-export-routes.ts +8 -0
  299. package/src/runtime/routes/memory-v2-routes.ts +15 -8
  300. package/src/runtime/routes/memory-v3-routes.ts +50 -28
  301. package/src/runtime/routes/oauth-apps.ts +66 -12
  302. package/src/runtime/routes/oauth-providers.ts +44 -5
  303. package/src/runtime/routes/platform-routes.ts +81 -5
  304. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +6 -4
  305. package/src/runtime/routes/playground/force-compact.ts +1 -1
  306. package/src/runtime/routes/rename-conversation-routes.ts +5 -0
  307. package/src/runtime/routes/schedule-routes.ts +152 -42
  308. package/src/runtime/routes/secret-routes.ts +14 -2
  309. package/src/runtime/routes/skills-routes.ts +43 -14
  310. package/src/runtime/routes/tool-call-confirmation-enrichment.test.ts +161 -0
  311. package/src/runtime/routes/tool-call-confirmation-enrichment.ts +107 -0
  312. package/src/runtime/routes/trust-rules-routes.ts +26 -2
  313. package/src/runtime/routes/tts-routes.ts +35 -0
  314. package/src/runtime/routes/types.ts +66 -8
  315. package/src/runtime/routes/usage-routes.ts +47 -39
  316. package/src/runtime/routes/webhook-routes.ts +41 -2
  317. package/src/runtime/routes/workspace-routes.ts +4 -0
  318. package/src/runtime/services/__tests__/analyze-conversation.test.ts +6 -0
  319. package/src/runtime/services/analyze-conversation.ts +2 -2
  320. package/src/schedule/schedule-store.ts +20 -1
  321. package/src/schedule/schedule-usage-store.ts +83 -0
  322. package/src/schedule/scheduler.ts +12 -5
  323. package/src/skills/catalog-files.ts +2 -2
  324. package/src/skills/catalog-install.ts +3 -0
  325. package/src/skills/categories-cache.ts +118 -0
  326. package/src/skills/clawhub-files.ts +1 -2
  327. package/src/skills/skillssh-files.ts +1 -2
  328. package/src/telemetry/types.ts +29 -1
  329. package/src/telemetry/usage-telemetry-reporter.test.ts +112 -3
  330. package/src/telemetry/usage-telemetry-reporter.ts +57 -2
  331. package/src/tools/executor.ts +1 -53
  332. package/src/tools/network/__tests__/web-search-metadata.test.ts +7 -1
  333. package/src/tools/network/__tests__/web-search.test.ts +11 -3
  334. package/src/tools/network/web-search-error.test.ts +248 -0
  335. package/src/tools/network/web-search-error.ts +267 -0
  336. package/src/tools/network/web-search.ts +207 -48
  337. package/src/tools/schedule/create.ts +2 -0
  338. package/src/tools/terminal/safe-env.ts +10 -1
  339. package/src/tools/ui-surface/definitions.ts +9 -1
  340. package/src/tts/__tests__/provider-catalog-consistency.test.ts +85 -1
  341. package/src/tts/provider-catalog.ts +76 -1
  342. package/src/util/mutex.ts +47 -0
  343. package/src/workspace/git-service.ts +1 -42
  344. package/src/workspace/migrations/095-bump-heartbeat-interval-30m-to-60m.ts +51 -0
  345. package/src/workspace/migrations/096-reduce-quality-profile-effort.ts +72 -0
  346. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +93 -0
  347. package/src/workspace/migrations/registry.ts +6 -0
  348. package/src/__tests__/bootstrap-turn-cleanup.test.ts +0 -44
  349. package/src/__tests__/empty-response-pipeline.test.ts +0 -423
  350. package/src/__tests__/llm-call-pipeline.test.ts +0 -287
  351. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -418
  352. package/src/__tests__/persistence-pipeline.test.ts +0 -503
  353. package/src/__tests__/title-generate-pipeline.test.ts +0 -211
  354. package/src/__tests__/token-estimate-pipeline.test.ts +0 -479
  355. package/src/__tests__/tool-error-pipeline.test.ts +0 -241
  356. package/src/__tests__/tool-execute-pipeline.test.ts +0 -417
  357. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -341
  358. package/src/daemon/bootstrap-turn-cleanup.ts +0 -45
  359. package/src/gallery/default-gallery.ts +0 -1359
  360. package/src/gallery/gallery-manifest.ts +0 -28
  361. package/src/home/feature-gate.ts +0 -22
  362. package/src/plugins/defaults/empty-response/middlewares/emptyResponse.ts +0 -22
  363. package/src/plugins/defaults/empty-response/terminal.ts +0 -106
  364. package/src/plugins/defaults/injectors/package.json +0 -15
  365. package/src/plugins/defaults/llm-call/middlewares/llmCall.ts +0 -17
  366. package/src/plugins/defaults/llm-call/package.json +0 -15
  367. package/src/plugins/defaults/llm-call/register.ts +0 -45
  368. package/src/plugins/defaults/memory-retrieval/middlewares/memoryRetrieval.ts +0 -17
  369. package/src/plugins/defaults/memory-retrieval/package.json +0 -15
  370. package/src/plugins/defaults/memory-retrieval/register.ts +0 -181
  371. package/src/plugins/defaults/persistence/middlewares/persistence.ts +0 -19
  372. package/src/plugins/defaults/persistence/package.json +0 -15
  373. package/src/plugins/defaults/persistence/register.ts +0 -38
  374. package/src/plugins/defaults/persistence/terminal.ts +0 -83
  375. package/src/plugins/defaults/title-generate/terminal.ts +0 -31
  376. package/src/plugins/defaults/token-estimate/middlewares/tokenEstimate.ts +0 -23
  377. package/src/plugins/defaults/token-estimate/package.json +0 -15
  378. package/src/plugins/defaults/token-estimate/register.ts +0 -34
  379. package/src/plugins/defaults/token-estimate/terminal.ts +0 -40
  380. package/src/plugins/defaults/tool-error/middlewares/toolError.ts +0 -21
  381. package/src/plugins/defaults/tool-error/terminal.ts +0 -47
  382. package/src/plugins/defaults/tool-execute/middlewares/toolExecute.ts +0 -23
  383. package/src/plugins/defaults/tool-execute/package.json +0 -15
  384. package/src/plugins/defaults/tool-execute/register.ts +0 -49
  385. package/src/plugins/defaults/tool-result-truncate/middlewares/toolResultTruncate.ts +0 -23
  386. package/src/plugins/defaults/tool-result-truncate/types.ts +0 -22
  387. package/src/skills/category-inference.ts +0 -111
@@ -9,13 +9,18 @@ mock.module("../util/logger.js", () => ({
9
9
 
10
10
  import { getSqlite } from "../memory/db-connection.js";
11
11
  import { initializeDb } from "../memory/db-init.js";
12
- import { recordUsageEvent } from "../memory/llm-usage-store.js";
12
+ import {
13
+ getUsageCostForConversationWindow,
14
+ recordUsageEvent,
15
+ } from "../memory/llm-usage-store.js";
13
16
  import { BadRequestError } from "../runtime/routes/errors.js";
14
17
  import { ROUTES } from "../runtime/routes/usage-routes.js";
15
18
 
16
19
  initializeDb();
17
20
 
18
21
  function clearUsageEvents() {
22
+ getSqlite().run("DELETE FROM cron_runs");
23
+ getSqlite().run("DELETE FROM cron_jobs");
19
24
  getSqlite().run("DELETE FROM llm_usage_events");
20
25
  }
21
26
 
@@ -118,6 +123,107 @@ function seedEvents() {
118
123
  return { day1, day2 };
119
124
  }
120
125
 
126
+ function recordCostAt(
127
+ conversationId: string,
128
+ requestId: string,
129
+ createdAt: number,
130
+ estimatedCostUsd: number,
131
+ ) {
132
+ recordUsageEvent(
133
+ {
134
+ conversationId,
135
+ runId: null,
136
+ requestId,
137
+ actor: "main_agent",
138
+ callSite: "mainAgent",
139
+ inferenceProfile: "balanced",
140
+ provider: "anthropic",
141
+ model: "claude-sonnet-4-20250514",
142
+ inputTokens: 100,
143
+ outputTokens: 50,
144
+ cacheCreationInputTokens: 0,
145
+ cacheReadInputTokens: 0,
146
+ rawUsage: null,
147
+ },
148
+ { estimatedCostUsd, pricingStatus: "priced" },
149
+ );
150
+ getSqlite().run(
151
+ "UPDATE llm_usage_events SET created_at = ? WHERE request_id = ?",
152
+ [createdAt, requestId],
153
+ );
154
+ }
155
+
156
+ function insertScheduleJob(id: string, name: string): void {
157
+ const now = new Date("2026-01-01T00:00:00Z").getTime();
158
+ getSqlite().run(
159
+ `INSERT INTO cron_jobs (
160
+ id,
161
+ name,
162
+ cron_expression,
163
+ message,
164
+ next_run_at,
165
+ created_by,
166
+ created_at,
167
+ updated_at
168
+ ) VALUES (?, ?, '* * * * *', 'Example scheduled task', ?, 'user', ?, ?)`,
169
+ [id, name, now, now, now],
170
+ );
171
+ }
172
+
173
+ function insertScheduleRun({
174
+ id,
175
+ scheduleId,
176
+ conversationId,
177
+ startedAt,
178
+ finishedAt,
179
+ }: {
180
+ id: string;
181
+ scheduleId: string;
182
+ conversationId: string;
183
+ startedAt: number;
184
+ finishedAt: number | null;
185
+ }): void {
186
+ getSqlite().run(
187
+ `INSERT INTO cron_runs (
188
+ id,
189
+ job_id,
190
+ status,
191
+ started_at,
192
+ finished_at,
193
+ conversation_id,
194
+ created_at
195
+ ) VALUES (?, ?, 'ok', ?, ?, ?, ?)`,
196
+ [id, scheduleId, startedAt, finishedAt, conversationId, startedAt],
197
+ );
198
+ }
199
+
200
+ function seedScheduleRouteEvents() {
201
+ insertScheduleJob("schedule-a", "Morning summary");
202
+ insertScheduleJob("schedule-b", "Nightly sync");
203
+ insertScheduleRun({
204
+ id: "run-a-1",
205
+ scheduleId: "schedule-a",
206
+ conversationId: "conv-reused",
207
+ startedAt: 1_000,
208
+ finishedAt: 2_000,
209
+ });
210
+ insertScheduleRun({
211
+ id: "run-b-1",
212
+ scheduleId: "schedule-b",
213
+ conversationId: "conv-reused",
214
+ startedAt: 3_000,
215
+ finishedAt: 3_500,
216
+ });
217
+
218
+ recordCostAt("conv-reused", "route-before-a", 900, 0.09);
219
+ recordCostAt("conv-reused", "route-a-start", 1_000, 0.1);
220
+ recordCostAt("conv-reused", "route-a-inside", 1_500, 0.2);
221
+ recordCostAt("conv-reused", "route-a-finish", 2_000, 0.3);
222
+ recordCostAt("conv-reused", "route-after-a", 2_500, 0.4);
223
+ recordCostAt("conv-reused", "route-b-inside", 3_200, 0.5);
224
+ recordCostAt("conv-other", "route-other", 1_500, 0.8);
225
+ }
226
+
121
227
  // ---------------------------------------------------------------------------
122
228
  // Tests
123
229
  // ---------------------------------------------------------------------------
@@ -125,6 +231,25 @@ function seedEvents() {
125
231
  describe("usage routes", () => {
126
232
  beforeEach(clearUsageEvents);
127
233
 
234
+ describe("getUsageCostForConversationWindow", () => {
235
+ test("sums only events for the conversation inside the inclusive window", () => {
236
+ recordCostAt("conv-window", "req-before", 999, 0.5);
237
+ recordCostAt("conv-window", "req-start", 1000, 0.01);
238
+ recordCostAt("conv-window", "req-middle", 1500, 0.02);
239
+ recordCostAt("conv-window", "req-end", 2000, 0.03);
240
+ recordCostAt("conv-window", "req-after", 2001, 0.75);
241
+ recordCostAt("conv-other", "req-other", 1500, 0.9);
242
+
243
+ const total = getUsageCostForConversationWindow({
244
+ conversationId: "conv-window",
245
+ from: 1000,
246
+ to: 2000,
247
+ });
248
+
249
+ expect(total).toBeCloseTo(0.06);
250
+ });
251
+ });
252
+
128
253
  // -- query parsing / validation --
129
254
 
130
255
  describe("query parameter validation", () => {
@@ -204,6 +329,19 @@ describe("usage routes", () => {
204
329
  expect(body.totalCacheCreationTokens).toBe(50);
205
330
  expect(body.totalCacheReadTokens).toBe(100);
206
331
  });
332
+
333
+ test("filters by trimmed scheduleId using schedule run windows", () => {
334
+ seedScheduleRouteEvents();
335
+
336
+ const body = dispatch(
337
+ "GET",
338
+ "usage/totals?from=0&to=4000&scheduleId=%20schedule-a%20",
339
+ ) as Record<string, number>;
340
+
341
+ expect(body.eventCount).toBe(3);
342
+ expect(body.totalInputTokens).toBe(300);
343
+ expect(body.totalEstimatedCostUsd).toBeCloseTo(0.6);
344
+ });
207
345
  });
208
346
 
209
347
  // -- daily buckets --
@@ -255,6 +393,21 @@ describe("usage routes", () => {
255
393
  expect(body.buckets[1].totalInputTokens).toBe(2000);
256
394
  expect(body.buckets[1].eventCount).toBe(1);
257
395
  });
396
+
397
+ test("filters daily buckets by scheduleId", () => {
398
+ seedScheduleRouteEvents();
399
+
400
+ const body = dispatch(
401
+ "GET",
402
+ "usage/daily?from=0&to=4000&scheduleId=schedule-a",
403
+ ) as {
404
+ buckets: Array<{ totalEstimatedCostUsd: number; eventCount: number }>;
405
+ };
406
+
407
+ expect(body.buckets).toHaveLength(1);
408
+ expect(body.buckets[0].eventCount).toBe(3);
409
+ expect(body.buckets[0].totalEstimatedCostUsd).toBeCloseTo(0.6);
410
+ });
258
411
  });
259
412
 
260
413
  // -- breakdown --
@@ -396,6 +549,70 @@ describe("usage routes", () => {
396
549
  null,
397
550
  ]);
398
551
  });
552
+
553
+ test("accepts groupBy=schedule and labels groups with schedule names", () => {
554
+ seedScheduleRouteEvents();
555
+
556
+ const body = dispatch(
557
+ "GET",
558
+ "usage/breakdown?from=0&to=4000&groupBy=schedule",
559
+ ) as {
560
+ breakdown: Array<{
561
+ group: string;
562
+ groupId: string | null;
563
+ groupKey: string | null;
564
+ totalEstimatedCostUsd: number;
565
+ eventCount: number;
566
+ }>;
567
+ };
568
+
569
+ expect(
570
+ body.breakdown.find((row) => row.groupKey === "schedule-a"),
571
+ ).toMatchObject({
572
+ group: "Morning summary",
573
+ groupId: "schedule-a",
574
+ totalEstimatedCostUsd: 0.6,
575
+ eventCount: 3,
576
+ });
577
+ expect(
578
+ body.breakdown.find((row) => row.groupKey === "schedule-b"),
579
+ ).toMatchObject({
580
+ group: "Nightly sync",
581
+ groupId: "schedule-b",
582
+ totalEstimatedCostUsd: 0.5,
583
+ eventCount: 1,
584
+ });
585
+ expect(body.breakdown.find((row) => row.groupKey === null)).toMatchObject(
586
+ {
587
+ group: "Other",
588
+ groupId: null,
589
+ totalEstimatedCostUsd: 1.29,
590
+ eventCount: 3,
591
+ },
592
+ );
593
+ });
594
+
595
+ test("filters breakdown by scheduleId", () => {
596
+ seedScheduleRouteEvents();
597
+
598
+ const body = dispatch(
599
+ "GET",
600
+ "usage/breakdown?from=0&to=4000&groupBy=provider&scheduleId=schedule-a",
601
+ ) as {
602
+ breakdown: Array<{
603
+ group: string;
604
+ totalEstimatedCostUsd: number;
605
+ eventCount: number;
606
+ }>;
607
+ };
608
+
609
+ expect(body.breakdown).toHaveLength(1);
610
+ expect(body.breakdown[0]).toMatchObject({
611
+ group: "anthropic",
612
+ totalEstimatedCostUsd: 0.6,
613
+ eventCount: 3,
614
+ });
615
+ });
399
616
  });
400
617
 
401
618
  describe("GET /v1/usage/series", () => {
@@ -484,5 +701,72 @@ describe("usage routes", () => {
484
701
  totalInputTokens: 2000,
485
702
  });
486
703
  });
704
+
705
+ test("accepts groupBy=schedule and labels schedule series groups", () => {
706
+ seedScheduleRouteEvents();
707
+
708
+ const body = dispatch(
709
+ "GET",
710
+ "usage/series?from=0&to=4000&groupBy=schedule&granularity=daily",
711
+ ) as {
712
+ buckets: Array<{
713
+ groups: Record<
714
+ string,
715
+ {
716
+ group: string;
717
+ groupKey: string | null;
718
+ totalEstimatedCostUsd: number;
719
+ }
720
+ >;
721
+ }>;
722
+ };
723
+
724
+ expect(body.buckets).toHaveLength(1);
725
+ expect(body.buckets[0].groups["value:schedule-a"]).toMatchObject({
726
+ group: "Morning summary",
727
+ groupKey: "schedule-a",
728
+ });
729
+ expect(
730
+ body.buckets[0].groups["value:schedule-a"].totalEstimatedCostUsd,
731
+ ).toBeCloseTo(0.6);
732
+ expect(body.buckets[0].groups["value:schedule-b"]).toMatchObject({
733
+ group: "Nightly sync",
734
+ groupKey: "schedule-b",
735
+ });
736
+ expect(
737
+ body.buckets[0].groups["value:schedule-b"].totalEstimatedCostUsd,
738
+ ).toBeCloseTo(0.5);
739
+ expect(body.buckets[0].groups["null:schedule"]).toMatchObject({
740
+ group: "Other",
741
+ groupKey: null,
742
+ });
743
+ expect(
744
+ body.buckets[0].groups["null:schedule"].totalEstimatedCostUsd,
745
+ ).toBeCloseTo(1.29);
746
+ });
747
+
748
+ test("filters grouped series by scheduleId", () => {
749
+ seedScheduleRouteEvents();
750
+
751
+ const body = dispatch(
752
+ "GET",
753
+ "usage/series?from=0&to=4000&groupBy=call_site&granularity=daily&scheduleId=schedule-a",
754
+ ) as {
755
+ buckets: Array<{
756
+ totalEstimatedCostUsd: number;
757
+ groups: Record<string, { totalEstimatedCostUsd: number }>;
758
+ }>;
759
+ };
760
+
761
+ expect(body.buckets).toHaveLength(1);
762
+ expect(body.buckets[0].totalEstimatedCostUsd).toBeCloseTo(0.6);
763
+ expect(body.buckets[0].groups["value:mainAgent"]).toMatchObject({
764
+ group: "Main Agent",
765
+ groupKey: "mainAgent",
766
+ });
767
+ expect(
768
+ body.buckets[0].groups["value:mainAgent"].totalEstimatedCostUsd,
769
+ ).toBeCloseTo(0.6);
770
+ });
487
771
  });
488
772
  });
@@ -181,8 +181,8 @@ registerPlugin({
181
181
  // continues running in the background and would otherwise call
182
182
  // `registerPlugin()` after `loadUserPlugins()` has returned (and after
183
183
  // `bootstrapPlugins()` has potentially already walked the registry),
184
- // leaving the plugin visible to `getMiddlewaresFor()` / `getInjectors()`
185
- // with its `init()` hook never invoked.
184
+ // leaving the plugin visible to `getMiddlewaresFor()` with its `init()`
185
+ // hook never invoked.
186
186
  //
187
187
  // The `closeRegistration()` latch must reject that late arrival so the
188
188
  // registry stays consistent with the bootstrap invariant.
@@ -96,10 +96,10 @@ function makePersistingStreamingSession(
96
96
 
97
97
  let turnChannelContext: TurnChannelContext | null = null;
98
98
  let turnInterfaceContext: TurnInterfaceContext | null = null;
99
+ let processing = false;
99
100
  const session = {
100
101
  conversationId,
101
102
  messages: [],
102
- processing: false,
103
103
  abortController: null,
104
104
  currentRequestId: undefined,
105
105
  queue: {} as never,
@@ -108,7 +108,10 @@ function makePersistingStreamingSession(
108
108
  scopeId: "default",
109
109
  includeDefaultFallback: false,
110
110
  },
111
- isProcessing: () => session.processing,
111
+ isProcessing: () => processing,
112
+ setProcessing: (value: boolean) => {
113
+ processing = value;
114
+ },
112
115
  persistUserMessage: async (
113
116
  ...args: Parameters<Conversation["persistUserMessage"]>
114
117
  ) => persistUserMessageImpl(session, ...args),
@@ -138,7 +141,7 @@ function makePersistingStreamingSession(
138
141
  for (const event of events) {
139
142
  onEvent(event);
140
143
  }
141
- session.processing = false;
144
+ processing = false;
142
145
  session.abortController = null;
143
146
  session.currentRequestId = undefined;
144
147
  },
@@ -0,0 +1,166 @@
1
+ // End-to-end integration coverage for the centralized web_search
2
+ // backend-failure normalization layer (ATL-727).
3
+ //
4
+ // PRs 1-4 built the layer in three places:
5
+ // - `tools/network/web-search-error.ts` — `classifyWebSearchFailure` +
6
+ // `WEB_SEARCH_BACKEND_FAILURE_MESSAGE` + the structured
7
+ // `web_search_backend_failure` log helper.
8
+ // - `daemon/conversation-agent-loop-handlers.ts` — the native
9
+ // `server_tool_complete` handler maps Anthropic backend failures to the
10
+ // friendly copy, dedups per turn (`webSearchBackendFailureNotified`), and
11
+ // gates telemetry on `classification.isBackendFailure`.
12
+ // - `tools/network/web-search.ts` — the app-side `backendFailureResult`
13
+ // helper routes 5xx/network/429 to the same copy.
14
+ //
15
+ // The single-layer native-handler invariants (friendly copy + isError + empty
16
+ // results, raw-detail-logged-not-shown, per-turn dedup, successful-empty
17
+ // search) are owned by `native-web-search.test.ts`. This file locks the
18
+ // cross-cutting acceptance criteria that file does not cover:
19
+ // - honest continuation (the failure is a recoverable tool_result, not a
20
+ // thrown provider error; the search is never marked successful),
21
+ // - web_fetch DNS failures are NOT conflated with the search backend copy.
22
+ //
23
+ // It genuinely reuses the shared handler harness in
24
+ // `helpers/native-web-search-harness.ts` (the same harness driven by
25
+ // `native-web-search.test.ts`); the web_fetch invariant is exercised through
26
+ // the same `handleToolResult` path an app-executed fetch tool drives.
27
+
28
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
29
+
30
+ import type { ToolActivityMetadata } from "../daemon/message-types/web-activity.js";
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Mock the daemon collaborators the handler module imports at load time so the
34
+ // handler can be driven in isolation (mirrors native-web-search.test.ts).
35
+ // `mock.module()` is file-scoped, so the shared harness cannot install these.
36
+ // ---------------------------------------------------------------------------
37
+
38
+ mock.module("../config/loader.js", () => ({
39
+ getConfig: () => ({
40
+ skills: {
41
+ entries: {},
42
+ load: { extraDirs: [], watch: false, watchDebounceMs: 0 },
43
+ install: { nodeManager: "npm" },
44
+ allowBundled: null,
45
+ remoteProviders: {
46
+ skillssh: { enabled: true },
47
+ clawhub: { enabled: true },
48
+ },
49
+ remotePolicy: {
50
+ blockSuspicious: true,
51
+ blockMalware: true,
52
+ maxSkillsShRisk: "medium",
53
+ },
54
+ },
55
+ }),
56
+ loadConfig: () => ({}),
57
+ }));
58
+
59
+ mock.module("../memory/conversation-crud.js", () => ({
60
+ addMessage: () => ({ id: "mock-msg-id" }),
61
+ getMessageById: () => null,
62
+ updateMessageContent: () => {},
63
+ provenanceFromTrustContext: () => ({}),
64
+ reserveMessage: mock(async () => ({ id: "msg-reserve" })),
65
+ }));
66
+
67
+ mock.module("../memory/llm-request-log-store.js", () => ({
68
+ recordRequestLog: () => {},
69
+ backfillMessageIdOnLogs: () => {},
70
+ }));
71
+
72
+ // Import after mocking.
73
+ import {
74
+ createEventHandlerState,
75
+ dispatchAgentEvent,
76
+ type EventHandlerState,
77
+ } from "../daemon/conversation-agent-loop-handlers.js";
78
+ import { WEB_SEARCH_BACKEND_FAILURE_MESSAGE } from "../tools/network/web-search-error.js";
79
+ import {
80
+ backendFailureLogs,
81
+ completeNativeWebSearch,
82
+ createHandlerDeps,
83
+ lastToolResult,
84
+ } from "./helpers/native-web-search-harness.js";
85
+
86
+ describe("web_search backend-failure end-to-end (ATL-727)", () => {
87
+ let state: EventHandlerState;
88
+
89
+ beforeEach(() => {
90
+ state = createEventHandlerState();
91
+ });
92
+
93
+ test("backend failure stays an honest, recoverable tool result (not a thrown provider error, never marked successful)", async () => {
94
+ const { deps, events } = createHandlerDeps("req-honesty");
95
+
96
+ // A recoverable backend failure flows as a tool_result, so dispatch must
97
+ // resolve without throwing.
98
+ await expect(
99
+ completeNativeWebSearch(state, deps, "tu_honesty", {
100
+ isError: true,
101
+ errorCode: "unavailable",
102
+ }),
103
+ ).resolves.toBeUndefined();
104
+
105
+ const result = lastToolResult(events);
106
+ // The search is never silently upgraded to a success.
107
+ expect(result?.isError).toBe(true);
108
+ expect(result?.activityMetadata?.webSearch?.resultCount).toBe(0);
109
+ expect(result?.activityMetadata?.webSearch?.results).toEqual([]);
110
+ });
111
+
112
+ test("web_fetch DNS failure is NOT conflated with the web_search backend copy", async () => {
113
+ // The normalization layer keys exclusively on web_search (the native
114
+ // `server_tool_complete` handler and `web-search.ts`). It never inspects
115
+ // `webFetch` metadata. handleToolResult forwards an app-executed tool's
116
+ // `activityMetadata` verbatim, so a web_fetch DNS failure must reach the
117
+ // client with its own copy and acquire NO `webSearch` metadata. Drive that
118
+ // path directly with a DNS-failure fetch result for grimgoods.io.
119
+ const { deps, events, warnings } = createHandlerDeps("req-web-fetch");
120
+
121
+ const dnsError =
122
+ 'Error: Unable to resolve host "grimgoods.io" (DNS lookup failed)';
123
+ const fetchMetadata: ToolActivityMetadata = {
124
+ webFetch: {
125
+ url: "https://grimgoods.io",
126
+ finalUrl: "https://grimgoods.io",
127
+ status: 0,
128
+ byteCount: 0,
129
+ charCount: 0,
130
+ truncated: false,
131
+ domain: "grimgoods.io",
132
+ redirectCount: 0,
133
+ durationMs: 12,
134
+ errorMessage: dnsError,
135
+ },
136
+ };
137
+
138
+ await dispatchAgentEvent(state, deps, {
139
+ type: "tool_use",
140
+ id: "tu_fetch",
141
+ name: "web_fetch",
142
+ input: { url: "https://grimgoods.io" },
143
+ });
144
+ await dispatchAgentEvent(state, deps, {
145
+ type: "tool_result",
146
+ toolUseId: "tu_fetch",
147
+ content: dnsError,
148
+ isError: true,
149
+ activityMetadata: fetchMetadata,
150
+ });
151
+
152
+ const result = lastToolResult(events);
153
+ expect(result?.isError).toBe(true);
154
+
155
+ // The DNS error keeps its own webFetch copy untouched...
156
+ expect(result?.activityMetadata?.webFetch?.errorMessage).toBe(dnsError);
157
+ // ...and is never rewritten to the search backend copy.
158
+ expect(result?.activityMetadata?.webFetch?.errorMessage).not.toBe(
159
+ WEB_SEARCH_BACKEND_FAILURE_MESSAGE,
160
+ );
161
+ // No webSearch metadata is fabricated for a fetch failure.
162
+ expect(result?.activityMetadata?.webSearch).toBeUndefined();
163
+ // And the web_search backend-failure telemetry never fires for a fetch.
164
+ expect(backendFailureLogs(warnings)).toHaveLength(0);
165
+ });
166
+ });