@vellumai/assistant 0.8.4 → 0.8.5

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 (438) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/docs/browser-use-architecture-phase2.md +1 -1
  3. package/knip.json +2 -1
  4. package/openapi.yaml +809 -11
  5. package/package.json +1 -1
  6. package/src/__tests__/anthropic-provider.test.ts +34 -37
  7. package/src/__tests__/assistant-event-hub-self-exclusion.test.ts +293 -0
  8. package/src/__tests__/assistant-feature-flags-integration.test.ts +3 -3
  9. package/src/__tests__/audit-log-rotation.test.ts +70 -16
  10. package/src/__tests__/background-workers-disk-pressure.test.ts +3 -3
  11. package/src/__tests__/btw-routes.test.ts +2 -3
  12. package/src/__tests__/call-controller.test.ts +0 -1
  13. package/src/__tests__/cancel-resolves-conversation-key.test.ts +1 -1
  14. package/src/__tests__/channel-guardian.test.ts +3 -3
  15. package/src/__tests__/checker.test.ts +6 -15
  16. package/src/__tests__/compaction-events.test.ts +1 -0
  17. package/src/__tests__/compactor-call-site-logging.test.ts +214 -0
  18. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +5 -11
  19. package/src/__tests__/computer-use-tools.test.ts +2 -4
  20. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
  21. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +1 -1
  22. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  23. package/src/__tests__/conversation-agent-loop-overflow.test.ts +197 -2
  24. package/src/__tests__/conversation-agent-loop.test.ts +163 -122
  25. package/src/__tests__/conversation-app-control-instantiation.test.ts +2 -5
  26. package/src/__tests__/conversation-clear-safety.test.ts +25 -25
  27. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +1 -1
  28. package/src/__tests__/conversation-disk-view-integration.test.ts +2 -2
  29. package/src/__tests__/conversation-error.test.ts +31 -0
  30. package/src/__tests__/conversation-fork-crud.test.ts +178 -15
  31. package/src/__tests__/conversation-lifecycle.test.ts +52 -11
  32. package/src/__tests__/{conversation-load-cleaned-at.test.ts → conversation-load-history-stripped.test.ts} +13 -13
  33. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -0
  34. package/src/__tests__/conversation-routes-disk-view.test.ts +109 -0
  35. package/src/__tests__/conversation-routes-slash-commands.test.ts +35 -0
  36. package/src/__tests__/conversation-skill-tools.test.ts +2 -5
  37. package/src/__tests__/conversation-store.test.ts +1 -1
  38. package/src/__tests__/conversation-sync-tags.test.ts +99 -32
  39. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
  40. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  41. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  42. package/src/__tests__/credential-execution-feature-gates.test.ts +9 -7
  43. package/src/__tests__/credential-execution-tools.test.ts +6 -6
  44. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  45. package/src/__tests__/credential-vault-unit.test.ts +2 -2
  46. package/src/__tests__/dynamic-page-surface.test.ts +2 -2
  47. package/src/__tests__/email-html-renderer.test.ts +12 -0
  48. package/src/__tests__/gateway-flag-listener.test.ts +237 -0
  49. package/src/__tests__/gemini-provider.test.ts +78 -0
  50. package/src/__tests__/guardian-dispatch.test.ts +0 -1
  51. package/src/__tests__/guardian-outbound-http.test.ts +7 -5
  52. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  53. package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -0
  54. package/src/__tests__/heartbeat-service.test.ts +4 -0
  55. package/src/__tests__/host-shell-tool.test.ts +1 -1
  56. package/src/__tests__/init-feature-flag-overrides.test.ts +5 -6
  57. package/src/__tests__/list-messages-tool-merge.test.ts +70 -11
  58. package/src/__tests__/llm-request-log-call-site.test.ts +136 -0
  59. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +26 -0
  60. package/src/__tests__/llm-resolver.test.ts +77 -9
  61. package/src/__tests__/llm-usage-store.test.ts +66 -0
  62. package/src/__tests__/logger.test.ts +89 -0
  63. package/src/__tests__/mcp-abort-signal.test.ts +2 -2
  64. package/src/__tests__/media-generate-image.test.ts +31 -0
  65. package/src/__tests__/memory-v2-static-injector.test.ts +7 -7
  66. package/src/__tests__/model-intents.test.ts +2 -4
  67. package/src/__tests__/notification-guardian-path.test.ts +0 -1
  68. package/src/__tests__/onboarding-template-contract.test.ts +1 -1
  69. package/src/__tests__/openai-provider.test.ts +46 -0
  70. package/src/__tests__/openai-responses-provider.test.ts +114 -12
  71. package/src/__tests__/pending-interactions-resolved-event.test.ts +0 -1
  72. package/src/__tests__/platform-bash-auto-approve.test.ts +2 -2
  73. package/src/__tests__/platform.test.ts +2 -2
  74. package/src/__tests__/plugin-api-tool-definition.test.ts +92 -0
  75. package/src/__tests__/plugin-bootstrap.test.ts +2 -2
  76. package/src/__tests__/plugin-tool-contribution.test.ts +13 -6
  77. package/src/__tests__/plugin-types.test.ts +3 -2
  78. package/src/__tests__/prechat-onboarding-contract.test.ts +131 -98
  79. package/src/__tests__/pricing.test.ts +12 -0
  80. package/src/__tests__/prune-jobs-changes-parser.test.ts +61 -0
  81. package/src/__tests__/registry.test.ts +2 -8
  82. package/src/__tests__/require-fresh-approval.test.ts +2 -2
  83. package/src/__tests__/runtime-events-sse-bilingual.test.ts +154 -0
  84. package/src/__tests__/shell-tool-proxy-mode.test.ts +1 -1
  85. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  86. package/src/__tests__/skill-projection-feature-flag.test.ts +4 -7
  87. package/src/__tests__/skill-projection.benchmark.test.ts +2 -6
  88. package/src/__tests__/skill-tool-factory.test.ts +1 -1
  89. package/src/__tests__/subagent-notify-parent.test.ts +1 -1
  90. package/src/__tests__/suggestion-routes.test.ts +1 -0
  91. package/src/__tests__/sync-message-contract.test.ts +59 -0
  92. package/src/__tests__/system-prompt.test.ts +145 -131
  93. package/src/__tests__/terminal-tools.test.ts +1 -1
  94. package/src/__tests__/tool-approval-handler.test.ts +1 -5
  95. package/src/__tests__/tool-execute-pipeline.test.ts +2 -2
  96. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -5
  97. package/src/__tests__/tool-executor-lifecycle-events.test.ts +15 -5
  98. package/src/__tests__/tool-executor.test.ts +9 -62
  99. package/src/__tests__/tool-grant-request-escalation.test.ts +1 -6
  100. package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
  101. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1 -6
  102. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
  103. package/src/__tests__/ui-file-upload-surface.test.ts +2 -2
  104. package/src/__tests__/usage-routes.test.ts +3 -0
  105. package/src/__tests__/verification-control-plane-policy.test.ts +2 -2
  106. package/src/__tests__/workspace-git-service.test.ts +6 -5
  107. package/src/__tests__/workspace-migration-089-move-memory-tree-out-of-v3.test.ts +86 -0
  108. package/src/acp/__tests__/prepare-agent-env.test.ts +146 -0
  109. package/src/acp/prepare-agent-env.ts +78 -0
  110. package/src/acp/session-manager.ts +1 -1
  111. package/src/agent/loop.ts +8 -0
  112. package/src/api/README.md +5 -0
  113. package/src/api/index.ts +4 -0
  114. package/src/api/package.json +10 -0
  115. package/src/background-wake/background-wake-routes.test.ts +233 -0
  116. package/src/background-wake/runtime-registry.ts +24 -0
  117. package/src/cli/commands/__tests__/browser.test.ts +23 -5
  118. package/src/cli/commands/__tests__/domain-register.test.ts +110 -0
  119. package/src/cli/commands/__tests__/domain-status.test.ts +33 -33
  120. package/src/cli/commands/__tests__/inference-send.test.ts +108 -5
  121. package/src/cli/commands/__tests__/memory-v2-compare-render.test.ts +98 -0
  122. package/src/cli/commands/__tests__/memory-v2.test.ts +1 -0
  123. package/src/cli/commands/__tests__/memory-v3-render.test.ts +340 -0
  124. package/src/cli/commands/browser.ts +247 -0
  125. package/src/cli/commands/domain.ts +91 -41
  126. package/src/cli/commands/inference.ts +93 -40
  127. package/src/cli/commands/memory-v2-compare-render.ts +115 -0
  128. package/src/cli/commands/memory-v2.ts +176 -1
  129. package/src/cli/commands/memory-v3-render.ts +344 -0
  130. package/src/cli/commands/memory-v3.ts +316 -0
  131. package/src/cli/program.ts +2 -0
  132. package/src/config/assistant-feature-flags.ts +21 -9
  133. package/src/config/bundled-skills/document-editor/SKILL.md +11 -2
  134. package/src/config/bundled-skills/document-editor/TOOLS.json +18 -0
  135. package/src/config/bundled-skills/document-editor/tools/document-open.ts +12 -0
  136. package/src/config/bundled-skills/image-studio/SKILL.md +4 -0
  137. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -2
  138. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +13 -8
  139. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +10 -3
  140. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +16 -14
  141. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +7 -2
  142. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +7 -2
  143. package/src/config/bundled-tool-registry.ts +2 -0
  144. package/src/config/call-site-defaults.ts +7 -6
  145. package/src/config/feature-flag-registry.json +16 -0
  146. package/src/config/schemas/__tests__/memory-v2.test.ts +213 -1
  147. package/src/config/schemas/call-site-catalog.ts +21 -7
  148. package/src/config/schemas/llm.ts +12 -1
  149. package/src/config/schemas/memory-v2.ts +246 -0
  150. package/src/config/schemas/memory.ts +2 -1
  151. package/src/context/compactor.ts +52 -0
  152. package/src/conversations/__tests__/message-consolidation.test.ts +350 -0
  153. package/src/conversations/message-consolidation.ts +404 -0
  154. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +1 -1
  155. package/src/daemon/__tests__/meet-manifest-loader.test.ts +1 -1
  156. package/src/daemon/conversation-agent-loop-handlers.ts +2 -13
  157. package/src/daemon/conversation-agent-loop.ts +126 -76
  158. package/src/daemon/conversation-error.ts +31 -1
  159. package/src/daemon/conversation-lifecycle.ts +27 -22
  160. package/src/daemon/conversation-runtime-assembly.ts +10 -9
  161. package/src/daemon/conversation-tool-setup.ts +63 -3
  162. package/src/daemon/conversation-usage.ts +2 -0
  163. package/src/daemon/conversation.ts +14 -29
  164. package/src/daemon/disk-pressure-guard.ts +14 -2
  165. package/src/daemon/handlers/config-model.test.ts +1 -0
  166. package/src/daemon/handlers/conversations.ts +11 -3
  167. package/src/daemon/host-browser-proxy.ts +5 -5
  168. package/src/daemon/host-cu-proxy.ts +4 -4
  169. package/src/daemon/host-file-proxy.ts +4 -4
  170. package/src/daemon/host-proxy-base.ts +4 -4
  171. package/src/daemon/host-transfer-proxy.ts +10 -10
  172. package/src/daemon/lifecycle.ts +23 -20
  173. package/src/daemon/meet-manifest-loader.ts +1 -7
  174. package/src/daemon/message-types/conversations.ts +6 -9
  175. package/src/daemon/message-types/home.ts +1 -13
  176. package/src/daemon/message-types/messages.ts +6 -14
  177. package/src/daemon/message-types/sync.ts +14 -0
  178. package/src/daemon/shutdown-handlers.ts +24 -5
  179. package/src/daemon/switch-inference-profile-tool.ts +52 -0
  180. package/src/daemon/tool-setup-types.ts +13 -0
  181. package/src/events/relationship-state-updated.ts +25 -0
  182. package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
  183. package/src/home/home-greeting.ts +0 -9
  184. package/src/home/suggested-prompts.ts +0 -9
  185. package/src/ipc/gateway-flag-listener.ts +123 -0
  186. package/src/ipc/skill-routes/registries.ts +8 -12
  187. package/src/memory/__tests__/db-async-query.test.ts +165 -0
  188. package/src/memory/__tests__/db-maintenance.test.ts +115 -0
  189. package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +241 -0
  190. package/src/memory/__tests__/jobs-store-job-classes.test.ts +28 -1
  191. package/src/memory/__tests__/memory-retrospective-job.test.ts +7 -0
  192. package/src/memory/auto-analysis-enqueue.ts +5 -1
  193. package/src/memory/conversation-crud.ts +71 -70
  194. package/src/memory/conversation-starters-cadence.ts +3 -1
  195. package/src/memory/conversation-title-service.ts +19 -3
  196. package/src/memory/db-async-query.ts +214 -0
  197. package/src/memory/db-init.ts +10 -0
  198. package/src/memory/db-maintenance.ts +30 -21
  199. package/src/memory/graph/bootstrap.ts +8 -1
  200. package/src/memory/graph/capability-seed.ts +7 -3
  201. package/src/memory/graph/conversation-graph-memory.ts +100 -17
  202. package/src/memory/graph/extraction.ts +1 -5
  203. package/src/memory/graph/graph-search.ts +7 -1
  204. package/src/memory/indexer.ts +28 -18
  205. package/src/memory/job-handlers/cleanup.ts +76 -18
  206. package/src/memory/job-handlers/conversation-starters.ts +1 -4
  207. package/src/memory/jobs/embed-pkb-file.ts +6 -1
  208. package/src/memory/jobs-store.ts +14 -0
  209. package/src/memory/jobs-worker.ts +55 -22
  210. package/src/memory/llm-request-log-source-clickhouse.ts +42 -2
  211. package/src/memory/llm-request-log-source-local.ts +7 -0
  212. package/src/memory/llm-request-log-source.ts +9 -2
  213. package/src/memory/llm-request-log-store.ts +43 -1
  214. package/src/memory/llm-usage-store.ts +24 -0
  215. package/src/memory/memory-retrospective-enqueue.ts +8 -1
  216. package/src/memory/memory-retrospective-job.ts +5 -0
  217. package/src/memory/memory-v2-activation-log-store.ts +15 -6
  218. package/src/memory/migrations/260-rename-cleaned-at.ts +44 -0
  219. package/src/memory/migrations/261-llm-usage-add-raw-usage.ts +36 -0
  220. package/src/memory/migrations/262-memory-v3-coactivation.ts +57 -0
  221. package/src/memory/migrations/263-memory-v3-auto-edges.ts +50 -0
  222. package/src/memory/migrations/264-llm-request-log-call-site.ts +29 -0
  223. package/src/memory/migrations/index.ts +17 -0
  224. package/src/memory/migrations/registry.ts +33 -0
  225. package/src/memory/schema/conversations.ts +1 -1
  226. package/src/memory/schema/infrastructure.ts +21 -0
  227. package/src/memory/tool-usage-store.ts +36 -8
  228. package/src/memory/v2/__tests__/consolidation-job.test.ts +1 -0
  229. package/src/memory/v2/__tests__/harness-compare.test.ts +186 -0
  230. package/src/memory/v2/__tests__/harness-metrics.test.ts +74 -0
  231. package/src/memory/v2/__tests__/harness-oracle.test.ts +257 -0
  232. package/src/memory/v2/__tests__/harness-replay-input.test.ts +225 -0
  233. package/src/memory/v2/__tests__/harness-runner.test.ts +109 -0
  234. package/src/memory/v2/__tests__/injection.test.ts +127 -98
  235. package/src/memory/v2/__tests__/qdrant.test.ts +36 -0
  236. package/src/memory/v2/__tests__/router.test.ts +171 -3
  237. package/src/memory/v2/harness/compare.ts +57 -0
  238. package/src/memory/v2/harness/metrics.ts +124 -0
  239. package/src/memory/v2/harness/oracle.ts +145 -0
  240. package/src/memory/v2/harness/replay-input.ts +224 -0
  241. package/src/memory/v2/harness/retriever.ts +74 -0
  242. package/src/memory/v2/harness/router-retriever.ts +43 -0
  243. package/src/memory/v2/harness/runner.ts +106 -0
  244. package/src/memory/v2/harness/trace.ts +58 -0
  245. package/src/memory/v2/injection.ts +21 -15
  246. package/src/memory/v2/prompts/router.ts +26 -1
  247. package/src/memory/v2/qdrant.ts +14 -2
  248. package/src/memory/v2/router.ts +171 -18
  249. package/src/memory/v3/__tests__/coactivation-store.test.ts +422 -0
  250. package/src/memory/v3/__tests__/consolidation-job.test.ts +468 -0
  251. package/src/memory/v3/__tests__/edge-learning-job.test.ts +324 -0
  252. package/src/memory/v3/__tests__/edges.test.ts +563 -0
  253. package/src/memory/v3/__tests__/filter.test.ts +512 -0
  254. package/src/memory/v3/__tests__/gate.test.ts +574 -0
  255. package/src/memory/v3/__tests__/index-composition.test.ts +233 -0
  256. package/src/memory/v3/__tests__/loop.test.ts +530 -0
  257. package/src/memory/v3/__tests__/retriever.test.ts +226 -0
  258. package/src/memory/v3/__tests__/scouts.test.ts +440 -0
  259. package/src/memory/v3/__tests__/shadow-middleware.test.ts +312 -0
  260. package/src/memory/v3/__tests__/system-prompts.test.ts +154 -0
  261. package/src/memory/v3/__tests__/traversal.test.ts +469 -0
  262. package/src/memory/v3/__tests__/tree-index.test.ts +280 -0
  263. package/src/memory/v3/__tests__/tree-store.test.ts +529 -0
  264. package/src/memory/v3/__tests__/tree-walk.test.ts +707 -0
  265. package/src/memory/v3/__tests__/validate.test.ts +245 -0
  266. package/src/memory/v3/auto-edges.ts +223 -0
  267. package/src/memory/v3/coactivation-store.ts +124 -0
  268. package/src/memory/v3/consolidation-job.ts +323 -0
  269. package/src/memory/v3/edge-learning-job.ts +160 -0
  270. package/src/memory/v3/edges.ts +249 -0
  271. package/src/memory/v3/filter.ts +281 -0
  272. package/src/memory/v3/gate.ts +334 -0
  273. package/src/memory/v3/index-composition.ts +113 -0
  274. package/src/memory/v3/llm-capture.ts +46 -0
  275. package/src/memory/v3/loop.ts +382 -0
  276. package/src/memory/v3/maintenance.ts +144 -0
  277. package/src/memory/v3/prompt-context.ts +33 -0
  278. package/src/memory/v3/prompts/consolidation.ts +458 -0
  279. package/src/memory/v3/prompts/system-prompts.ts +196 -0
  280. package/src/memory/v3/retriever.ts +33 -0
  281. package/src/memory/v3/scouts.ts +420 -0
  282. package/src/memory/v3/shadow-middleware.ts +305 -0
  283. package/src/memory/v3/traversal.ts +206 -0
  284. package/src/memory/v3/tree-index.ts +237 -0
  285. package/src/memory/v3/tree-store.ts +394 -0
  286. package/src/memory/v3/tree-walk.ts +351 -0
  287. package/src/memory/v3/types.ts +65 -0
  288. package/src/memory/v3/validate.ts +300 -0
  289. package/src/notifications/adapters/macos.ts +18 -1
  290. package/src/notifications/adapters/platform.ts +1 -1
  291. package/src/notifications/decision-engine.ts +1 -4
  292. package/src/notifications/emit-signal.ts +29 -49
  293. package/src/permissions/prompter.ts +3 -3
  294. package/src/permissions/question-prompter.ts +5 -2
  295. package/src/permissions/secret-prompter.ts +2 -2
  296. package/src/plugin-api/index.ts +4 -0
  297. package/src/plugin-api/types.ts +7 -33
  298. package/src/plugins/defaults/index.ts +6 -0
  299. package/src/plugins/defaults/injectors.ts +18 -11
  300. package/src/plugins/external-plugin-loader.ts +5 -68
  301. package/src/plugins/types.ts +11 -16
  302. package/src/proactive-artifact/aux-message-injector.ts +17 -4
  303. package/src/prompts/__tests__/task-progress-hint-section.test.ts +3 -9
  304. package/src/prompts/persona-resolver.ts +36 -21
  305. package/src/prompts/sections.ts +39 -7
  306. package/src/prompts/system-prompt.ts +50 -185
  307. package/src/prompts/templates/BOOTSTRAP.md +2 -2
  308. package/src/prompts/templates/system-sections.ts +230 -8
  309. package/src/providers/__tests__/connection-model-compat.test.ts +234 -0
  310. package/src/providers/__tests__/retry-callsite.test.ts +85 -5
  311. package/src/providers/anthropic/client.ts +32 -66
  312. package/src/providers/call-site-routing.ts +14 -2
  313. package/src/providers/connection-model-compat.ts +38 -0
  314. package/src/providers/connection-resolution.ts +16 -2
  315. package/src/providers/gemini/client.ts +49 -6
  316. package/src/providers/inference/adapter-factory.ts +3 -0
  317. package/src/providers/minimax/client.ts +106 -0
  318. package/src/providers/model-catalog.ts +43 -0
  319. package/src/providers/model-intents.ts +1 -1
  320. package/src/providers/openai/chat-completions-provider.ts +6 -3
  321. package/src/providers/openai/codex-models.ts +18 -0
  322. package/src/providers/openai/responses-provider.ts +78 -21
  323. package/src/providers/provider-send-message.ts +7 -1
  324. package/src/providers/retry.ts +34 -3
  325. package/src/providers/thinking-config.ts +26 -1
  326. package/src/providers/usage-tracking.ts +2 -0
  327. package/src/runtime/AGENTS.md +2 -2
  328. package/src/runtime/agent-wake.ts +1 -0
  329. package/src/runtime/assistant-event-hub.ts +76 -6
  330. package/src/runtime/auth/route-policy.ts +36 -0
  331. package/src/runtime/btw-sidechain.ts +0 -6
  332. package/src/runtime/http-types.ts +0 -2
  333. package/src/runtime/migrations/vbundle-builder.ts +10 -3
  334. package/src/runtime/pending-interactions.ts +0 -1
  335. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +106 -0
  336. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +25 -6
  337. package/src/runtime/routes/__tests__/plugins-routes.test.ts +512 -0
  338. package/src/runtime/routes/acp-routes.test.ts +255 -6
  339. package/src/runtime/routes/acp-routes.ts +8 -1
  340. package/src/runtime/routes/avatar-routes.ts +10 -10
  341. package/src/runtime/routes/background-wake-routes.ts +188 -0
  342. package/src/runtime/routes/browser-tabs-routes.ts +200 -0
  343. package/src/runtime/routes/btw-routes.ts +0 -6
  344. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  345. package/src/runtime/routes/conversation-list-routes.ts +12 -4
  346. package/src/runtime/routes/conversation-management-routes.ts +77 -20
  347. package/src/runtime/routes/conversation-query-routes.ts +142 -36
  348. package/src/runtime/routes/conversation-routes.ts +252 -410
  349. package/src/runtime/routes/conversation-starter-routes.ts +6 -3
  350. package/src/runtime/routes/disk-pressure-routes.ts +1 -1
  351. package/src/runtime/routes/domain-routes.ts +60 -10
  352. package/src/runtime/routes/email-routes.ts +5 -2
  353. package/src/runtime/routes/events-routes.ts +54 -10
  354. package/src/runtime/routes/group-routes.ts +24 -8
  355. package/src/runtime/routes/host-browser-routes.ts +10 -2
  356. package/src/runtime/routes/host-cu-routes.ts +2 -2
  357. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +96 -3
  358. package/src/runtime/routes/index.ts +8 -0
  359. package/src/runtime/routes/inference-profile-session-handler.ts +22 -12
  360. package/src/runtime/routes/inference-profile-session-routes.ts +7 -1
  361. package/src/runtime/routes/llm-call-sites-routes.ts +32 -5
  362. package/src/runtime/routes/memory-item-routes.ts +8 -3
  363. package/src/runtime/routes/memory-v2-routes.ts +215 -5
  364. package/src/runtime/routes/memory-v3-routes.ts +316 -0
  365. package/src/runtime/routes/migration-routes.ts +21 -24
  366. package/src/runtime/routes/plugins-routes.ts +337 -0
  367. package/src/runtime/routes/rename-conversation-routes.ts +6 -2
  368. package/src/runtime/routes/secret-routes.ts +25 -5
  369. package/src/runtime/routes/settings-routes.ts +12 -11
  370. package/src/runtime/routes/slack-channel-routes.ts +5 -4
  371. package/src/runtime/routes/workspace-routes.ts +25 -10
  372. package/src/runtime/sync/resource-sync-events.ts +106 -38
  373. package/src/runtime/sync/sync-publisher.test.ts +49 -0
  374. package/src/runtime/sync/sync-publisher.ts +2 -1
  375. package/src/runtime/verification-outbound-actions.ts +73 -1
  376. package/src/telemetry/types.ts +12 -0
  377. package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
  378. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  379. package/src/tools/acp/spawn.test.ts +119 -0
  380. package/src/tools/acp/spawn.ts +15 -2
  381. package/src/tools/apps/definitions.ts +2 -8
  382. package/src/tools/ask-question/ask-question-tool.test.ts +3 -3
  383. package/src/tools/ask-question/ask-question-tool.ts +38 -45
  384. package/src/tools/browser/__tests__/pinned-tabs.test.ts +70 -0
  385. package/src/tools/browser/browser-execution.ts +16 -3
  386. package/src/tools/browser/cdp-client/__tests__/browser-tabs-factory.test.ts +402 -0
  387. package/src/tools/browser/cdp-client/__tests__/types.test.ts +3 -0
  388. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +12 -0
  389. package/src/tools/browser/cdp-client/extension-cdp-client.ts +27 -1
  390. package/src/tools/browser/cdp-client/factory.ts +100 -17
  391. package/src/tools/browser/cdp-client/local-cdp-client.ts +12 -0
  392. package/src/tools/browser/cdp-client/types.ts +65 -0
  393. package/src/tools/browser/pinned-tabs.ts +96 -40
  394. package/src/tools/computer-use/definitions.ts +22 -78
  395. package/src/tools/credential-execution/make-authenticated-request.ts +3 -9
  396. package/src/tools/credential-execution/manage-secure-command-tool.ts +3 -9
  397. package/src/tools/credential-execution/run-authenticated-command.ts +3 -9
  398. package/src/tools/credentials/vault.ts +3 -9
  399. package/src/tools/document/document-tool.ts +59 -0
  400. package/src/tools/execution-target.ts +21 -23
  401. package/src/tools/executor.ts +6 -1
  402. package/src/tools/filesystem/edit.ts +3 -9
  403. package/src/tools/filesystem/list.ts +3 -9
  404. package/src/tools/filesystem/read.ts +3 -9
  405. package/src/tools/filesystem/write.ts +3 -9
  406. package/src/tools/host-filesystem/edit.ts +3 -9
  407. package/src/tools/host-filesystem/read.ts +3 -9
  408. package/src/tools/host-filesystem/transfer.ts +3 -9
  409. package/src/tools/host-filesystem/write.ts +3 -9
  410. package/src/tools/host-terminal/host-shell.ts +3 -9
  411. package/src/tools/mcp/mcp-tool-factory.ts +1 -8
  412. package/src/tools/memory/register.test.ts +1 -1
  413. package/src/tools/memory/register.ts +4 -9
  414. package/src/tools/network/web-fetch.ts +3 -9
  415. package/src/tools/network/web-search.ts +25 -32
  416. package/src/tools/registry.ts +7 -23
  417. package/src/tools/schema-transforms.ts +1 -1
  418. package/src/tools/skills/execute.ts +3 -9
  419. package/src/tools/skills/load.ts +3 -9
  420. package/src/tools/skills/skill-tool-factory.ts +1 -8
  421. package/src/tools/subagent/notify-parent.ts +3 -9
  422. package/src/tools/system/request-permission.ts +3 -9
  423. package/src/tools/terminal/shell.ts +3 -9
  424. package/src/tools/tool-defaults.ts +94 -0
  425. package/src/tools/types.ts +27 -98
  426. package/src/tools/ui-surface/definitions.ts +6 -22
  427. package/src/usage/pricing.ts +23 -0
  428. package/src/usage/types.ts +12 -0
  429. package/src/util/logger.ts +16 -7
  430. package/src/util/platform.ts +7 -2
  431. package/src/util/sqlite3-runtime.ts +65 -0
  432. package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +1 -0
  433. package/src/workspace/migrations/089-move-memory-tree-out-of-v3.ts +86 -0
  434. package/src/workspace/migrations/registry.ts +2 -0
  435. package/src/__tests__/compaction-strip-metadata-clear.test.ts +0 -206
  436. package/src/__tests__/message-complete-display-id.test.ts +0 -175
  437. package/src/daemon/query-complexity-router.ts +0 -75
  438. package/src/prompts/cache-boundary.ts +0 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.8.4",
