@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
@@ -94,7 +94,15 @@ mock.module("../version.js", () => ({
94
94
  let mockCollectUsageData = true;
95
95
 
96
96
  mock.module("../config/loader.js", () => ({
97
- getConfig: () => ({ collectUsageData: mockCollectUsageData }),
97
+ getConfig: () => ({
98
+ ui: {},
99
+ model: "test",
100
+ provider: "test",
101
+ memory: { enabled: false },
102
+ rateLimit: { maxRequestsPerMinute: 0 },
103
+ secretDetection: { enabled: false },
104
+ collectUsageData: mockCollectUsageData,
105
+ }),
98
106
  }));
99
107
 
100
108
  const mockQueryUnreportedLifecycleEvents = mock(
@@ -130,13 +138,24 @@ mock.module("../memory/onboarding-events-store.js", () => ({
130
138
  queryUnreportedOnboardingEvents: mockQueryUnreportedOnboardingEvents,
131
139
  }));
132
140
 
141
+ // The auth-fallback store is intentionally NOT mocked — it has its own
142
+ // DB-backed tests, and Bun's `mock.module` is process-global, so mocking it
143
+ // here would leak into those tests when files share an invocation. We seed the
144
+ // real DB instead so every auth-fallback test stays order-independent.
145
+
133
146
  // ---------------------------------------------------------------------------
134
147
  // Production import (after mocks)
135
148
  // ---------------------------------------------------------------------------
136
149
 
150
+ import { recordAuthFallbackCounts } from "../memory/auth-fallback-events-store.js";
151
+ import { getDb } from "../memory/db-connection.js";
152
+ import { initializeDb } from "../memory/db-init.js";
153
+ import { authFallbackEvents } from "../memory/schema.js";
137
154
  import type { UsageEvent } from "../usage/types.js";
138
155
  import { UsageTelemetryReporter } from "./usage-telemetry-reporter.js";
139
156
 
157
+ initializeDb();
158
+
140
159
  // ---------------------------------------------------------------------------
141
160
  // Helpers
142
161
  // ---------------------------------------------------------------------------
@@ -201,6 +220,7 @@ beforeEach(() => {
201
220
  mockQueryUnreportedLifecycleEvents.mockReturnValue([]);
202
221
  mockQueryUnreportedOnboardingEvents.mockReset();
203
222
  mockQueryUnreportedOnboardingEvents.mockReturnValue([]);
223
+ getDb().delete(authFallbackEvents).run();
204
224
  mockPlatformClient = null;
205
225
  mockGetPlatformBaseUrl.mockReset();
206
226
  mockGetDeviceId.mockReset();
@@ -909,9 +929,9 @@ describe("UsageTelemetryReporter", () => {
909
929
  // No HTTP call should have been made
910
930
  expect(mockFetch).not.toHaveBeenCalled();
911
931
 
912
- // All 4 timestamp watermarks should have been advanced (IDs left untouched
932
+ // All 5 timestamp watermarks should have been advanced (IDs left untouched
913
933
  // so the compound-cursor branch stays active)
914
- expect(mockSetMemoryCheckpoint).toHaveBeenCalledTimes(4);
934
+ expect(mockSetMemoryCheckpoint).toHaveBeenCalledTimes(5);
915
935
 
916
936
  const calls = mockSetMemoryCheckpoint.mock.calls;
917
937
  const keys = calls.map((c) => c[0]);
@@ -919,6 +939,7 @@ describe("UsageTelemetryReporter", () => {
919
939
  expect(keys).toContain("telemetry:turns:last_reported_at");
920
940
  expect(keys).toContain("telemetry:lifecycle:last_reported_at");
921
941
  expect(keys).toContain("telemetry:onboarding:last_reported_at");
942
+ expect(keys).toContain("telemetry:auth_fallback:last_reported_at");
922
943
  });
923
944
 
924
945
  test("events sent normally after re-enabling collectUsageData", async () => {
@@ -1075,4 +1096,92 @@ describe("UsageTelemetryReporter", () => {
1075
1096
  // Envelope still reflects the running binary, not either event.
1076
1097
  expect(body.assistant_version).toBe("1.2.3-test");
1077
1098
  });
1099
+
1100
+ // -------------------------------------------------------------------------
1101
+ // Auth-fallback events
1102
+ // -------------------------------------------------------------------------
1103
+
1104
+ test("auth_fallback events are included in the events array with type discriminator", async () => {
1105
+ mockQueryUnreportedUsageEvents.mockReturnValue([]);
1106
+ recordAuthFallbackCounts(1700000740000, 1700000800000, [
1107
+ {
1108
+ guard: "edge",
1109
+ path: "/v1/messages",
1110
+ failureKind: "missing_authorization",
1111
+ count: 42,
1112
+ },
1113
+ ]);
1114
+ mockFetch.mockImplementation(() =>
1115
+ Promise.resolve(new Response('{"accepted":1}', { status: 200 })),
1116
+ );
1117
+
1118
+ const reporter = new UsageTelemetryReporter();
1119
+ await reporter.flush();
1120
+
1121
+ expect(mockFetch).toHaveBeenCalledTimes(1);
1122
+ const body = JSON.parse(
1123
+ (mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
1124
+ );
1125
+ expect(body.events.length).toBe(1);
1126
+ expect(body.events[0]).toMatchObject({
1127
+ type: "auth_fallback",
1128
+ guard: "edge",
1129
+ path: "/v1/messages",
1130
+ failure_kind: "missing_authorization",
1131
+ count: 42,
1132
+ window_start: 1700000740000,
1133
+ window_end: 1700000800000,
1134
+ assistant_version: "1.2.3-test",
1135
+ });
1136
+ // recorded_at is the row's createdAt (stamped at record time).
1137
+ expect(typeof body.events[0].recorded_at).toBe("number");
1138
+ expect(typeof body.events[0].daemon_event_id).toBe("string");
1139
+ });
1140
+
1141
+ test("auth_fallback watermark advances to the last reported row on success", async () => {
1142
+ mockQueryUnreportedUsageEvents.mockReturnValue([]);
1143
+ recordAuthFallbackCounts(1700000000000, 1700000001000, [
1144
+ {
1145
+ guard: "edge-scoped",
1146
+ path: "/v1/a",
1147
+ failureKind: "insufficient_scope",
1148
+ count: 1,
1149
+ },
1150
+ {
1151
+ guard: "edge-guardian",
1152
+ path: "/v1/b",
1153
+ failureKind: "guardian_mismatch",
1154
+ count: 3,
1155
+ },
1156
+ ]);
1157
+ mockFetch.mockImplementation(() =>
1158
+ Promise.resolve(new Response('{"accepted":2}', { status: 200 })),
1159
+ );
1160
+
1161
+ // The last row by the reporter's (createdAt, id) cursor order is the one
1162
+ // whose watermark should be persisted after a successful upload.
1163
+ const rows = getDb()
1164
+ .select()
1165
+ .from(authFallbackEvents)
1166
+ .orderBy(authFallbackEvents.createdAt, authFallbackEvents.id)
1167
+ .all();
1168
+ const lastRow = rows[rows.length - 1];
1169
+
1170
+ const reporter = new UsageTelemetryReporter();
1171
+ await reporter.flush();
1172
+
1173
+ const watermarkCalls = mockSetMemoryCheckpoint.mock.calls.filter(
1174
+ (c) => c[0] === "telemetry:auth_fallback:last_reported_at",
1175
+ );
1176
+ expect(watermarkCalls.length).toBeGreaterThanOrEqual(1);
1177
+ expect(watermarkCalls[watermarkCalls.length - 1][1]).toBe(
1178
+ String(lastRow.createdAt),
1179
+ );
1180
+
1181
+ const idCalls = mockSetMemoryCheckpoint.mock.calls.filter(
1182
+ (c) => c[0] === "telemetry:auth_fallback:last_reported_id",
1183
+ );
1184
+ expect(idCalls.length).toBeGreaterThanOrEqual(1);
1185
+ expect(idCalls[idCalls.length - 1][1]).toBe(lastRow.id);
1186
+ });
1078
1187
  });
@@ -15,6 +15,7 @@ import {
15
15
  getPlatformUserId,
16
16
  } from "../config/env.js";
17
17
  import { getConfig } from "../config/loader.js";
18
+ import { queryUnreportedAuthFallbackEvents } from "../memory/auth-fallback-events-store.js";
18
19
  import {
19
20
  getMemoryCheckpoint,
20
21
  setMemoryCheckpoint,
@@ -47,6 +48,10 @@ const CHECKPOINT_KEY_ONBOARDING_WATERMARK =
47
48
  "telemetry:onboarding:last_reported_at";
48
49
  const CHECKPOINT_KEY_ONBOARDING_WATERMARK_ID =
49
50
  "telemetry:onboarding:last_reported_id";
51
+ const CHECKPOINT_KEY_AUTH_FALLBACK_WATERMARK =
52
+ "telemetry:auth_fallback:last_reported_at";
53
+ const CHECKPOINT_KEY_AUTH_FALLBACK_WATERMARK_ID =
54
+ "telemetry:auth_fallback:last_reported_id";
50
55
  const REPORT_INTERVAL_MS = 5 * 60 * 1000;
51
56
  const INITIAL_FLUSH_DELAY_MS = 30_000; // Delay first flush to let CES handshake complete
52
57
  const BATCH_SIZE = 500;
@@ -139,6 +144,7 @@ export class UsageTelemetryReporter {
139
144
  setMemoryCheckpoint(CHECKPOINT_KEY_TURN_WATERMARK, now);
140
145
  setMemoryCheckpoint(CHECKPOINT_KEY_LIFECYCLE_WATERMARK, now);
141
146
  setMemoryCheckpoint(CHECKPOINT_KEY_ONBOARDING_WATERMARK, now);
147
+ setMemoryCheckpoint(CHECKPOINT_KEY_AUTH_FALLBACK_WATERMARK, now);
142
148
  return;
143
149
  }
144
150
 
@@ -171,6 +177,14 @@ export class UsageTelemetryReporter {
171
177
  getMemoryCheckpoint(CHECKPOINT_KEY_ONBOARDING_WATERMARK_ID) ??
172
178
  undefined;
173
179
 
180
+ // Read auth-fallback watermark (compound cursor: createdAt + id)
181
+ const authFallbackWatermark = Number(
182
+ getMemoryCheckpoint(CHECKPOINT_KEY_AUTH_FALLBACK_WATERMARK) ?? "0",
183
+ );
184
+ const authFallbackWatermarkId =
185
+ getMemoryCheckpoint(CHECKPOINT_KEY_AUTH_FALLBACK_WATERMARK_ID) ??
186
+ undefined;
187
+
174
188
  // Query unreported events
175
189
  const events = queryUnreportedUsageEvents(
176
190
  watermark,
@@ -192,12 +206,18 @@ export class UsageTelemetryReporter {
192
206
  onboardingWatermarkId,
193
207
  BATCH_SIZE,
194
208
  );
209
+ const authFallbackEvents = queryUnreportedAuthFallbackEvents(
210
+ authFallbackWatermark,
211
+ authFallbackWatermarkId,
212
+ BATCH_SIZE,
213
+ );
195
214
 
196
215
  if (
197
216
  events.length === 0 &&
198
217
  turnEvents.length === 0 &&
199
218
  lifecycleEvents.length === 0 &&
200
- onboardingEvents.length === 0
219
+ onboardingEvents.length === 0 &&
220
+ authFallbackEvents.length === 0
201
221
  )
202
222
  return;
203
223
 
@@ -211,6 +231,7 @@ export class UsageTelemetryReporter {
211
231
  turnCount: turnEvents.length,
212
232
  lifecycleCount: lifecycleEvents.length,
213
233
  onboardingCount: onboardingEvents.length,
234
+ authFallbackCount: authFallbackEvents.length,
214
235
  },
215
236
  "Telemetry flush: resolved auth context",
216
237
  );
@@ -337,6 +358,25 @@ export class UsageTelemetryReporter {
337
358
  assistant_version: APP_VERSION,
338
359
  }),
339
360
  ),
361
+ ...authFallbackEvents.map(
362
+ (e): TelemetryEvent => ({
363
+ type: "auth_fallback",
364
+ daemon_event_id: e.id,
365
+ recorded_at: e.createdAt,
366
+ guard: e.guard,
367
+ path: e.path,
368
+ failure_kind: e.failureKind,
369
+ count: e.count,
370
+ window_start: e.windowStart,
371
+ window_end: e.windowEnd,
372
+ // Aggregated counts forwarded by the gateway carry no record-time
373
+ // binary version; stamp the running binary's `APP_VERSION` so the
374
+ // wire value is concrete rather than an explicit null that would
375
+ // override the envelope under the platform's per-event-wins
376
+ // contract.
377
+ assistant_version: APP_VERSION,
378
+ }),
379
+ ),
340
380
  ];
341
381
 
342
382
  const organizationId = getPlatformOrganizationId() || undefined;
@@ -422,12 +462,27 @@ export class UsageTelemetryReporter {
422
462
  );
423
463
  }
424
464
 
465
+ // Advance auth-fallback watermark (compound cursor)
466
+ if (authFallbackEvents.length > 0) {
467
+ const lastAuthFallback =
468
+ authFallbackEvents[authFallbackEvents.length - 1];
469
+ setMemoryCheckpoint(
470
+ CHECKPOINT_KEY_AUTH_FALLBACK_WATERMARK,
471
+ String(lastAuthFallback.createdAt),
472
+ );
473
+ setMemoryCheckpoint(
474
+ CHECKPOINT_KEY_AUTH_FALLBACK_WATERMARK_ID,
475
+ lastAuthFallback.id,
476
+ );
477
+ }
478
+
425
479
  // If we got a full batch of any type, there may be more — recurse
426
480
  if (
427
481
  events.length === BATCH_SIZE ||
428
482
  turnEvents.length === BATCH_SIZE ||
429
483
  lifecycleEvents.length === BATCH_SIZE ||
430
- onboardingEvents.length === BATCH_SIZE
484
+ onboardingEvents.length === BATCH_SIZE ||
485
+ authFallbackEvents.length === BATCH_SIZE
431
486
  ) {
432
487
  await this._doFlush(batchCount + 1);
433
488
  }
@@ -1,18 +1,10 @@
1
1
  import { readFileSync } from "node:fs";
2
2
 
3
- import { parseChannelId } from "../channels/types.js";
4
3
  import { getConfig } from "../config/loader.js";
5
4
  import { bridgeCesApproval } from "../credential-execution/approval-bridge.js";
6
5
  import { isCesShellLockdownEnabled } from "../credential-execution/feature-gates.js";
7
6
  import { PermissionPrompter } from "../permissions/prompter.js";
8
7
  import { RiskLevel } from "../permissions/types.js";
9
- import { runPipeline } from "../plugins/pipeline.js";
10
- import { getMiddlewaresFor } from "../plugins/registry.js";
11
- import type {
12
- ToolExecuteArgs,
13
- ToolExecuteResult,
14
- TurnContext,
15
- } from "../plugins/types.js";
16
8
  import { isUntrustedTrustClass } from "../runtime/actor-trust-resolver.js";
17
9
  import { redactSensitiveFields } from "../security/redaction.js";
18
10
  import { TokenExpiredError } from "../security/token-manager.js";
@@ -52,54 +44,10 @@ export class ToolExecutor {
52
44
  name: string,
53
45
  input: Record<string, unknown>,
54
46
  context: ToolContext,
55
- /**
56
- * Optional per-turn context threaded in by the agent loop. Production
57
- * sites propagate the orchestrator-built `TurnContext` (real
58
- * `conversationId`, trust cascade, attached `contextWindowManager`) so
59
- * middleware registered on the `toolExecute` pipeline sees the same
60
- * context every other pipeline slot uses. When omitted (CLI/test
61
- * invocations that call `ToolExecutor.execute` directly), the executor
62
- * synthesizes a fallback context from the {@link ToolContext}.
63
- */
64
- turnContext?: TurnContext,
65
47
  ): Promise<ToolExecutionResult> {
66
- // Prefer the orchestrator-supplied `turnContext` so the pipeline sees
67
- // the real conversation identity, per-turn trust, and context-window
68
- // manager. When absent (CLI / test invocations that bypass the agent
69
- // loop), synthesize a minimal context from the `ToolContext`.
70
- const turnCtx: TurnContext = turnContext ?? {
71
- requestId: context.requestId ?? "",
72
- conversationId: context.conversationId,
73
- turnIndex: 0,
74
- trust: {
75
- sourceChannel: parseChannelId(context.executionChannel) ?? "vellum",
76
- trustClass: context.trustClass,
77
- },
78
- };
79
-
80
- const middlewares = getMiddlewaresFor("toolExecute");
81
48
  const { name: executionName, input: executionInput } =
82
49
  resolveToolInvocationAlias(name, input, context.allowedToolNames);
83
- const pipelineArgs: ToolExecuteArgs = {
84
- name: executionName,
85
- input: executionInput,
86
- context,
87
- };
88
-
89
- // No pipeline-level timeout: `executeInternal` already wraps the real
90
- // tool invocation in `executeWithTimeout`, which is the sole enforcer
91
- // of the per-tool budget. The pipeline itself runs untimed so that
92
- // upstream phases (permission checks, approval waits, middleware) are
93
- // not racing the tool budget — only the actual tool call is. Runaway
94
- // middleware is a plugin-health concern handled by per-plugin timeouts,
95
- // not here.
96
- return runPipeline<ToolExecuteArgs, ToolExecuteResult>(
97
- "toolExecute",
98
- middlewares,
99
- (args) => this.executeInternal(args.name, args.input, args.context),
100
- pipelineArgs,
101
- turnCtx,
102
- );
50
+ return this.executeInternal(executionName, executionInput, context);
103
51
  }
104
52
 
105
53
  private async executeInternal(
@@ -1,5 +1,7 @@
1
1
  import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
+ import { WEB_SEARCH_BACKEND_FAILURE_MESSAGE } from "../web-search-error.js";
4
+
3
5
  // Mutable mock state - set per test
4
6
  let mockWebSearchProvider: string | undefined = "perplexity";
5
7
  let mockBraveSecureKey: string | undefined;
@@ -32,7 +34,9 @@ mock.module("../../../security/secure-keys.js", () => ({
32
34
  },
33
35
  }));
34
36
 
37
+ const realLogger = await import("../../../util/logger.js");
35
38
  mock.module("../../../util/logger.js", () => ({
39
+ ...realLogger,
36
40
  getLogger: () =>
37
41
  new Proxy({} as Record<string, unknown>, {
38
42
  get: () => () => {},
@@ -169,7 +173,9 @@ describe("web_search activity metadata", () => {
169
173
  expect(meta.provider).toBe("perplexity");
170
174
  expect(meta.resultCount).toBe(0);
171
175
  expect(meta.results).toEqual([]);
172
- expect(meta.errorMessage).toContain("rate limit exceeded");
176
+ // Post-retry rate limits now surface the centralized friendly recoverable
177
+ // copy (ATL-727) rather than provider-specific rate-limit wording.
178
+ expect(meta.errorMessage).toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
173
179
  });
174
180
 
175
181
  // ---- Tavily -------------------------------------------------------------
@@ -1,5 +1,7 @@
1
1
  import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
+ import { WEB_SEARCH_BACKEND_FAILURE_MESSAGE } from "../web-search-error.js";
4
+
3
5
  // Mutable mock state - set per test
4
6
  let mockWebSearchProvider: string | undefined = "perplexity";
5
7
  let mockWebSearchMode: string | undefined = "your-own";
@@ -42,7 +44,9 @@ mock.module("../../../security/secure-keys.js", () => ({
42
44
  },
43
45
  }));
44
46
 
47
+ const realLogger = await import("../../../util/logger.js");
45
48
  mock.module("../../../util/logger.js", () => ({
49
+ ...realLogger,
46
50
  getLogger: () =>
47
51
  new Proxy({} as Record<string, unknown>, {
48
52
  get: () => () => {},
@@ -201,7 +205,8 @@ describe("web_search tool", () => {
201
205
 
202
206
  const result = await execute({ query: "test" });
203
207
  expect(result.isError).toBe(true);
204
- expect(result.content).toContain("rate limit exceeded");
208
+ // Post-retry rate limits surface the friendly recoverable copy (ATL-727).
209
+ expect(result.content).toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
205
210
  // 1 initial + 3 retries = 4 calls
206
211
  expect(callCount).toBe(4);
207
212
  });
@@ -214,7 +219,9 @@ describe("web_search tool", () => {
214
219
 
215
220
  const result = await execute({ query: "test" });
216
221
  expect(result.isError).toBe(true);
217
- expect(result.content).toContain("status 500");
222
+ // 5xx is a backend failure -> friendly recoverable copy, no raw status.
223
+ expect(result.content).toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
224
+ expect(result.content).not.toContain("500");
218
225
  });
219
226
 
220
227
  // ---- Brave provider -----------------------------------------------------
@@ -673,7 +680,8 @@ describe("web_search tool", () => {
673
680
 
674
681
  const result = await execute({ query: "test" });
675
682
  expect(result.isError).toBe(true);
676
- expect(result.content).toContain("rate limit exceeded");
683
+ // Post-retry rate limits surface the friendly recoverable copy (ATL-727).
684
+ expect(result.content).toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
677
685
  expect(callCount).toBe(4);
678
686
  });
679
687
 
@@ -0,0 +1,248 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { createAbortReason } from "../../util/abort-reasons.js";
4
+ import {
5
+ classifyWebSearchFailure,
6
+ logWebSearchBackendFailure,
7
+ WEB_SEARCH_BACKEND_FAILURE_MESSAGE,
8
+ } from "./web-search-error.js";
9
+
10
+ describe("classifyWebSearchFailure", () => {
11
+ test("Anthropic 'unavailable' code is a backend failure with friendly copy", () => {
12
+ const result = classifyWebSearchFailure({
13
+ isError: true,
14
+ errorCode: "unavailable",
15
+ });
16
+ expect(result.category).toBe("backend_unavailable");
17
+ expect(result.isBackendFailure).toBe(true);
18
+ expect(result.userMessage).toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
19
+ expect(result.rawDetail).toContain("unavailable");
20
+ });
21
+
22
+ test.each(["internal_error", "overloaded_error"])(
23
+ "Anthropic '%s' code is a backend failure",
24
+ (errorCode) => {
25
+ const result = classifyWebSearchFailure({ isError: true, errorCode });
26
+ expect(result.category).toBe("backend_unavailable");
27
+ expect(result.isBackendFailure).toBe(true);
28
+ },
29
+ );
30
+
31
+ test("HTTP 503 is a backend failure", () => {
32
+ const result = classifyWebSearchFailure({ isError: true, statusCode: 503 });
33
+ expect(result.category).toBe("backend_unavailable");
34
+ expect(result.userMessage).toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
35
+ });
36
+
37
+ test("thrown TypeError('fetch failed') is a backend failure", () => {
38
+ const result = classifyWebSearchFailure({
39
+ isError: true,
40
+ error: new TypeError("fetch failed"),
41
+ });
42
+ expect(result.category).toBe("backend_unavailable");
43
+ expect(result.isBackendFailure).toBe(true);
44
+ });
45
+
46
+ test("AbortError-shaped timeout is a backend failure", () => {
47
+ const err = new Error("The operation was aborted due to timeout");
48
+ err.name = "AbortError";
49
+ const result = classifyWebSearchFailure({ isError: true, error: err });
50
+ expect(result.category).toBe("backend_unavailable");
51
+ expect(result.isBackendFailure).toBe(true);
52
+ });
53
+
54
+ test("user-initiated abort is not a backend failure", () => {
55
+ const err = new Error("The operation was aborted");
56
+ err.name = "AbortError";
57
+ Object.assign(err, {
58
+ reason: createAbortReason("user_cancel", "cancelGeneration"),
59
+ });
60
+ const result = classifyWebSearchFailure({ isError: true, error: err });
61
+ expect(result.isBackendFailure).toBe(false);
62
+ expect(result.userMessage).not.toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
63
+ });
64
+
65
+ test("wrapped ProviderError carrying abortReason is not a backend failure", () => {
66
+ // A provider wrapper erases the AbortError name and re-words the message,
67
+ // but carries the tagged reason on `abortReason` (ProviderError shape).
68
+ const err = Object.assign(new Error("Request was aborted"), {
69
+ name: "ProviderError",
70
+ abortReason: createAbortReason("user_cancel", "cancelGeneration"),
71
+ });
72
+ const result = classifyWebSearchFailure({ isError: true, error: err });
73
+ expect(result.category).not.toBe("backend_unavailable");
74
+ expect(result.isBackendFailure).toBe(false);
75
+ expect(result.userMessage).not.toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
76
+ });
77
+
78
+ test("tagged abort with a transport-shaped cause is not a backend failure", () => {
79
+ // A user cancellation wrapped as a ProviderError that ALSO carries a
80
+ // transport-shaped `cause` (ECONNRESET). The tagged abort guard must win
81
+ // over transport-retryability so this is not mislabeled a backend outage.
82
+ const err = Object.assign(new Error("Request was aborted"), {
83
+ name: "ProviderError",
84
+ cause: { code: "ECONNRESET" },
85
+ abortReason: createAbortReason("user_cancel", "cancelGeneration"),
86
+ });
87
+ const result = classifyWebSearchFailure({ isError: true, error: err });
88
+ expect(result.category).not.toBe("backend_unavailable");
89
+ expect(result.isBackendFailure).toBe(false);
90
+ expect(result.userMessage).not.toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
91
+ });
92
+
93
+ test("explicit statusCode wins over a misleading error-body keyword", () => {
94
+ // The provider response body contains "aborted" (a keyword the error-body
95
+ // heuristic would sniff as a non-failure), but the authoritative HTTP 503
96
+ // must classify this as a backend failure.
97
+ const result = classifyWebSearchFailure({
98
+ isError: true,
99
+ statusCode: 503,
100
+ error: new Error("the upstream request was aborted unexpectedly"),
101
+ });
102
+ expect(result.category).toBe("backend_unavailable");
103
+ expect(result.isBackendFailure).toBe(true);
104
+ expect(result.userMessage).toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
105
+ });
106
+
107
+ test("ECONNRESET network error is a backend failure", () => {
108
+ const err = Object.assign(new Error("socket hang up"), {
109
+ code: "ECONNRESET",
110
+ });
111
+ const result = classifyWebSearchFailure({ isError: true, error: err });
112
+ expect(result.category).toBe("backend_unavailable");
113
+ });
114
+
115
+ test("Anthropic 'too_many_requests' code is rate-limited with backend copy", () => {
116
+ const result = classifyWebSearchFailure({
117
+ isError: true,
118
+ errorCode: "too_many_requests",
119
+ });
120
+ expect(result.category).toBe("rate_limited");
121
+ expect(result.isBackendFailure).toBe(true);
122
+ expect(result.userMessage).toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
123
+ });
124
+
125
+ test("HTTP 429 is rate-limited with backend copy", () => {
126
+ const result = classifyWebSearchFailure({ isError: true, statusCode: 429 });
127
+ expect(result.category).toBe("rate_limited");
128
+ expect(result.userMessage).toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
129
+ });
130
+
131
+ test("'query_too_long' has a distinct, non-backend message", () => {
132
+ const result = classifyWebSearchFailure({
133
+ isError: true,
134
+ errorCode: "query_too_long",
135
+ });
136
+ expect(result.category).toBe("query_too_long");
137
+ expect(result.isBackendFailure).toBe(false);
138
+ expect(result.userMessage).not.toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
139
+ expect(result.userMessage.length).toBeGreaterThan(0);
140
+ });
141
+
142
+ test("'max_uses_exceeded' has a distinct, non-backend message", () => {
143
+ const result = classifyWebSearchFailure({
144
+ isError: true,
145
+ errorCode: "max_uses_exceeded",
146
+ });
147
+ expect(result.category).toBe("max_uses_exceeded");
148
+ expect(result.isBackendFailure).toBe(false);
149
+ expect(result.userMessage).not.toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
150
+ expect(result.userMessage.length).toBeGreaterThan(0);
151
+ });
152
+
153
+ test("'invalid_input' is unknown and not a backend failure", () => {
154
+ const result = classifyWebSearchFailure({
155
+ isError: true,
156
+ errorCode: "invalid_input",
157
+ });
158
+ expect(result.category).toBe("unknown");
159
+ expect(result.isBackendFailure).toBe(false);
160
+ });
161
+
162
+ test("HTTP 401 is a config failure, not the backend copy", () => {
163
+ const result = classifyWebSearchFailure({ isError: true, statusCode: 401 });
164
+ expect(result.category).toBe("config");
165
+ expect(result.isBackendFailure).toBe(false);
166
+ expect(result.userMessage).not.toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
167
+ });
168
+
169
+ test("HTTP 403 is a config failure", () => {
170
+ const result = classifyWebSearchFailure({ isError: true, statusCode: 403 });
171
+ expect(result.category).toBe("config");
172
+ expect(result.isBackendFailure).toBe(false);
173
+ });
174
+
175
+ test("successful-but-empty result is no_results, not a failure", () => {
176
+ const result = classifyWebSearchFailure({
177
+ isError: false,
178
+ hasResults: false,
179
+ });
180
+ expect(result.category).toBe("no_results");
181
+ expect(result.isBackendFailure).toBe(false);
182
+ expect(result.userMessage).not.toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
183
+ });
184
+
185
+ test("rawDetail is truncated to 500 chars", () => {
186
+ const result = classifyWebSearchFailure({
187
+ isError: true,
188
+ error: new Error("x".repeat(1000)),
189
+ });
190
+ expect(result.rawDetail.length).toBeLessThanOrEqual(500 + 40);
191
+ expect(result.rawDetail).toContain("truncated");
192
+ });
193
+ });
194
+
195
+ describe("WEB_SEARCH_BACKEND_FAILURE_MESSAGE copy safety", () => {
196
+ test("offers retry / continue-without / paste", () => {
197
+ expect(WEB_SEARCH_BACKEND_FAILURE_MESSAGE).toContain("try again");
198
+ expect(WEB_SEARCH_BACKEND_FAILURE_MESSAGE).toContain("continue without");
199
+ expect(WEB_SEARCH_BACKEND_FAILURE_MESSAGE).toContain("paste");
200
+ });
201
+
202
+ test("contains no raw provider details, JSON, or exception names", () => {
203
+ for (const banned of [
204
+ "{",
205
+ "error_code",
206
+ "Anthropic",
207
+ "web_search_tool_result_error",
208
+ "TypeError",
209
+ "stack",
210
+ ]) {
211
+ expect(WEB_SEARCH_BACKEND_FAILURE_MESSAGE).not.toContain(banned);
212
+ }
213
+ });
214
+ });
215
+
216
+ describe("logWebSearchBackendFailure", () => {
217
+ test("captures the event and rawDetail without raw query text", () => {
218
+ const calls: unknown[][] = [];
219
+ const fakeLogger = {
220
+ warn: (...args: unknown[]) => {
221
+ calls.push(args);
222
+ },
223
+ } as unknown as Parameters<typeof logWebSearchBackendFailure>[0];
224
+
225
+ const secretQuery = "super secret user query text";
226
+ logWebSearchBackendFailure(fakeLogger, {
227
+ provider: "anthropic",
228
+ requestId: "req-1",
229
+ errorCategory: "backend_unavailable",
230
+ rawDetail: "errorCode=unavailable",
231
+ fallbackShown: true,
232
+ queryLength: secretQuery.length,
233
+ });
234
+
235
+ expect(calls).toHaveLength(1);
236
+ const [payload, msg] = calls[0] as [Record<string, unknown>, string];
237
+ expect(payload.event).toBe("web_search_backend_failure");
238
+ expect(payload.tool).toBe("web_search");
239
+ expect(payload.provider).toBe("anthropic");
240
+ expect(payload.rawDetail).toBe("errorCode=unavailable");
241
+ expect(payload.queryLength).toBe(secretQuery.length);
242
+ expect(msg).toBe("web_search backend failure");
243
+
244
+ // The raw query text must never appear anywhere in the logged payload.
245
+ const serialized = JSON.stringify(calls);
246
+ expect(serialized).not.toContain(secretQuery);
247
+ });
248
+ });