@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
@@ -87,6 +87,14 @@ export const DaemonConfigSchema = z
87
87
  .describe(
88
88
  "Whether the daemon records conversations even when no client is connected",
89
89
  ),
90
+ reapOrphanedSubprocesses: z
91
+ .boolean({
92
+ error: "daemon.reapOrphanedSubprocesses must be a boolean",
93
+ })
94
+ .default(false)
95
+ .describe(
96
+ "Whether the daemon, when running as PID 1 in a container, periodically reaps orphaned subprocesses that reparented to it. Off by default while the behavior is being validated.",
97
+ ),
90
98
  })
91
99
  .describe("Background daemon process configuration");
92
100
 
@@ -58,7 +58,7 @@ const MANAGED_PROFILE_TEMPLATES: Record<string, ManagedProfileTemplate> = {
58
58
  label: "Quality",
59
59
  description: "Best results with the most capable model",
60
60
  maxTokens: 32000,
61
- effort: "max",
61
+ effort: "high",
62
62
  thinking: { enabled: true, streamThinking: true },
63
63
  contextWindow: { maxInputTokens: DEFAULT_CONTEXT_WINDOW_MAX_INPUT_TOKENS },
64
64
  },
@@ -104,7 +104,7 @@ const USER_PROFILE_TEMPLATES: Record<string, ManagedProfileTemplate> = {
104
104
  label: "Quality",
105
105
  description: "Best results with the most capable model",
106
106
  maxTokens: 32000,
107
- effort: "max",
107
+ effort: "high",
108
108
  thinking: { enabled: true, streamThinking: true },
109
109
  contextWindow: { maxInputTokens: DEFAULT_CONTEXT_WINDOW_MAX_INPUT_TOKENS },
110
110
  },
@@ -52,6 +52,7 @@ const VellumMetadataSchema = z
52
52
 
53
53
  const SkillMetadataSchema = z
54
54
  .object({
55
+ icon: z.string().optional(),
55
56
  emoji: z.string().optional(),
56
57
  vellum: VellumMetadataSchema.optional(),
57
58
  })