3
+ "version": "0.8.5",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -102,7 +102,6 @@ mock.module("@anthropic-ai/sdk", () => ({
102
102
  }));
103
103
 
104
104
  // Import after mocking
105
- import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../prompts/system-prompt.js";
106
105
  import {
107
106
  AnthropicProvider,
108
107
  isPlaceholderSentinelText,
@@ -228,28 +227,8 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
228
227
  expect(assistant?.content).toEqual([{ type: "text", text: "I can help." }]);
229
228
  });
230
229
 
231
- test("splits system prompt into two cache blocks on boundary marker", async () => {
232
- const staticBlock = "You are a helpful assistant.";
233
- const dynamicBlock = "User workspace files here.";
234
- const prompt = staticBlock + SYSTEM_PROMPT_CACHE_BOUNDARY + dynamicBlock;
235
-
236
- await provider.sendMessage([userMsg("Hi")], undefined, prompt);
237
-
238
- const system = lastStreamParams!.system as Array<{
239
- type: string;
240
- text: string;
241
- cache_control?: { type: string; ttl?: string };
242
- }>;
243
- expect(system).toHaveLength(2);
244
- expect(system[0].text).toBe(staticBlock);
245
- expect(system[0].cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
246
- expect(system[1].text).toBe(dynamicBlock);
247
- expect(system[1].cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
248
- });
249
-
250
- test("omits empty dynamic system block after cache boundary", async () => {
251
- const staticBlock = "You are a helpful assistant.";
252
- const prompt = staticBlock + SYSTEM_PROMPT_CACHE_BOUNDARY;
230
+ test("renders the system prompt as a single 1h-cached block", async () => {
231
+ const prompt = "You are a helpful assistant.";
253
232
 
254
233
  await provider.sendMessage([userMsg("Hi")], undefined, prompt);
255
234
 
@@ -259,16 +238,14 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
259
238
  cache_control?: { type: string; ttl?: string };
260
239
  }>;
261
240
  expect(system).toHaveLength(1);
262
- expect(system[0].text).toBe(staticBlock);
241
+ expect(system[0].text).toBe(prompt);
263
242
  expect(system[0].cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
264
243
  });
265
244
 
266
- test("drops static system block cache_control when total would exceed 4", async () => {
267
- const staticBlock = "You are a helpful assistant.";
268
- const dynamicBlock = "User workspace files here.";
269
- const prompt = staticBlock + SYSTEM_PROMPT_CACHE_BOUNDARY + dynamicBlock;
270
-
271
- // Boundary (2 system) + tools (1) + turn-start (1) + tail (1) = 5 → must cap at 4
245
+ test("applies the standard 4-breakpoint cache layout in a tool-use loop", async () => {
246
+ // system(1) + tools(1) + turn-start(1) + tail(1) = 4, the Anthropic
247
+ // per-request cap. All four breakpoints should be present.
248
+ const prompt = "You are a helpful assistant.";
272
249
  const messages: Message[] = [
273
250
  userMsg("Do something"),
274
251
  toolUseMsg("tu_1", "bash"),
@@ -281,13 +258,9 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
281
258
  text: string;
282
259
  cache_control?: { type: string; ttl?: string };
283
260
  }>;
284
- expect(system).toHaveLength(2);
285
- // Static block's cache_control dropped (small, cheap to re-read)
286
- expect(system[0].cache_control).toBeUndefined();
287
- // Dynamic block keeps its cache_control
288
- expect(system[1].cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
261
+ expect(system).toHaveLength(1);
262
+ expect(system[0].cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
289
263
 
290
- // Tools breakpoint still present
291
264
  const tools = lastStreamParams!.tools as Array<{
292
265
  cache_control?: { type: string; ttl?: string };
293
266
  }>;
@@ -296,7 +269,6 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
296
269
  ttl: "1h",
297
270
  });
298
271
 
299
- // Turn-start + tail breakpoints still present
300
272
  const sent = lastStreamParams!.messages as Array<{
301
273
  role: string;
302
274
  content: Array<{
@@ -432,6 +404,31 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
432
404
  expect(lastBlock.cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
433
405
  });
434
406
 
407
+ test("disableTurnStartCache suppresses the 1h breakpoint on the turn-starting user message", async () => {
408
+ // One-shot callers (e.g. the memory router) send a single user message
409
+ // per call with content that changes every time. Caching the turn-start
410
+ // block would create unused entries — `disableTurnStartCache: true`
411
+ // opts out.
412
+ await provider.sendMessage(
413
+ [userMsg("Pick relevant pages")],
414
+ undefined,
415
+ undefined,
416
+ {
417
+ config: { disableTurnStartCache: true },
418
+ },
419
+ );
420
+
421
+ const sent = lastStreamParams!.messages as Array<{
422
+ role: string;
423
+ content: Array<{
424
+ type: string;
425
+ cache_control?: { type: string; ttl?: string };
426
+ }>;
427
+ }>;
428
+ const lastBlock = sent[0].content[sent[0].content.length - 1];
429
+ expect(lastBlock.cache_control).toBeUndefined();
430
+ });
431
+
435
432
  test("previous-turn anchor is NOT applied during a tool-use loop", async () => {
436
433
  // When the request is mid tool-use (last msg is a tool_result), the
437
434
  // turn-start anchor already covers the long prefix, so we must not
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Tests for self-echo suppression in `AssistantEventHub`.
3
+ *
4
+ * Validates:
5
+ * - hub.publish(event, { excludeClientId }) skips the matching client
6
+ * subscriber and delivers to every other matching subscriber.
7
+ * - Suppression is unconditional: it applies whether the broadcast is
8
+ * untargeted, conversation-scoped, or capability-targeted.
9
+ * - `broadcastMessage(sync_changed { originClientId })` derives the
10
+ * exclusion from the message itself — no caller wiring needed.
11
+ * - `broadcastMessage(sync_changed)` without an originClientId fans out
12
+ * to every subscriber (the daemon-internal emit path).
13
+ * - Process-type subscribers are never excluded by a clientId match.
14
+ */
15
+ import { describe, expect, test } from "bun:test";
16
+
17
+ import type { AssistantEvent } from "../runtime/assistant-event.js";
18
+ import {
19
+ AssistantEventHub,
20
+ assistantEventHub,
21
+ broadcastMessage,
22
+ } from "../runtime/assistant-event-hub.js";
23
+
24
+ function makeSyncChangedEvent(originClientId?: string): AssistantEvent {
25
+ return {
26
+ id: "evt_test_sync",
27
+ conversationId: undefined,
28
+ emittedAt: "2026-05-03T00:00:00.000Z",
29
+ message: {
30
+ type: "sync_changed",
31
+ tags: ["conversation:abc:messages"],
32
+ ...(originClientId ? { originClientId } : {}),
33
+ },
34
+ };
35
+ }
36
+
37
+ describe("AssistantEventHub — self-echo suppression (excludeClientId)", () => {
38
+ test("skips the named client and delivers to every other client", async () => {
39
+ const hub = new AssistantEventHub();
40
+ const receivedA: AssistantEvent[] = [];
41
+ const receivedB: AssistantEvent[] = [];
42
+ const receivedC: AssistantEvent[] = [];
43
+
44
+ hub.subscribe({
45
+ type: "client",
46
+ clientId: "client-a",
47
+ interfaceId: "web",
48
+ capabilities: [],
49
+ callback: (e) => {
50
+ receivedA.push(e);
51
+ },
52
+ });
53
+
54
+ hub.subscribe({
55
+ type: "client",
56
+ clientId: "client-b",
57
+ interfaceId: "web",
58
+ capabilities: [],
59
+ callback: (e) => {
60
+ receivedB.push(e);
61
+ },
62
+ });
63
+
64
+ hub.subscribe({
65
+ type: "client",
66
+ clientId: "client-c",
67
+ interfaceId: "macos",
68
+ capabilities: [],
69
+ callback: (e) => {
70
+ receivedC.push(e);
71
+ },
72
+ });
73
+
74
+ await hub.publish(makeSyncChangedEvent("client-a"), {
75
+ excludeClientId: "client-a",
76
+ });
77
+
78
+ expect(receivedA).toHaveLength(0);
79
+ expect(receivedB).toHaveLength(1);
80
+ expect(receivedC).toHaveLength(1);
81
+ });
82
+
83
+ test("delivers to every subscriber when excludeClientId is omitted", async () => {
84
+ const hub = new AssistantEventHub();
85
+ const receivedA: AssistantEvent[] = [];
86
+ const receivedB: AssistantEvent[] = [];
87
+
88
+ hub.subscribe({
89
+ type: "client",
90
+ clientId: "client-a",
91
+ interfaceId: "web",
92
+ capabilities: [],
93
+ callback: (e) => {
94
+ receivedA.push(e);
95
+ },
96
+ });
97
+ hub.subscribe({
98
+ type: "client",
99
+ clientId: "client-b",
100
+ interfaceId: "web",
101
+ capabilities: [],
102
+ callback: (e) => {
103
+ receivedB.push(e);
104
+ },
105
+ });
106
+
107
+ await hub.publish(makeSyncChangedEvent());
108
+
109
+ expect(receivedA).toHaveLength(1);
110
+ expect(receivedB).toHaveLength(1);
111
+ });
112
+
113
+ test("excludeClientId does not match process-type subscribers", async () => {
114
+ const hub = new AssistantEventHub();
115
+ const receivedProcess: AssistantEvent[] = [];
116
+
117
+ // A process subscriber sits in the same hub as a client whose id
118
+ // matches `excludeClientId`. It must never be suppressed because
119
+ // `excludeClientId` only matches `type: "client"` entries — process
120
+ // entries have no `clientId` field at all.
121
+ hub.subscribe({
122
+ type: "process",
123
+ callback: (e) => {
124
+ receivedProcess.push(e);
125
+ },
126
+ });
127
+
128
+ await hub.publish(makeSyncChangedEvent("client-a"), {
129
+ excludeClientId: "client-a",
130
+ });
131
+
132
+ expect(receivedProcess).toHaveLength(1);
133
+ });
134
+
135
+ test("excludeClientId composes with conversation-scoped subscribers", async () => {
136
+ const hub = new AssistantEventHub();
137
+ const receivedA: AssistantEvent[] = [];
138
+ const receivedB: AssistantEvent[] = [];
139
+
140
+ hub.subscribe({
141
+ type: "client",
142
+ clientId: "client-a",
143
+ interfaceId: "web",
144
+ capabilities: [],
145
+ filter: { conversationId: "sess_x" },
146
+ callback: (e) => {
147
+ receivedA.push(e);
148
+ },
149
+ });
150
+ hub.subscribe({
151
+ type: "client",
152
+ clientId: "client-b",
153
+ interfaceId: "web",
154
+ capabilities: [],
155
+ filter: { conversationId: "sess_x" },
156
+ callback: (e) => {
157
+ receivedB.push(e);
158
+ },
159
+ });
160
+
161
+ // Scoped event for sess_x; both clients filter to sess_x; exclude A.
162
+ const event: AssistantEvent = {
163
+ id: "evt_scoped",
164
+ conversationId: "sess_x",
165
+ emittedAt: "2026-05-03T00:00:00.000Z",
166
+ message: {
167
+ type: "sync_changed",
168
+ tags: ["conversation:sess_x:messages"],
169
+ originClientId: "client-a",
170
+ },
171
+ };
172
+
173
+ await hub.publish(event, { excludeClientId: "client-a" });
174
+
175
+ expect(receivedA).toHaveLength(0);
176
+ expect(receivedB).toHaveLength(1);
177
+ });
178
+ });
179
+
180
+ describe("broadcastMessage — derives excludeClientId from sync_changed.originClientId", () => {
181
+ test("skips the originating client when originClientId is present", async () => {
182
+ const receivedA: AssistantEvent[] = [];
183
+ const receivedB: AssistantEvent[] = [];
184
+
185
+ const subA = assistantEventHub.subscribe({
186
+ type: "client",
187
+ clientId: "broadcast-a",
188
+ interfaceId: "web",
189
+ capabilities: [],
190
+ callback: (e) => {
191
+ receivedA.push(e);
192
+ },
193
+ });
194
+ const subB = assistantEventHub.subscribe({
195
+ type: "client",
196
+ clientId: "broadcast-b",
197
+ interfaceId: "web",
198
+ capabilities: [],
199
+ callback: (e) => {
200
+ receivedB.push(e);
201
+ },
202
+ });
203
+
204
+ try {
205
+ broadcastMessage({
206
+ type: "sync_changed",
207
+ tags: ["resource:test"],
208
+ originClientId: "broadcast-a",
209
+ });
210
+ // broadcastMessage queues onto a microtask chain — yield until it
211
+ // drains. Two awaits cover the publish promise + its `.then` chain.
212
+ await new Promise((r) => setTimeout(r, 0));
213
+ await new Promise((r) => setTimeout(r, 0));
214
+
215
+ expect(receivedA).toHaveLength(0);
216
+ expect(receivedB).toHaveLength(1);
217
+ } finally {
218
+ subA.dispose();
219
+ subB.dispose();
220
+ }
221
+ });
222
+
223
+ test("fans out to every client when originClientId is absent", async () => {
224
+ const receivedA: AssistantEvent[] = [];
225
+ const receivedB: AssistantEvent[] = [];
226
+
227
+ const subA = assistantEventHub.subscribe({
228
+ type: "client",
229
+ clientId: "broadcast-c",
230
+ interfaceId: "web",
231
+ capabilities: [],
232
+ callback: (e) => {
233
+ receivedA.push(e);
234
+ },
235
+ });
236
+ const subB = assistantEventHub.subscribe({
237
+ type: "client",
238
+ clientId: "broadcast-d",
239
+ interfaceId: "web",
240
+ capabilities: [],
241
+ callback: (e) => {
242
+ receivedB.push(e);
243
+ },
244
+ });
245
+
246
+ try {
247
+ broadcastMessage({
248
+ type: "sync_changed",
249
+ tags: ["resource:test"],
250
+ });
251
+ await new Promise((r) => setTimeout(r, 0));
252
+ await new Promise((r) => setTimeout(r, 0));
253
+
254
+ expect(receivedA).toHaveLength(1);
255
+ expect(receivedB).toHaveLength(1);
256
+ } finally {
257
+ subA.dispose();
258
+ subB.dispose();
259
+ }
260
+ });
261
+
262
+ test("ignores an empty-string originClientId (treats as absent)", async () => {
263
+ const receivedA: AssistantEvent[] = [];
264
+
265
+ const subA = assistantEventHub.subscribe({
266
+ type: "client",
267
+ clientId: "broadcast-e",
268
+ interfaceId: "web",
269
+ capabilities: [],
270
+ callback: (e) => {
271
+ receivedA.push(e);
272
+ },
273
+ });
274
+
275
+ try {
276
+ broadcastMessage({
277
+ type: "sync_changed",
278
+ tags: ["resource:test"],
279
+ // Exercise the length-zero guard. `originClientId?: string` accepts
280
+ // empty strings at the type level, but production code path can't
281
+ // produce one — `buildSyncChangedMessage` trims and drops empty
282
+ // values before calling `broadcastMessage`.
283
+ originClientId: "",
284
+ });
285
+ await new Promise((r) => setTimeout(r, 0));
286
+ await new Promise((r) => setTimeout(r, 0));
287
+
288
+ expect(receivedA).toHaveLength(1);
289
+ } finally {
290
+ subA.dispose();
291
+ }
292
+ });
293
+ });
@@ -4,7 +4,7 @@
4
4
  * Covers:
5
5
  * - Missing persisted value falls back to code default
6
6
  * - Protected feature-flags.json is the sole override mechanism
7
- * - Undeclared keys default to enabled
7
+ * - Undeclared keys default to disabled
8
8
  */
9
9
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
10
10
 
@@ -80,10 +80,10 @@ describe("isAssistantFeatureFlagEnabled", () => {
80
80
  ).toBe(true);
81
81
  });
82
82
 
83
- test("unknown flag defaults to true when no persisted override", () => {
83
+ test("unknown flag defaults to false when no persisted override", () => {
84
84
  const config = {} as any;
85
85
 
86
- expect(isAssistantFeatureFlagEnabled("unknown-skill", config)).toBe(true);
86
+ expect(isAssistantFeatureFlagEnabled("unknown-skill", config)).toBe(false);
87
87
  });
88
88
 
89
89
  test("undeclared flag respects persisted override", () => {
@@ -69,72 +69,126 @@ describe("audit log rotation", () => {
69
69
  clearTable();
70
70
  });
71
71
 
72
- test("returns 0 when retentionDays is 0 (retain forever)", () => {
72
+ test("returns 0 when retentionDays is 0 (retain forever)", async () => {
73
73
  addInvocation(100 * ONE_DAY_MS); // 100 days old
74
- const deleted = rotateToolInvocations(0);
74
+ const deleted = await rotateToolInvocations(0);
75
75
  expect(deleted).toBe(0);
76
76
  expect(getRecentInvocations(100).length).toBe(1);
77
77
  });
78
78
 
79
- test("returns 0 when retentionDays is negative", () => {
79
+ test("returns 0 when retentionDays is negative", async () => {
80
80
  addInvocation(100 * ONE_DAY_MS);
81
- const deleted = rotateToolInvocations(-5);
81
+ const deleted = await rotateToolInvocations(-5);
82
82
  expect(deleted).toBe(0);
83
83
  expect(getRecentInvocations(100).length).toBe(1);
84
84
  });
85
85
 
86
- test("deletes records older than retentionDays", () => {
86
+ test("deletes records older than retentionDays", async () => {
87
87
  addInvocation(10 * ONE_DAY_MS); // 10 days old — should be deleted with 7-day retention
88
88
  addInvocation(3 * ONE_DAY_MS); // 3 days old — should be kept
89
89
  addInvocation(1 * ONE_DAY_MS); // 1 day old — should be kept
90
90
 
91
- const deleted = rotateToolInvocations(7);
91
+ const deleted = await rotateToolInvocations(7);
92
92
  expect(deleted).toBe(1);
93
93
  expect(getRecentInvocations(100).length).toBe(2);
94
94
  });
95
95
 
96
- test("keeps all records when none exceed retention", () => {
96
+ test("keeps all records when none exceed retention", async () => {
97
97
  addInvocation(1 * ONE_DAY_MS);
98
98
  addInvocation(2 * ONE_DAY_MS);
99
99
  addInvocation(3 * ONE_DAY_MS);
100
100
 
101
- const deleted = rotateToolInvocations(30);
101
+ const deleted = await rotateToolInvocations(30);
102
102
  expect(deleted).toBe(0);
103
103
  expect(getRecentInvocations(100).length).toBe(3);
104
104
  });
105
105
 
106
- test("deletes all records when all exceed retention", () => {
106
+ test("deletes all records when all exceed retention", async () => {
107
107
  addInvocation(60 * ONE_DAY_MS);
108
108
  addInvocation(90 * ONE_DAY_MS);
109
109
  addInvocation(120 * ONE_DAY_MS);
110
110
 
111
- const deleted = rotateToolInvocations(30);
111
+ const deleted = await rotateToolInvocations(30);
112
112
  expect(deleted).toBe(3);
113
113
  expect(getRecentInvocations(100).length).toBe(0);
114
114
  });
115
115
 
116
- test("returns 0 when table is empty", () => {
117
- const deleted = rotateToolInvocations(7);
116
+ test("returns 0 when table is empty", async () => {
117
+ const deleted = await rotateToolInvocations(7);
118
118
  expect(deleted).toBe(0);
119
119
  });
120
120
 
121
- test("handles 1-day retention (deletes everything older than 24h)", () => {
121
+ test("handles 1-day retention (deletes everything older than 24h)", async () => {
122
122
  addInvocation(2 * ONE_DAY_MS); // 2 days old — delete
123
123
  addInvocation(12 * 60 * 60 * 1000); // 12 hours old — keep
124
124
 
125
- const deleted = rotateToolInvocations(1);
125
+ const deleted = await rotateToolInvocations(1);
126
126
  expect(deleted).toBe(1);
127
127
  expect(getRecentInvocations(100).length).toBe(1);
128
128
  });
129
129
 
130
- test("works with recordToolInvocation (via ORM)", () => {
130
+ test("works with recordToolInvocation (via ORM)", async () => {
131
131
  // Use raw SQL to insert (avoids db singleton issues in parallel test runs)
132
132
  // and verify the rotation/query functions work correctly with it
133
133
  addInvocation(0); // just-created record
134
134
 
135
135
  // This record was just created, so it should not be rotated
136
- const deleted = rotateToolInvocations(1);
136
+ const deleted = await rotateToolInvocations(1);
137
137
  expect(deleted).toBe(0);
138
138
  expect(getRecentInvocations(100).length).toBe(1);
139
139
  });
140
+
141
+ test(
142
+ "yields to the event loop while the purge is in flight (anti-block)",
143
+ async () => {
144
+ // Seed a few thousand rows so the DELETE has measurable
145
+ // subprocess work behind the scenes.
146
+ const ROW_COUNT = 2000;
147
+ const past = 100 * ONE_DAY_MS;
148
+ const sqlite = getSqlite();
149
+ sqlite.exec("BEGIN");
150
+ try {
151
+ for (let i = 0; i < ROW_COUNT; i++) {
152
+ addInvocation(past);
153
+ }
154
+ sqlite.exec("COMMIT");
155
+ } catch (err) {
156
+ sqlite.exec("ROLLBACK");
157
+ throw err;
158
+ }
159
+
160
+ // Race the purge against a `setImmediate` ping. The ping
161
+ // resolves on the very next event-loop iteration after it is
162
+ // scheduled.
163
+ //
164
+ // If the purge is async (subprocess), `rotateToolInvocations`
165
+ // returns a pending Promise immediately; the ping wins the race
166
+ // while the subprocess is still running.
167
+ //
168
+ // If the purge is ever regressed back to a synchronous DELETE,
169
+ // `rotateToolInvocations` blocks the main thread for the full
170
+ // DELETE duration. `setImmediate` cannot fire until the main
171
+ // thread releases, so the purge's already-resolved Promise
172
+ // beats the ping through the microtask queue and "purge" wins.
173
+ //
174
+ // The signal is deterministic regardless of how fast the
175
+ // subprocess is or how busy the event loop is.
176
+ const ping = new Promise<"ping">((resolve) =>
177
+ setImmediate(() => resolve("ping")),
178
+ );
179
+ const purgePromise = rotateToolInvocations(7);
180
+ const winner = await Promise.race([
181
+ ping,
182
+ purgePromise.then(() => "purge" as const),
183
+ ]);
184
+
185
+ expect(winner).toBe("ping");
186
+
187
+ // Drain the purge so the log + subprocess cleanup complete
188
+ // before the test exits.
189
+ const deleted = await purgePromise;
190
+ expect(deleted).toBe(ROW_COUNT);
191
+ },
192
+ 60_000,
193
+ );
140
194
  });
@@ -108,7 +108,6 @@ mock.module("../memory/conversation-crud.js", () => ({
108
108
  addMessage: mock(() => ({ id: "msg-1" })),
109
109
  archiveConversation: mock(() => true),
110
110
  batchSetDisplayOrders: mock(() => {}),
111
- clearStrippedInjectionMetadataForConversation: mock(() => {}),
112
111
  createConversation: (opts: { conversationType: string }) => {
113
112
  createdConversations.push(opts);
114
113
  return { id: "conv-1", ...opts };
@@ -116,7 +115,7 @@ mock.module("../memory/conversation-crud.js", () => ({
116
115
  countConversationsByScheduleJobId: mock(() => 0),
117
116
  countMessagesAfter: mock(() => 0),
118
117
  deleteMessageById: mock(() => {}),
119
- clearAll: mock(() => ({ conversations: 0, messages: 0 })),
118
+ clearAll: mock(async () => ({ conversations: 0, messages: 0 })),
120
119
  deleteConversation: mock(() => ({ memoryIds: [] })),
121
120
  deleteLastExchange: mock(() => 0),
122
121
  findAnalysisConversationFor: mock(() => null),
@@ -156,7 +155,7 @@ mock.module("../memory/conversation-crud.js", () => ({
156
155
  updateConversationTitle: mock(() => {}),
157
156
  updateConversationUsage: mock(() => {}),
158
157
  setLastNotifiedInferenceProfile: mock(() => {}),
159
- setConversationCleanedAt: mock(() => {}),
158
+ setConversationHistoryStrippedAt: mock(() => {}),
160
159
  wipeConversation: mock(() => ({ memoryIds: [] })),
161
160
  }));
162
161
 
@@ -182,6 +181,7 @@ mock.module("../memory/jobs-store.js", () => ({
182
181
  failStalledJobs: mockFailStalledJobs,
183
182
  getMemoryJobCounts: mock(() => ({})),
184
183
  hasActiveJobOfType: mock(() => false),
184
+ isMemoryEnabled: () => true,
185
185
  resetRunningJobsToPending: mock(() => 0),
186
186
  SLOW_LLM_JOB_TYPES: [],
187
187
  upsertAutoAnalysisJob: mock(() => "job-auto-analysis"),
@@ -317,12 +317,11 @@ describe("POST /v1/btw", () => {
317
317
 
318
318
  expect(tools).toEqual(MOCK_TOOLS);
319
319
  expect(systemPrompt).toBe(MOCK_SYSTEM_PROMPT);
320
+ // Persona is resolved internally now; btw-routes no longer plumbs
321
+ // it through to buildSystemPrompt.
320
322
  expect(mockBuildSystemPrompt).toHaveBeenCalledWith({
321
- channelPersona: null,
322
323
  excludeBootstrap: true,
323
324
  excludeCustomPrefix: true,
324
- userPersona: null,
325
- userSlug: null,
326
325
  });
327
326
  expect(options!.config!.tool_choice).toEqual({ type: "none" });
328
327
  expect(options!.config!.callSite).toBe("identityIntro");
@@ -166,7 +166,6 @@ mock.module("../notifications/emit-signal.js", () => ({
166
166
  reason: "mocked",
167
167
  deliveryResults: [],
168
168
  }),
169
- registerBroadcastFn: () => {},
170
169
  }));
171
170
 
172
171
  mock.module("../calls/voice-session-bridge.js", () => {
@@ -49,7 +49,7 @@ mock.module("../daemon/handlers/conversations.js", () => ({
49
49
  return true;
50
50
  },
51
51
  switchConversation: async () => null,
52
- clearAllConversations: () => 0,
52
+ clearAllConversations: async () => 0,
53
53
  undoLastMessage: async () => null,
54
54
  regenerateResponse: async () => null,
55
55
  }));
@@ -2777,7 +2777,7 @@ describe("outbound voice verification", () => {
2777
2777
  }
2778
2778
  });
2779
2779
 
2780
- test("start_outbound rejects unsupported channels", async () => {
2780
+ test("start_outbound succeeds for email channel", async () => {
2781
2781
  const { lastResponse } = createResponseReader();
2782
2782
  await handleChannelVerificationSession(
2783
2783
  {
@@ -2790,8 +2790,8 @@ describe("outbound voice verification", () => {
2790
2790
 
2791
2791
  const resp = lastResponse();
2792
2792
  expect(resp).not.toBeNull();
2793
- expect(resp!.success).toBe(false);
2794
- expect(resp!.error).toBe("unsupported_channel");
2793
+ expect(resp!.success).toBe(true);
2794
+ expect(resp!.channel).toBe("email");
2795
2795
  });
2796
2796
 
2797
2797
  test("create_session without destination falls through to inbound challenge", async () => {