@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
@@ -24,6 +24,7 @@ mock.module("../runtime/background-job-runner.js", () => ({
24
24
  prompt: string;
25
25
  groupId?: string;
26
26
  trustContext: { sourceChannel: string; trustClass: string };
27
+ onConversationCreated?: (conversationId: string) => void;
27
28
  }) => {
28
29
  const { createConversation } =
29
30
  await import("../memory/conversation-crud.js");
@@ -33,6 +34,7 @@ mock.module("../runtime/background-job-runner.js", () => ({
33
34
  source: "schedule",
34
35
  ...(opts.groupId ? { groupId: opts.groupId } : {}),
35
36
  });
37
+ opts.onConversationCreated?.(conv.id);
36
38
  onRunBackgroundJobCall?.({
37
39
  conversationId: conv.id,
38
40
  prompt: opts.prompt,
@@ -60,12 +62,17 @@ mock.module("../notifications/emit-signal.js", () => ({
60
62
 
61
63
  import { getDb } from "../memory/db-connection.js";
62
64
  import { initializeDb } from "../memory/db-init.js";
65
+ import { recordUsageEvent } from "../memory/llm-usage-store.js";
63
66
  import {
64
67
  createSchedule,
65
68
  getSchedule,
66
69
  getScheduleRuns,
67
70
  } from "../schedule/schedule-store.js";
68
- import { startScheduler } from "../schedule/scheduler.js";
71
+ import { getScheduleUsageSummaries } from "../schedule/schedule-usage-store.js";
72
+ import {
73
+ runScheduleDueWorkOnce,
74
+ startScheduler,
75
+ } from "../schedule/scheduler.js";
69
76
  import { scheduleTask } from "../tasks/task-scheduler.js";
70
77
  import { createTask } from "../tasks/task-store.js";
71
78
 
@@ -160,6 +167,7 @@ describe("scheduler run_task detection", () => {
160
167
  db.run("DELETE FROM cron_jobs");
161
168
  db.run("DELETE FROM task_runs");
162
169
  db.run("DELETE FROM tasks");
170
+ db.run("DELETE FROM llm_usage_events");
163
171
  db.run("DELETE FROM messages");
164
172
  db.run("DELETE FROM conversations");
165
173
  onRunBackgroundJobCall = null;
@@ -298,4 +306,157 @@ describe("scheduler run_task detection", () => {
298
306
  /^activity-failed:task:nonexistent-task-id:\d{4}-\d{2}-\d{2}$/,
299
307
  );
300
308
  });
309
+
310
+ test("opens a task-backed schedule run before task processing and backfills the real conversation id", async () => {
311
+ const task = createTask({
312
+ title: "Usage Attribution Task",
313
+ template: "Spend scheduled tokens",
314
+ });
315
+ const schedule = scheduleTask({
316
+ taskId: task.id,
317
+ name: "Usage Attribution Schedule",
318
+ cronExpression: "* * * * *",
319
+ });
320
+ forceScheduleDue(schedule.id);
321
+
322
+ const from = Date.now() - 1000;
323
+ let processingConversationId: string | null = null;
324
+ let usageEventCreatedAt: number | null = null;
325
+ let runsDuringProcessing: ReturnType<typeof getScheduleRuns> = [];
326
+
327
+ const result = await runScheduleDueWorkOnce(
328
+ async (conversationId) => {
329
+ processingConversationId = conversationId;
330
+ runsDuringProcessing = getScheduleRuns(schedule.id);
331
+ const event = recordUsageEvent(
332
+ {
333
+ conversationId,
334
+ runId: null,
335
+ requestId: "req-scheduled-task-usage",
336
+ actor: "main_agent",
337
+ callSite: "mainAgent",
338
+ inferenceProfile: "balanced",
339
+ provider: "anthropic",
340
+ model: "claude-sonnet-4-20250514",
341
+ inputTokens: 100,
342
+ outputTokens: 50,
343
+ cacheCreationInputTokens: 0,
344
+ cacheReadInputTokens: 0,
345
+ rawUsage: null,
346
+ },
347
+ { estimatedCostUsd: 0.25, pricingStatus: "priced" },
348
+ );
349
+ usageEventCreatedAt = event.createdAt;
350
+ },
351
+ () => {},
352
+ );
353
+ const to = Date.now() + 1000;
354
+
355
+ expect(result.completed).toBe(1);
356
+ expect(result.failed).toBe(0);
357
+ expect(processingConversationId).not.toBeNull();
358
+ expect(usageEventCreatedAt).not.toBeNull();
359
+ expect(runsDuringProcessing).toHaveLength(1);
360
+ expect(runsDuringProcessing[0].status).toBe("running");
361
+ expect(runsDuringProcessing[0].conversationId).toBeNull();
362
+ expect(runsDuringProcessing[0].startedAt).toBeLessThanOrEqual(
363
+ usageEventCreatedAt!,
364
+ );
365
+
366
+ const runs = getScheduleRuns(schedule.id);
367
+ expect(runs).toHaveLength(1);
368
+ expect(runs[0].id).toBe(runsDuringProcessing[0].id);
369
+ expect(runs[0].status).toBe("ok");
370
+ expect(runs[0].conversationId).toBe(processingConversationId);
371
+ expect(runs[0].startedAt).toBeLessThanOrEqual(usageEventCreatedAt!);
372
+ expect(runs[0].finishedAt).not.toBeNull();
373
+ expect(runs[0].finishedAt!).toBeGreaterThanOrEqual(usageEventCreatedAt!);
374
+
375
+ const summary = getScheduleUsageSummaries({ from, to }).find(
376
+ (row) => row.scheduleId === schedule.id,
377
+ );
378
+ expect(summary).toEqual({
379
+ scheduleId: schedule.id,
380
+ runCount: 1,
381
+ totalEstimatedCostUsd: 0.25,
382
+ eventCount: 1,
383
+ });
384
+ });
385
+
386
+ test("opens a normal execute schedule run before fresh background processing and records the conversation id", async () => {
387
+ const schedule = createSchedule({
388
+ name: "Usage Attribution Message Schedule",
389
+ cronExpression: "* * * * *",
390
+ message: "Spend scheduled message tokens",
391
+ syntax: "cron",
392
+ });
393
+ forceScheduleDue(schedule.id);
394
+
395
+ const from = Date.now() - 1000;
396
+ let processingConversationId: string | null = null;
397
+ let usageEventCreatedAt: number | null = null;
398
+ let runsDuringProcessing: ReturnType<typeof getScheduleRuns> = [];
399
+
400
+ onRunBackgroundJobCall = (info) => {
401
+ processingConversationId = info.conversationId;
402
+ runsDuringProcessing = getScheduleRuns(schedule.id);
403
+ const event = recordUsageEvent(
404
+ {
405
+ conversationId: info.conversationId,
406
+ runId: null,
407
+ requestId: "req-scheduled-message-usage",
408
+ actor: "main_agent",
409
+ callSite: "mainAgent",
410
+ inferenceProfile: "balanced",
411
+ provider: "anthropic",
412
+ model: "claude-sonnet-4-20250514",
413
+ inputTokens: 80,
414
+ outputTokens: 20,
415
+ cacheCreationInputTokens: 0,
416
+ cacheReadInputTokens: 0,
417
+ rawUsage: null,
418
+ },
419
+ { estimatedCostUsd: 0.1, pricingStatus: "priced" },
420
+ );
421
+ usageEventCreatedAt = event.createdAt;
422
+ };
423
+
424
+ const result = await runScheduleDueWorkOnce(
425
+ async () => {},
426
+ () => {},
427
+ );
428
+ const to = Date.now() + 1000;
429
+
430
+ expect(result.completed).toBe(1);
431
+ expect(result.failed).toBe(0);
432
+ expect(processingConversationId).not.toBeNull();
433
+ expect(usageEventCreatedAt).not.toBeNull();
434
+ expect(runsDuringProcessing).toHaveLength(1);
435
+ expect(runsDuringProcessing[0].status).toBe("running");
436
+ expect(runsDuringProcessing[0].conversationId).toBe(
437
+ processingConversationId,
438
+ );
439
+ expect(runsDuringProcessing[0].startedAt).toBeLessThanOrEqual(
440
+ usageEventCreatedAt!,
441
+ );
442
+
443
+ const runs = getScheduleRuns(schedule.id);
444
+ expect(runs).toHaveLength(1);
445
+ expect(runs[0].id).toBe(runsDuringProcessing[0].id);
446
+ expect(runs[0].status).toBe("ok");
447
+ expect(runs[0].conversationId).toBe(processingConversationId);
448
+ expect(runs[0].startedAt).toBeLessThanOrEqual(usageEventCreatedAt!);
449
+ expect(runs[0].finishedAt).not.toBeNull();
450
+ expect(runs[0].finishedAt!).toBeGreaterThanOrEqual(usageEventCreatedAt!);
451
+
452
+ const summary = getScheduleUsageSummaries({ from, to }).find(
453
+ (row) => row.scheduleId === schedule.id,
454
+ );
455
+ expect(summary).toEqual({
456
+ scheduleId: schedule.id,
457
+ runCount: 1,
458
+ totalEstimatedCostUsd: 0.1,
459
+ eventCount: 1,
460
+ });
461
+ });
301
462
  });
@@ -141,7 +141,7 @@ describe("buildSanitizedEnv", () => {
141
141
  expect(env.LC_CTYPE).toBe("UTF-8");
142
142
  });
143
143
 
144
- test("only includes Kata apt variables when sandbox runtime is kata", () => {
144
+ test("only includes Kata apt variables for Kata-family sandbox runtimes", () => {
145
145
  process.env.VELLUM_SANDBOX_RUNTIME = "gvisor";
146
146
  process.env.PATH = "/usr/bin";
147
147
  process.env.VELLUM_APT_DATA_ROOT = "/data/system";
@@ -162,6 +162,11 @@ describe("buildSanitizedEnv", () => {
162
162
  "/data/system/usr/local/lib",
163
163
  );
164
164
  expect(env.LD_LIBRARY_PATH.split(":")).not.toContain("/host/lib");
165
+
166
+ process.env.VELLUM_SANDBOX_RUNTIME = "cloud-hypervisor";
167
+ env = buildSanitizedEnv();
168
+ expect(env.VELLUM_APT_DATA_ROOT).toBe("/data/system");
169
+ expect(env.PATH.split(":")).toContain("/data/system/usr/bin");
165
170
  });
166
171
 
167
172
  test("defaults LANG and LC_ALL to UTF-8 when unset", () => {
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Tests for the default `title-generate` plugin's hooks.
3
+ *
4
+ * The plugin contributes two pure-trigger hooks that delegate the title work
5
+ * to the service:
6
+ *
7
+ * - `user-prompt-submit` — first-pass generation from the submitted prompt,
8
+ * scheduled on a later macrotask so the main agent-loop LLM request is
9
+ * issued first.
10
+ * - `stop` — second-pass regeneration once the conversation reaches its third
11
+ * user turn (turn count derived from the user prompts in history).
12
+ *
13
+ * Both let the title service resolve the provider, persist the title, and
14
+ * broadcast the resulting `conversation_title_updated` / `sync_changed`
15
+ * events.
16
+ *
17
+ * Mocks `memory/conversation-title-service.js` and `config/loader.js` so the
18
+ * tests don't touch the real provider stack or config, and resets the plugin
19
+ * registry between cases.
20
+ */
21
+
22
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
23
+
24
+ // Stub the title-generation service before importing anything that binds
25
+ // to it, so both the default plugin and the hooks capture the stubbed binding.
26
+ const queueGenerateConversationTitleMock = mock(
27
+ (_params: {
28
+ conversationId: string;
29
+ provider?: unknown;
30
+ userMessage?: string;
31
+ }): void => undefined,
32
+ );
33
+ const queueRegenerateConversationTitleMock = mock(
34
+ (_params: { conversationId: string; provider?: unknown }): void => undefined,
35
+ );
36
+ mock.module("../memory/conversation-title-service.js", () => ({
37
+ queueGenerateConversationTitle: queueGenerateConversationTitleMock,
38
+ queueRegenerateConversationTitle: queueRegenerateConversationTitleMock,
39
+ }));
40
+
41
+ // The `stop` hook reads `conversations.skipAutoRetitling`; stub the loader so
42
+ // the opt-out is controllable per test.
43
+ let skipAutoRetitling = false;
44
+ mock.module("../config/loader.js", () => ({
45
+ getConfig: () => ({ conversations: { skipAutoRetitling } }),
46
+ }));
47
+
48
+ import { HOOKS } from "../plugin-api/constants.js";
49
+ import type {
50
+ PluginLogger,
51
+ StopContext,
52
+ UserPromptSubmitContext,
53
+ } from "../plugin-api/types.js";
54
+ import stop from "../plugins/defaults/title-generate/hooks/stop.js";
55
+ import userPromptSubmit from "../plugins/defaults/title-generate/hooks/user-prompt-submit.js";
56
+ import { defaultTitleGeneratePlugin } from "../plugins/defaults/title-generate/register.js";
57
+ import { runHook } from "../plugins/pipeline.js";
58
+ import {
59
+ registerPlugin,
60
+ resetPluginRegistryForTests,
61
+ } from "../plugins/registry.js";
62
+ import type { Message } from "../providers/types.js";
63
+
64
+ const noopLogger: PluginLogger = {
65
+ info: () => {},
66
+ warn: () => {},
67
+ error: () => {},
68
+ debug: () => {},
69
+ };
70
+
71
+ function makeCtx(
72
+ overrides: Partial<UserPromptSubmitContext> = {},
73
+ ): UserPromptSubmitContext {
74
+ const messages: Message[] = [
75
+ { role: "user", content: [{ type: "text", text: "first message" }] },
76
+ ];
77
+ return {
78
+ conversationId: "conv-1",
79
+ prompt: "first message",
80
+ originalMessages: messages,
81
+ latestMessages: messages,
82
+ logger: noopLogger,
83
+ ...overrides,
84
+ };
85
+ }
86
+
87
+ /** Flush pending `setTimeout(0)` callbacks so the fire-and-forget trigger runs. */
88
+ function flushMacrotasks(): Promise<void> {
89
+ return new Promise((resolve) => setTimeout(resolve, 0));
90
+ }
91
+
92
+ function userTurn(text: string): Message {
93
+ return { role: "user", content: [{ type: "text", text }] };
94
+ }
95
+
96
+ function assistantTurn(text: string): Message {
97
+ return { role: "assistant", content: [{ type: "text", text }] };
98
+ }
99
+
100
+ /** A user-role message carrying only tool results, not a fresh prompt. */
101
+ function toolResultTurn(): Message {
102
+ return {
103
+ role: "user",
104
+ content: [{ type: "tool_result", tool_use_id: "tool-1", content: "ok" }],
105
+ };
106
+ }
107
+
108
+ /** History with `count` genuine user turns interleaved with assistant replies. */
109
+ function historyWithUserTurns(count: number): Message[] {
110
+ const messages: Message[] = [];
111
+ for (let i = 1; i <= count; i++) {
112
+ messages.push(userTurn(`message ${i}`));
113
+ messages.push(assistantTurn(`reply ${i}`));
114
+ }
115
+ return messages;
116
+ }
117
+
118
+ function makeStopCtx(overrides: Partial<StopContext> = {}): StopContext {
119
+ return {
120
+ conversationId: "conv-1",
121
+ messages: historyWithUserTurns(3),
122
+ responseContent: [{ type: "text", text: "reply 3" }],
123
+ stopReason: "end_turn",
124
+ decision: "stop",
125
+ logger: noopLogger,
126
+ ...overrides,
127
+ };
128
+ }
129
+
130
+ describe("title-generate user-prompt-submit hook", () => {
131
+ beforeEach(() => {
132
+ resetPluginRegistryForTests();
133
+ queueGenerateConversationTitleMock.mockReset();
134
+ queueGenerateConversationTitleMock.mockImplementation(() => undefined);
135
+ });
136
+
137
+ test("queues a title-generation job from the submitted prompt", async () => {
138
+ // GIVEN a fresh user prompt submission
139
+ const ctx = makeCtx({ conversationId: "conv-1", prompt: "first message" });
140
+
141
+ // WHEN the default hook runs and its deferred work flushes
142
+ await userPromptSubmit(ctx);
143
+ await flushMacrotasks();
144
+
145
+ // THEN the title service is invoked with just the conversation id and the
146
+ // submitted prompt — provider resolution and emit are owned by the service.
147
+ expect(queueGenerateConversationTitleMock).toHaveBeenCalledTimes(1);
148
+ const call = queueGenerateConversationTitleMock.mock.calls[0]?.[0];
149
+ expect(call?.conversationId).toBe("conv-1");
150
+ expect(call?.userMessage).toBe("first message");
151
+ expect(call).not.toHaveProperty("provider");
152
+ expect(call).not.toHaveProperty("onTitleUpdated");
153
+ });
154
+
155
+ test("does not block: returns before the title job is scheduled", async () => {
156
+ // GIVEN a fresh user prompt submission
157
+ const ctx = makeCtx();
158
+
159
+ // WHEN the hook resolves
160
+ await userPromptSubmit(ctx);
161
+
162
+ // THEN the title job has not run yet (it is deferred to a later macrotask),
163
+ // AND it runs once the macrotask queue is flushed.
164
+ expect(queueGenerateConversationTitleMock).toHaveBeenCalledTimes(0);
165
+ await flushMacrotasks();
166
+ expect(queueGenerateConversationTitleMock).toHaveBeenCalledTimes(1);
167
+ });
168
+
169
+ test("fires through runHook once the default plugin is registered", async () => {
170
+ // GIVEN the default title-generate plugin registered in the registry
171
+ registerPlugin(defaultTitleGeneratePlugin);
172
+
173
+ // WHEN a prompt is submitted through the hook chain
174
+ await runHook(
175
+ HOOKS.USER_PROMPT_SUBMIT,
176
+ makeCtx({ prompt: "draft a plan" }),
177
+ );
178
+ await flushMacrotasks();
179
+
180
+ // THEN the title service is triggered with the submitted prompt text
181
+ expect(queueGenerateConversationTitleMock).toHaveBeenCalledTimes(1);
182
+ expect(
183
+ queueGenerateConversationTitleMock.mock.calls[0]?.[0]?.userMessage,
184
+ ).toBe("draft a plan");
185
+ });
186
+ });
187
+
188
+ describe("title-generate stop hook", () => {
189
+ beforeEach(() => {
190
+ resetPluginRegistryForTests();
191
+ queueRegenerateConversationTitleMock.mockReset();
192
+ queueRegenerateConversationTitleMock.mockImplementation(() => undefined);
193
+ skipAutoRetitling = false;
194
+ });
195
+
196
+ test("regenerates the title on the third user turn", async () => {
197
+ // GIVEN a turn ending with three genuine user prompts in history
198
+ const ctx = makeStopCtx({ messages: historyWithUserTurns(3) });
199
+
200
+ // WHEN the stop hook runs and its deferred work flushes
201
+ await stop(ctx);
202
+ await flushMacrotasks();
203
+
204
+ // THEN the second-pass regeneration is triggered with just the
205
+ // conversation id — provider resolution and emit are owned by the service.
206
+ expect(queueRegenerateConversationTitleMock).toHaveBeenCalledTimes(1);
207
+ const call = queueRegenerateConversationTitleMock.mock.calls[0]?.[0];
208
+ expect(call?.conversationId).toBe("conv-1");
209
+ expect(call).not.toHaveProperty("provider");
210
+ expect(call).not.toHaveProperty("signal");
211
+ });
212
+
213
+ test("defers the regeneration so the completed turn is persisted first", async () => {
214
+ // GIVEN a turn ending on the third user turn
215
+ const ctx = makeStopCtx({ messages: historyWithUserTurns(3) });
216
+
217
+ // WHEN the hook resolves
218
+ await stop(ctx);
219
+
220
+ // THEN the regeneration has not fired yet — it is deferred to a later
221
+ // macrotask so the turn's assistant reply lands first, AND it fires once
222
+ // the macrotask queue is flushed.
223
+ expect(queueRegenerateConversationTitleMock).toHaveBeenCalledTimes(0);
224
+ await flushMacrotasks();
225
+ expect(queueRegenerateConversationTitleMock).toHaveBeenCalledTimes(1);
226
+ });
227
+
228
+ test("does not regenerate before the third user turn", async () => {
229
+ // GIVEN a turn ending with only two genuine user prompts
230
+ const ctx = makeStopCtx({ messages: historyWithUserTurns(2) });
231
+
232
+ // WHEN the stop hook runs and any deferred work flushes
233
+ await stop(ctx);
234
+ await flushMacrotasks();
235
+
236
+ // THEN no regeneration fires — the conversation lacks enough context yet
237
+ expect(queueRegenerateConversationTitleMock).toHaveBeenCalledTimes(0);
238
+ });
239
+
240
+ test("does not regenerate after the third user turn", async () => {
241
+ // GIVEN a turn ending with four genuine user prompts
242
+ const ctx = makeStopCtx({ messages: historyWithUserTurns(4) });
243
+
244
+ // WHEN the stop hook runs and any deferred work flushes
245
+ await stop(ctx);
246
+ await flushMacrotasks();
247
+
248
+ // THEN no regeneration fires — the single second pass already passed
249
+ expect(queueRegenerateConversationTitleMock).toHaveBeenCalledTimes(0);
250
+ });
251
+
252
+ test("ignores tool-result user messages when counting turns", async () => {
253
+ // GIVEN three genuine user prompts plus a tool-result user message
254
+ const messages: Message[] = [
255
+ userTurn("message 1"),
256
+ assistantTurn("reply 1"),
257
+ userTurn("message 2"),
258
+ assistantTurn("calling a tool"),
259
+ toolResultTurn(),
260
+ assistantTurn("reply 2"),
261
+ userTurn("message 3"),
262
+ ];
263
+ const ctx = makeStopCtx({ messages });
264
+
265
+ // WHEN the stop hook runs and its deferred work flushes
266
+ await stop(ctx);
267
+ await flushMacrotasks();
268
+
269
+ // THEN the tool-result message is not counted as a turn, so the third
270
+ // genuine prompt still triggers the regeneration
271
+ expect(queueRegenerateConversationTitleMock).toHaveBeenCalledTimes(1);
272
+ });
273
+
274
+ test("defers when an earlier hook chose to continue the run", async () => {
275
+ // GIVEN a third-user-turn stop that an earlier hook flipped to "continue"
276
+ const ctx = makeStopCtx({
277
+ messages: historyWithUserTurns(3),
278
+ decision: "continue",
279
+ });
280
+
281
+ // WHEN the stop hook runs and any deferred work flushes
282
+ await stop(ctx);
283
+ await flushMacrotasks();
284
+
285
+ // THEN it defers to the eventual terminal stop rather than re-titling now
286
+ expect(queueRegenerateConversationTitleMock).toHaveBeenCalledTimes(0);
287
+ });
288
+
289
+ test("respects the skipAutoRetitling opt-out", async () => {
290
+ // GIVEN the user opted out of second-pass retitling
291
+ skipAutoRetitling = true;
292
+ const ctx = makeStopCtx({ messages: historyWithUserTurns(3) });
293
+
294
+ // WHEN the stop hook runs on the third user turn and any work flushes
295
+ await stop(ctx);
296
+ await flushMacrotasks();
297
+
298
+ // THEN no regeneration fires
299
+ expect(queueRegenerateConversationTitleMock).toHaveBeenCalledTimes(0);
300
+ });
301
+
302
+ test("fires through runHook once the default plugin is registered", async () => {
303
+ // GIVEN the default title-generate plugin registered in the registry
304
+ registerPlugin(defaultTitleGeneratePlugin);
305
+
306
+ // WHEN a third-user-turn stop is dispatched through the hook chain
307
+ await runHook(
308
+ HOOKS.STOP,
309
+ makeStopCtx({ messages: historyWithUserTurns(3) }),
310
+ );
311
+ await flushMacrotasks();
312
+
313
+ // THEN the second-pass regeneration is triggered
314
+ expect(queueRegenerateConversationTitleMock).toHaveBeenCalledTimes(1);
315
+ expect(
316
+ queueRegenerateConversationTitleMock.mock.calls[0]?.[0]?.conversationId,
317
+ ).toBe("conv-1");
318
+ });
319
+ });