@@ -210,6 +211,7 @@ interface ParsedFrontmatter {
210
211
  displayName: string;
211
212
  description: string;
212
213
  body: string;
214
+ icon?: string;
213
215
  emoji?: string;
214
216
  includes?: string[];
215
217
  featureFlag?: string;
@@ -253,6 +255,7 @@ function parseFrontmatter(
253
255
  }
254
256
 
255
257
  // metadata is already a parsed object from YAML — validate with Zod schema
258
+ let icon: string | undefined;
256
259
  let emoji: string | undefined;
257
260
  let parsedMeta: z.infer<typeof SkillMetadataSchema> | undefined;
258
261
  let vellum: z.infer<typeof VellumMetadataSchema> | undefined;
@@ -271,6 +274,7 @@ function parseFrontmatter(
271
274
  if (zodResult.success) {
272
275
  parsedMeta = zodResult.data;
273
276
  vellum = parsedMeta.vellum;
277
+ icon = parsedMeta.icon;
274
278
  emoji = vellum?.emoji ?? parsedMeta.emoji;
275
279
  } else {
276
280
  // Zod validation failed — fall back to raw parsed object so we don't
@@ -284,6 +288,9 @@ function parseFrontmatter(
284
288
  const raw = metadataRaw as Record<string, unknown>;
285
289
  parsedMeta = raw as z.infer<typeof SkillMetadataSchema>;
286
290
  vellum = raw?.vellum as z.infer<typeof VellumMetadataSchema>;
291
+ if (typeof parsedMeta?.icon === "string") {
292
+ icon = parsedMeta.icon;
293
+ }
287
294
  if (vellum && typeof vellum === "object") {
288
295
  emoji = typeof vellum.emoji === "string" ? vellum.emoji : undefined;
289
296
  }
@@ -337,6 +344,7 @@ function parseFrontmatter(
337
344
  displayName,
338
345
  description,
339
346
  body: strippedBody,
347
+ icon,
340
348
  emoji,
341
349
  includes,
342
350
  featureFlag,
@@ -489,6 +497,7 @@ function readSkillFromDirectory(
489
497
  directoryPath,
490
498
  skillFilePath,
491
499
  body: parsed.body,
500
+ icon: parsed.icon,
492
501
  emoji: parsed.emoji,
493
502
 
494
503
  source,
@@ -540,6 +549,7 @@ function readBundledSkillFromDirectory(
540
549
  skillFilePath,
541
550
  body: parsed.body,
542
551
  bundled: true,
552
+ icon: parsed.icon,
543
553
  emoji: parsed.emoji,
544
554
 
545
555
  source: "bundled",
@@ -599,6 +609,7 @@ function loadBundledSkills(): SkillSummary[] {
599
609
  directoryPath: skill.directoryPath,
600
610
  skillFilePath: skill.skillFilePath,
601
611
  bundled: true,
612
+ icon: skill.icon,
602
613
  emoji: skill.emoji,
603
614
 
604
615
  source: "bundled",
@@ -649,6 +660,7 @@ function skillSummaryFromDefinition(
649
660
  directoryPath: skill.directoryPath,
650
661
  skillFilePath: skill.skillFilePath,
651
662
  bundled: skill.bundled,
663
+ icon: skill.icon,
652
664
  emoji: skill.emoji,
653
665
  source,
654
666
  toolManifest: skill.toolManifest,
@@ -836,6 +848,7 @@ export function loadSkillCatalog(
836
848
  description: parsed.description,
837
849
  directoryPath: directory,
838
850
  skillFilePath,
851
+ icon: parsed.icon,
839
852
  emoji: parsed.emoji,
840
853
 
841
854
  source: "workspace",
@@ -21,7 +21,6 @@
21
21
  import { optimizeImageForTransport } from "../agent/image-optimize.js";
22
22
  import type { CompactionConfig } from "../config/schemas/compaction.js";
23
23
  import type { LLMCallSite } from "../config/schemas/llm.js";
24
- import { stripInjectionsForCompaction } from "../daemon/conversation-runtime-assembly.js";
25
24
  import { filterMessagesForUntrustedActor } from "../daemon/message-provenance.js";
26
25
  import {
27
26
  getAttachmentContent,
@@ -42,6 +41,7 @@ import {
42
41
  type TrustClass,
43
42
  } from "../runtime/actor-trust-resolver.js";
44
43
  import { getLogger } from "../util/logger.js";
44
+ import { stripInjectionsForCompaction } from "./strip-injections.js";
45
45
  import { estimatePromptTokens } from "./token-estimator.js";
46
46
 
47
47
  const log = getLogger("compactor");
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Runtime-injection stripping for compaction and overflow recovery.
3
+ *
4
+ * Runtime injections (turn context, memory, NOW.md, workspace, Slack
5
+ * chronological, etc.) persist in message history to keep the conversation
6
+ * prefix stable for Anthropic's prefix caching. They only need to be removed
7
+ * when compaction rewrites the message array, so the compactor summarizes the
8
+ * raw persistent messages rather than the ephemeral injected blocks.
9
+ *
10
+ * This module is the compaction-layer home for that strip so both the agent
11
+ * loop (which drives the compaction pipeline) and the compactor can call it
12
+ * without reaching up into the daemon orchestrator.
13
+ */
14
+ import type { Message } from "../providers/types.js";
15
+
16
+ /**
17
+ * A matcher for an injected text block. A plain string matches by prefix
18
+ * (`startsWith`). A `{ prefix, suffix }` wrapper requires BOTH the opening
19
+ * prefix and the closing suffix, so user-authored content that merely begins
20
+ * with an injection-like opening tag (e.g. a message discussing `<info>`
21
+ * markup) is not mistaken for an injected block and dropped. This mirrors
22
+ * `countMemoryPrefixBlocks`, which only treats `<memory>…</memory>` /
23
+ * `<info>…</info>` blocks as injected when the full wrapper is present.
24
+ */
25
+ export type InjectionMatcher = string | { prefix: string; suffix: string };
26
+
27
+ /**
28
+ * Remove text blocks from user messages that match any of the given matchers.
29
+ * If stripping removes all content blocks from a message, the message itself
30
+ * is dropped.
31
+ *
32
+ * This is the shared primitive behind the individual strip* functions and
33
+ * the `stripInjectionsForCompaction` pipeline.
34
+ */
35
+ export function stripUserTextBlocksByPrefix(
36
+ messages: Message[],
37
+ matchers: InjectionMatcher[],
38
+ ): Message[] {
39
+ return messages
40
+ .map((message) => {
41
+ if (message.role !== "user") return message;
42
+ const nextContent = message.content.filter((block) => {
43
+ if (block.type !== "text") return true;
44
+ return !matchers.some((m) =>
45
+ typeof m === "string"
46
+ ? block.text.startsWith(m)
47
+ : block.text.startsWith(m.prefix) && block.text.endsWith(m.suffix),
48
+ );
49
+ });
50
+ if (nextContent.length === message.content.length) return message;
51
+ if (nextContent.length === 0) return null;
52
+ return { ...message, content: nextContent };
53
+ })
54
+ .filter(
55
+ (message): message is NonNullable<typeof message> => message != null,
56
+ );
57
+ }
58
+
59
+ /** Matchers stripped by the pipeline (order doesn't matter — single pass). */
60
+ const RUNTIME_INJECTION_PREFIXES: InjectionMatcher[] = [
61
+ "<channel_capabilities>",
62
+ "<channel_command_context>",
63
+ "<disk_pressure_warning>",
64
+ "<channel_turn_context>", // backward-compat: strip legacy separate channel blocks
65
+ "<guardian_context>",
66
+ "<inbound_actor_context>", // backward-compat: strip legacy separate actor blocks
67
+ "<interface_turn_context>", // backward-compat: strip legacy separate interface blocks
68
+ // NOTE: <turn_context> is intentionally NOT stripped — unified turn context
69
+ // blocks persist in history so the assistant retains temporal/actor grounding.
70
+ "<background_turn>",
71
+ "<memory_context __injected>",
72
+ "<memory_context>", // backward-compat: strip legacy blocks from pre-__injected history
73
+ // The static `memory-v2-static` block (`<info>\n…</info>`) and the
74
+ // dynamic activation block (`<memory>\n…</memory>`, plus legacy
75
+ // `<memory __injected>…`) are both stripped so each compaction
76
+ // re-injects the freshest essentials/threads/recent/buffer view and
77
+ // re-runs the activation pipeline, matching the `<knowledge_base>`
78
+ // cadence. The activation pipeline dedupes via `everInjected`, and
79
+ // compaction handles aggregate growth, so accumulation does not cause
80
+ // unbounded context growth. Both wrappers may appear in persisted rows.
81
+ //
82
+ // These two use the full `{ prefix, suffix }` wrapper shape (not a bare
83
+ // prefix) so that user-authored text merely starting with `<memory>\n` or
84
+ // `<info>\n` is never silently dropped during compaction/`/clean`. This
85
+ // matches the full-wrapper requirement in `countMemoryPrefixBlocks`.
86
+ { prefix: "<memory>\n", suffix: "\n</memory>" },
87
+ { prefix: "<info>\n", suffix: "\n</info>" },
88
+ "<voice_call_control>",
89
+ "<workspace_top_level>", // backward-compat: strip legacy workspace blocks
90
+ // NOTE: <workspace> is intentionally NOT stripped — workspace context
91
+ // persists in history so the assistant retains workspace grounding.
92
+ "<temporal_context>\nToday:", // backward-compat: strip legacy temporal blocks
93
+ "<active_subagents>",
94
+ "<active_workspace>",
95
+ "<active_dynamic_page>",
96
+ "<non_interactive_context>",
97
+ // Shared prefix catches both the current NOW.md tag and any pre-line-limit
98
+ // variant that may linger in in-flight histories during a rolling deploy.
99
+ "<NOW.md Always keep this up to date",
100
+ "<now_scratchpad>", // backward-compat: strip legacy blocks from pre-rename history
101
+ "<knowledge_base>",
102
+ "<pkb>", // backward-compat: strip legacy tag from pre-rename history
103
+ "<system_reminder>",
104
+ "<transport_hints>",
105
+ // The Slack active-thread focus block is non-persisted and injected on
106
+ // the FINAL user turn only. Strip it here so re-assembly during compaction
107
+ // and overflow recovery does not duplicate it across turns.
108
+ "<active_thread>",
109
+ "<system_notice>One or more tool calls returned an error.",
110
+ ];
111
+
112
+ /**
113
+ * Strip all runtime-injected context from message history in a single pass.
114
+ *
115
+ * Used only during compaction and overflow recovery — not on normal turns.
116
+ * Runtime injections persist in history to keep the conversation prefix
117
+ * stable for Anthropic's prefix caching. Stripping is only needed when
118
+ * compaction rewrites the message array (cache miss is expected anyway).
119
+ */
120
+ export function stripInjectionsForCompaction(messages: Message[]): Message[] {
121
+ return stripUserTextBlocksByPrefix(messages, RUNTIME_INJECTION_PREFIXES);
122
+ }
@@ -359,6 +359,29 @@ export function estimatePromptTokens(
359
359
  return correction === 1.0 ? raw : Math.ceil(raw * correction);
360
360
  }
361
361
 
362
+ /**
363
+ * Calibrated prompt-token estimate including the tool-definition budget.
364
+ *
365
+ * Combines the per-tool budget ({@link estimateToolsTokens}) with the
366
+ * message/system estimate ({@link estimatePromptTokens}) under the EWMA
367
+ * calibration correction. This is the estimate the overflow gate consumes;
368
+ * the pre-send calibration capture in `agent/loop.ts` deliberately stays on
369
+ * `estimatePromptTokensRaw` so the calibrator trains against the uncorrected
370
+ * value rather than chasing its own output.
371
+ */
372
+ export function estimatePromptTokensWithTools(
373
+ history: Message[],
374
+ systemPrompt: string | undefined,
375
+ tools: ToolDefinition[],
376
+ providerName: string,
377
+ ): number {
378
+ const toolTokenBudget = tools.length > 0 ? estimateToolsTokens(tools) : 0;
379
+ return estimatePromptTokens(history, systemPrompt, {
380
+ providerName,
381
+ toolTokenBudget,
382
+ });
383
+ }
384
+
362
385
  function stableJson(value: unknown): string {
363
386
  try {
364
387
  return JSON.stringify(value);
@@ -5,29 +5,6 @@ import type {
5
5
  ToolResultContent,
6
6
  } from "../providers/types.js";
7
7
 
8
- /**
9
- * Maximum share of the context window that a single tool result may occupy.
10
- */
11
- const MAX_TOOL_RESULT_CONTEXT_SHARE = 0.3;
12
-
13
- /**
14
- * Absolute cap on tool-result characters (~100K tokens).
15
- */
16
- export const HARD_MAX_TOOL_RESULT_CHARS = 400_000;
17
-
18
- /**
19
- * Calculate the maximum allowed characters for a tool result based on the
20
- * context window size. Uses ~4 chars per token as a rough heuristic.
21
- */
22
- export function calculateMaxToolResultChars(
23
- contextWindowTokens: number,
24
- ): number {
25
- return Math.min(
26
- HARD_MAX_TOOL_RESULT_CHARS,
27
- Math.floor(contextWindowTokens * MAX_TOOL_RESULT_CONTEXT_SHARE * 4),
28
- );
29
- }
30
-
31
8
  /**
32
9
  * Aggressively truncate all tool-result text across an entire message history.
33
10
  *
@@ -90,7 +90,6 @@ export interface ShouldCompactResult {
90
90
  }
91
91
 
92
92
  export interface ContextWindowCompactOptions {
93
- lastCompactedAt?: number;
94
93
  /** Skip the auto-threshold check (used for /compact and recovery). */
95
94
  force?: boolean;
96
95
  /**
@@ -104,14 +103,12 @@ export interface ContextWindowCompactOptions {
104
103
  */
105
104
  precomputedEstimate?: number;
106
105
  /**
107
- * Legacy fields retained for backwards compatibility with existing
108
- * callers. The new assistant-driven compactor does not consume them
109
- * the model decides where to cut and what to keep — but accepting them
106
+ * Legacy field retained for backwards compatibility with existing
107
+ * callers. The new assistant-driven compactor does not consume it
108
+ * the model decides where to cut and what to keep — but accepting it
110
109
  * here lets callers keep their existing call sites unchanged.
111
110
  */
112
111
  minKeepRecentUserTurns?: number;
113
- conversationOriginChannel?: string;
114
- targetInputTokensOverride?: number;
115
112
  /**
116
113
  * Trust class of the actor whose turn triggered compaction. Forwarded to
117
114
  * the compactor so the image manifest excludes guardian-only attachments
@@ -17,6 +17,7 @@
17
17
  */
18
18
 
19
19
  import { existsSync } from "node:fs";
20
+ import { createRequire } from "node:module";
20
21
  import { dirname, join } from "node:path";
21
22
 
22
23
  import { getIsContainerized } from "../config/env-registry.js";
@@ -160,6 +161,21 @@ export function discoverLocalCes():
160
161
  return { mode: "local-source", sourcePath: sourceEntry };
161
162
  }
162
163
 
164
+ // npm-layout fallback: resolve via node_modules when installed as a package
165
+ try {
166
+ const _require = createRequire(import.meta.url);
167
+ const pkgPath = _require.resolve(
168
+ "@vellumai/credential-executor/package.json",
169
+ );
170
+ const npmSourceEntry = join(dirname(pkgPath), "src", "main.ts");
171
+ if (existsSync(npmSourceEntry)) {
172
+ log.info({ path: npmSourceEntry }, "Found CES source via npm package");
173
+ return { mode: "local-source", sourcePath: npmSourceEntry };
174
+ }
175
+ } catch {
176
+ // Package not installed — fall through to unavailable
177
+ }
178
+
163
179
  const reason = `CES executable not found. Searched: ${searchPaths.join(", ")}; also checked source at ${sourceEntry}`;
164
180
  log.warn(reason);
165
181
  return { mode: "unavailable", reason };
@@ -165,6 +165,12 @@ function makeDisposeContext(
165
165
  const ctx = {
166
166
  conversationId: overrides.conversationId ?? "conv-1",
167
167
  processing: false,
168
+ isProcessing(this: { processing: boolean }) {
169
+ return this.processing;
170
+ },
171
+ setProcessing(this: { processing: boolean }, value: boolean) {
172
+ this.processing = value;
173
+ },
168
174
  abortController,
169
175
  prompter,
170
176
  secretPrompter,
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Verifies that the inference-profile-change notification is persisted only
3
+ * once the model has actually received the turn context carrying the
4
+ * `model_profile` block — signalled by the first `message_complete` event from
5
+ * the agent loop — rather than inline before the provider call. A turn that
6
+ * fails or is cancelled before delivery never emits `message_complete`, so the
7
+ * notice is re-sent on the next turn instead of being silently suppressed.
8
+ */
9
+ import { describe, expect, mock, test } from "bun:test";
10
+
11
+ // ── Mock platform (must precede imports that read it) ────────────────────────
12
+ mock.module("../../util/logger.js", () => ({
13
+ getLogger: () =>
14
+ new Proxy({} as Record<string, unknown>, {
15
+ get: () => () => {},
16
+ }),
17
+ }));
18
+
19
+ mock.module("../../config/loader.js", () => ({
20
+ getConfig: () => ({ memory: {} }),
21
+ loadConfig: () => ({}),
22
+ }));
23
+
24
+ const setLastNotifiedInferenceProfile = mock(
25
+ (_conversationId: string, _profileKey: string) => {},
26
+ );
27
+
28
+ mock.module("../../memory/conversation-crud.js", () => ({
29
+ deleteMessageById: () => {},
30
+ getConversation: () => null,
31
+ getMessageById: () => null,
32
+ messageMetadataSchema: { safeParse: () => ({ success: false }) },
33
+ provenanceFromTrustContext: () => ({}),
34
+ reserveMessage: mock(async () => ({ id: "msg-reserve" })),
35
+ setConversationHistoryStrippedAt: () => {},
36
+ setLastNotifiedInferenceProfile,
37
+ updateMessageContent: () => {},
38
+ }));
39
+
40
+ mock.module("../../memory/llm-request-log-store.js", () => ({
41
+ backfillMessageIdOnLogs: () => {},
42
+ buildProviderErrorResponsePayload: () => ({}),
43
+ recordRequestLog: () => {},
44
+ setAgentLoopExitReasonOnLatestLog: () => {},
45
+ }));
46
+
47
+ mock.module("../../memory/memory-recall-log-store.js", () => ({
48
+ backfillMemoryRecallLogMessageId: () => {},
49
+ }));
50
+
51
+ mock.module("../../memory/memory-v2-activation-log-store.js", () => ({
52
+ backfillMemoryV2ActivationMessageId: () => {},
53
+ }));
54
+
55
+ // ── Imports (after mocks) ────────────────────────────────────────────────────
56
+ import type { AgentEvent } from "../../agent/loop.js";
57
+ import type { Message } from "../../providers/types.js";
58
+ import type {
59
+ EventHandlerDeps,
60
+ EventHandlerState,
61
+ } from "../conversation-agent-loop-handlers.js";
62
+ import {
63
+ createEventHandlerState,
64
+ dispatchAgentEvent,
65
+ } from "../conversation-agent-loop-handlers.js";
66
+
67
+ const CONVERSATION_ID = "conv-profile-notify";
68
+ const PROFILE_KEY = "quality-optimized";
69
+
70
+ function makeDeps(): EventHandlerDeps {
71
+ return {
72
+ ctx: {
73
+ conversationId: CONVERSATION_ID,
74
+ provider: { name: "mock-provider" },
75
+ traceEmitter: { emit: () => {} },
76
+ currentTurnSurfaces: [],
77
+ trustContext: undefined,
78
+ } as unknown as EventHandlerDeps["ctx"],
79
+ onEvent: () => {},
80
+ reqId: "req-profile-notify",
81
+ isFirstMessage: false,
82
+ shouldGenerateTitle: false,
83
+ rlog: new Proxy({} as Record<string, unknown>, {
84
+ get: () => () => {},
85
+ }) as unknown as EventHandlerDeps["rlog"],
86
+ turnChannelContext: {
87
+ userMessageChannel: "vellum",
88
+ assistantMessageChannel: "vellum",
89
+ } as EventHandlerDeps["turnChannelContext"],
90
+ turnInterfaceContext: {
91
+ userMessageInterface: "macos",
92
+ assistantMessageInterface: "macos",
93
+ } as EventHandlerDeps["turnInterfaceContext"],
94
+ applyCompaction: async () => {},
95
+ } as EventHandlerDeps;
96
+ }
97
+
98
+ function messageCompleteEvent(): Extract<
99
+ AgentEvent,
100
+ { type: "message_complete" }
101
+ > {
102
+ const message: Message = {
103
+ role: "assistant",
104
+ content: [{ type: "text", text: "response" }],
105
+ };
106
+ return { type: "message_complete", message };
107
+ }
108
+
109
+ // A reserved assistant row id is the precondition `handleMessageComplete`
110
+ // asserts before finalizing; the row reservation itself is exercised elsewhere.
111
+ function readyState(): EventHandlerState {
112
+ const state = createEventHandlerState();
113
+ state.lastAssistantMessageId = "assistant-row-1";
114
+ return state;
115
+ }
116
+
117
+ describe("inference-profile-change notification persistence", () => {
118
+ test("persists on the first message_complete and clears the pending slot", async () => {
119
+ setLastNotifiedInferenceProfile.mockClear();
120
+ const state = readyState();
121
+ state.pendingNotifiedInferenceProfile = PROFILE_KEY;
122
+
123
+ await dispatchAgentEvent(state, makeDeps(), messageCompleteEvent());
124
+
125
+ expect(setLastNotifiedInferenceProfile).toHaveBeenCalledTimes(1);
126
+ expect(setLastNotifiedInferenceProfile).toHaveBeenCalledWith(
127
+ CONVERSATION_ID,
128
+ PROFILE_KEY,
129
+ );
130
+ expect(state.pendingNotifiedInferenceProfile).toBeNull();
131
+ });
132
+
133
+ test("does not persist when the turn carries no profile-change notice", async () => {
134
+ setLastNotifiedInferenceProfile.mockClear();
135
+ const state = readyState();
136
+
137
+ await dispatchAgentEvent(state, makeDeps(), messageCompleteEvent());
138
+
139
+ expect(setLastNotifiedInferenceProfile).not.toHaveBeenCalled();
140
+ });
141
+
142
+ test("persists exactly once across a multi-call turn", async () => {
143
+ setLastNotifiedInferenceProfile.mockClear();
144
+ const state = readyState();
145
+ state.pendingNotifiedInferenceProfile = PROFILE_KEY;
146
+ const deps = makeDeps();
147
+
148
+ await dispatchAgentEvent(state, deps, messageCompleteEvent());
149
+ await dispatchAgentEvent(state, deps, messageCompleteEvent());
150
+
151
+ expect(setLastNotifiedInferenceProfile).toHaveBeenCalledTimes(1);
152
+ });
153
+ });
@@ -51,6 +51,7 @@ mock.module("../../memory/llm-request-log-store.js", () => ({
51
51
  }));
52
52
 
53
53
  // ── Imports (after mocks) ─────────────────────────────────────────────────────
54
+ import { WEB_SEARCH_BACKEND_FAILURE_MESSAGE } from "../../tools/network/web-search-error.js";
54
55
  import type {
55
56
  EventHandlerDeps,
56
57
  EventHandlerState,
@@ -95,6 +96,7 @@ function createCollectorDeps(providerName = "anthropic"): {
95
96
  userMessageInterface: "macos",
96
97
  assistantMessageInterface: "macos",
97
98
  } as EventHandlerDeps["turnInterfaceContext"],
99
+ applyCompaction: async () => {},
98
100
  } as EventHandlerDeps;
99
101
  return { deps, events };
100
102
  }
@@ -257,9 +259,7 @@ describe("native server_tool_complete metadata", () => {
257
259
  );
258
260
  expect(toolResults).toHaveLength(2);
259
261
 
260
- const byId = new Map(
261
- toolResults.map((r) => [r.toolUseId, r] as const),
262
- );
262
+ const byId = new Map(toolResults.map((r) => [r.toolUseId, r] as const));
263
263
  expect(byId.get("tu_a")?.activityMetadata?.webSearch?.query).toBe("alpha");
264
264
  expect(byId.get("tu_b")?.activityMetadata?.webSearch?.query).toBe("beta");
265
265
 
@@ -270,7 +270,7 @@ describe("native server_tool_complete metadata", () => {
270
270
  expect(durB).toBeGreaterThanOrEqual(durA);
271
271
  });
272
272
 
273
- test("forwards provider error codes instead of generic 'Search failed'", async () => {
273
+ test("maps recoverable error codes to friendly copy, not the raw code", async () => {
274
274
  const { deps, events } = createCollectorDeps();
275
275
  const toolUseId = "tu_err_code";
276
276
 
@@ -292,9 +292,11 @@ describe("native server_tool_complete metadata", () => {
292
292
  const toolResultEvent = events.find(
293
293
  (e): e is ToolResultEvent => e.type === "tool_result",
294
294
  );
295
- expect(toolResultEvent?.activityMetadata?.webSearch?.errorMessage).toBe(
296
- "max_uses_exceeded",
297
- );
295
+ const errorMessage =
296
+ toolResultEvent?.activityMetadata?.webSearch?.errorMessage;
297
+ // The raw provider code is never user-visible.
298
+ expect(errorMessage).not.toBe("max_uses_exceeded");
299
+ expect(errorMessage).toContain("web-search limit");
298
300
  });
299
301
 
300
302
  test("does NOT emit activityMetadata for non-Anthropic providers", async () => {
@@ -350,7 +352,7 @@ describe("native server_tool_complete metadata", () => {
350
352
  (e): e is ToolResultEvent => e.type === "tool_result",
351
353
  );
352
354
  const meta = toolResultEvent?.activityMetadata?.webSearch;
353
- expect(meta?.errorMessage).toBe("Search failed");
355
+ expect(meta?.errorMessage).toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
354
356
  expect(meta?.resultCount).toBe(0);
355
357
  expect(meta?.results).toEqual([]);
356
358
  expect(toolResultEvent?.isError).toBe(true);
@@ -8,7 +8,7 @@
8
8
  import { readFileSync, statSync } from "node:fs";
9
9
  import { basename } from "node:path";
10
10
 
11
- import { isPlaceholderSentinelText } from "../providers/anthropic/client.js";
11
+ import { isPlaceholderSentinelText } from "../providers/placeholder-sentinels.js";
12
12
  import {
13
13
  hostPolicy,
14
14
  sandboxPolicy,
@@ -178,8 +178,8 @@ export class ConfigWatcher {
178
178
  * Start all file watchers. `onConversationEvict` is called when watched
179
179
  * files change and conversations need to be evicted for reload.
180
180
  * `onIdentityChanged` is called when IDENTITY.md changes on disk.
181
- * `onIdentityIntroChanged` is called when SOUL.md changes and cached
182
- * identity intro greetings should be invalidated.
181
+ * `onIdentityIntroChanged` is called when SOUL.md changes and identity
182
+ * intro subscribers should refetch.
183
183
  * `onSkillsChanged` is called after skill directory changes evict
184
184
  * conversations.
185
185
  */
@@ -173,7 +173,6 @@ async function applyForcedCompaction(
173
173
  const compactionOptions: ContextWindowCompactOptions = {
174
174
  force: true,
175
175
  minKeepRecentUserTurns: 0,
176
- targetInputTokensOverride: config.targetTokens,
177
176
  };
178
177
 
179
178
  const result = await compactFn(messages, signal, compactionOptions);