@vellumai/assistant 0.10.1 → 0.10.2-dev.202606241651.2d2b40d

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 (367) hide show
  1. package/docs/workspace-tools.md +42 -33
  2. package/eslint-rules/cli-no-daemon-internals.js +6 -0
  3. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-delivery-contract.test.ts +91 -0
  4. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +31 -0
  5. package/node_modules/@vellumai/gateway-client/src/guardian-delivery-contract.ts +48 -0
  6. package/node_modules/@vellumai/gateway-client/src/index.ts +14 -0
  7. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +17 -0
  8. package/openapi.yaml +74 -1
  9. package/package.json +1 -1
  10. package/scripts/test.sh +36 -15
  11. package/src/__tests__/actor-token-service.test.ts +36 -14
  12. package/src/__tests__/agent-loop-override-profile.test.ts +1 -0
  13. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  14. package/src/__tests__/agent-wake-override-profile.test.ts +2 -0
  15. package/src/__tests__/annotate-activity-metadata.test.ts +2 -0
  16. package/src/__tests__/annotate-risk-options.test.ts +2 -0
  17. package/src/__tests__/approval-cascade.test.ts +2 -0
  18. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -0
  19. package/src/__tests__/btw-routes.test.ts +2 -0
  20. package/src/__tests__/build-persisted-content.test.ts +2 -0
  21. package/src/__tests__/call-controller.test.ts +19 -0
  22. package/src/__tests__/channel-guardian.test.ts +94 -58
  23. package/src/__tests__/channel-reply-delivery.test.ts +2 -0
  24. package/src/__tests__/compaction-events.test.ts +2 -0
  25. package/src/__tests__/compaction.benchmark.test.ts +2 -0
  26. package/src/__tests__/compactor-call-site-logging.test.ts +2 -0
  27. package/src/__tests__/compactor-low-watermark-cut.test.ts +2 -0
  28. package/src/__tests__/compactor-preserved-tail-count.test.ts +2 -0
  29. package/src/__tests__/compactor-summary-call-truncation.test.ts +2 -0
  30. package/src/__tests__/compactor-web-search-strip.test.ts +2 -0
  31. package/src/__tests__/computer-use-tools.test.ts +13 -0
  32. package/src/__tests__/config-loader-backfill.test.ts +5 -1
  33. package/src/__tests__/config-schema.test.ts +1 -0
  34. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +31 -29
  35. package/src/__tests__/contacts-relay-reads.test.ts +13 -15
  36. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  37. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +2 -0
  38. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  39. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
  40. package/src/__tests__/conversation-agent-loop.test.ts +7 -0
  41. package/src/__tests__/conversation-analysis-routes.test.ts +2 -0
  42. package/src/__tests__/conversation-app-control-lifecycle.test.ts +2 -0
  43. package/src/__tests__/conversation-confirmation-signals.test.ts +2 -0
  44. package/src/__tests__/conversation-history-web-search.test.ts +2 -0
  45. package/src/__tests__/conversation-load-history-repair.test.ts +2 -0
  46. package/src/__tests__/conversation-load-history-stripped.test.ts +2 -0
  47. package/src/__tests__/conversation-pairing.test.ts +2 -0
  48. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
  49. package/src/__tests__/conversation-process-callsite.test.ts +2 -0
  50. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  51. package/src/__tests__/conversation-queue.test.ts +91 -0
  52. package/src/__tests__/conversation-routes-guardian-reply.test.ts +14 -0
  53. package/src/__tests__/conversation-routes-slash-commands.test.ts +14 -0
  54. package/src/__tests__/conversation-slash-queue.test.ts +2 -0
  55. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  56. package/src/__tests__/conversation-speed-override.test.ts +2 -0
  57. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +65 -0
  58. package/src/__tests__/conversation-title-service.test.ts +2 -0
  59. package/src/__tests__/conversation-tool-setup-attribution.test.ts +47 -0
  60. package/src/__tests__/conversation-usage.test.ts +2 -0
  61. package/src/__tests__/conversation-workspace-cache-state.test.ts +2 -0
  62. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  63. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  64. package/src/__tests__/credential-security-invariants.test.ts +0 -1
  65. package/src/__tests__/db-migration-rollback.test.ts +205 -171
  66. package/src/__tests__/db-test-helpers.ts +5 -4
  67. package/src/__tests__/deterministic-verification-control-plane.test.ts +4 -2
  68. package/src/__tests__/disk-pressure-guard.test.ts +41 -0
  69. package/src/__tests__/dm-persistence.test.ts +2 -0
  70. package/src/__tests__/emit-signal-routing-intent.test.ts +10 -5
  71. package/src/__tests__/events-dev-bypass-actor.test.ts +7 -1
  72. package/src/__tests__/filing-service.test.ts +2 -0
  73. package/src/__tests__/guardian-binding-drift-heal.test.ts +75 -10
  74. package/src/__tests__/guardian-dispatch.test.ts +95 -1
  75. package/src/__tests__/guardian-outbound-http.test.ts +13 -0
  76. package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
  77. package/src/__tests__/heartbeat-service.test.ts +2 -0
  78. package/src/__tests__/helpers/channel-test-adapter.ts +1 -7
  79. package/src/__tests__/host-app-control-routes.test.ts +24 -30
  80. package/src/__tests__/host-bash-routes.test.ts +31 -41
  81. package/src/__tests__/host-browser-routes.test.ts +26 -32
  82. package/src/__tests__/host-cu-proxy.test.ts +299 -0
  83. package/src/__tests__/host-cu-routes-targeted.test.ts +25 -33
  84. package/src/__tests__/host-file-routes-targeted.test.ts +40 -52
  85. package/src/__tests__/host-transfer-routes-targeted.test.ts +31 -43
  86. package/src/__tests__/http-user-message-parity.test.ts +167 -8
  87. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  88. package/src/__tests__/invite-redemption-service.test.ts +43 -0
  89. package/src/__tests__/llm-context-normalization.test.ts +105 -0
  90. package/src/__tests__/llm-usage-store.test.ts +25 -0
  91. package/src/__tests__/media-stream-server-integration.test.ts +127 -0
  92. package/src/__tests__/memory-retrieval-hook.test.ts +2 -0
  93. package/src/__tests__/messaging-send-tool.test.ts +2 -0
  94. package/src/__tests__/migration-import-from-url.test.ts +2 -2
  95. package/src/__tests__/native-web-search.test.ts +2 -0
  96. package/src/__tests__/non-member-access-request.test.ts +189 -17
  97. package/src/__tests__/notification-broadcaster.test.ts +4 -0
  98. package/src/__tests__/notification-decision-recipient-context.test.ts +33 -32
  99. package/src/__tests__/notification-deep-link.test.ts +6 -0
  100. package/src/__tests__/notification-guardian-path.test.ts +19 -0
  101. package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
  102. package/src/__tests__/pending-interactions-resolved-event.test.ts +7 -4
  103. package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
  104. package/src/__tests__/plugin-bootstrap.test.ts +3 -73
  105. package/src/__tests__/plugin-route-contribution.test.ts +4 -17
  106. package/src/__tests__/plugin-tool-contribution.test.ts +3 -18
  107. package/src/__tests__/plugin-types.test.ts +0 -2
  108. package/src/__tests__/process-message-background-slack.test.ts +2 -0
  109. package/src/__tests__/process-message-display-content.test.ts +2 -0
  110. package/src/__tests__/provider-usage-tracking.test.ts +39 -0
  111. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +2 -0
  112. package/src/__tests__/registry.test.ts +3 -0
  113. package/src/__tests__/relay-server.test.ts +694 -25
  114. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  115. package/src/__tests__/secret-ingress-http.test.ts +14 -0
  116. package/src/__tests__/send-endpoint-busy.test.ts +30 -8
  117. package/src/__tests__/skills.test.ts +44 -0
  118. package/src/__tests__/slack-inbound-verification.test.ts +47 -2
  119. package/src/__tests__/sse-actor-principal-guardian-source.test.ts +102 -0
  120. package/src/__tests__/steer-on-enqueue-question.test.ts +181 -0
  121. package/src/__tests__/stt-hints.test.ts +44 -13
  122. package/src/__tests__/subagent-detail.test.ts +27 -0
  123. package/src/__tests__/subagent-disposal.test.ts +65 -0
  124. package/src/__tests__/subagent-notify-parent.test.ts +2 -0
  125. package/src/__tests__/subagent-spawn-tool-fork.test.ts +2 -0
  126. package/src/__tests__/subagent-tools.test.ts +2 -0
  127. package/src/__tests__/suggestion-routes.test.ts +2 -0
  128. package/src/__tests__/title-generate-hook.test.ts +2 -0
  129. package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
  130. package/src/__tests__/tool-executor.test.ts +16 -11
  131. package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
  132. package/src/__tests__/tool-result-metadata-plumbing.test.ts +2 -0
  133. package/src/__tests__/tool-start-timestamp.test.ts +2 -0
  134. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +10 -10
  135. package/src/__tests__/twilio-routes.test.ts +96 -0
  136. package/src/__tests__/verification-control-plane-policy.test.ts +2 -0
  137. package/src/__tests__/web-search-backend-failure.test.ts +2 -0
  138. package/src/__tests__/workspace-tool-loader.test.ts +195 -2
  139. package/src/agent/loop-exclusive-tool.test.ts +150 -0
  140. package/src/agent/loop.ts +56 -0
  141. package/src/api/constants/sse-replay.ts +41 -0
  142. package/src/api/index.ts +6 -0
  143. package/src/api/responses/llm-request-log-entry.ts +25 -0
  144. package/src/api/responses/subagent-detail.ts +17 -0
  145. package/src/calls/__tests__/relay-setup-router.test.ts +262 -4
  146. package/src/calls/call-domain.ts +3 -3
  147. package/src/calls/guardian-dispatch.ts +10 -8
  148. package/src/calls/inbound-trust-reader.ts +17 -1
  149. package/src/calls/media-stream-server.ts +21 -0
  150. package/src/calls/relay-server.ts +167 -50
  151. package/src/calls/relay-setup-router.ts +37 -7
  152. package/src/calls/relay-verification.ts +4 -4
  153. package/src/calls/stt-hints.ts +9 -12
  154. package/src/calls/twilio-routes.ts +14 -4
  155. package/src/cli/commands/__tests__/cache.test.ts +8 -1
  156. package/src/cli/commands/cache.ts +194 -181
  157. package/src/cli/commands/db/__tests__/repair.test.ts +6 -5
  158. package/src/cli/commands/db/status.ts +37 -1
  159. package/src/cli/commands/mcp.ts +252 -218
  160. package/src/cli/commands/memory/__tests__/worker.test.ts +302 -0
  161. package/src/cli/commands/memory/index.ts +2 -0
  162. package/src/cli/commands/memory/worker.ts +175 -0
  163. package/src/cli/commands/plugins.ts +75 -3
  164. package/src/cli/lib/__tests__/install-from-github.test.ts +102 -0
  165. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +160 -1
  166. package/src/cli/lib/list-installed-plugins.ts +179 -1
  167. package/src/config/__tests__/loader-callsite-strip-fallback.test.ts +143 -0
  168. package/src/config/bundled-skills/computer-use/TOOLS.json +6 -1
  169. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +27 -17
  170. package/src/config/bundled-skills/contacts/tools/contact-search.ts +13 -3
  171. package/src/config/feature-flag-registry.json +0 -8
  172. package/src/config/loader.ts +36 -5
  173. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  174. package/src/config/schemas/memory-lifecycle.ts +12 -0
  175. package/src/config/schemas/memory-v3.ts +7 -0
  176. package/src/config/schemas/memory.ts +4 -0
  177. package/src/config/schemas/timeouts.ts +8 -0
  178. package/src/config/seed-inference-profiles.ts +14 -5
  179. package/src/config/skills.ts +27 -5
  180. package/src/contacts/__tests__/guardian-delivery-reader.test.ts +312 -0
  181. package/src/contacts/contacts-write.ts +3 -0
  182. package/src/contacts/guardian-delivery-reader.ts +223 -0
  183. package/src/daemon/conversation-agent-loop.ts +9 -0
  184. package/src/daemon/conversation-process.ts +39 -17
  185. package/src/daemon/conversation-surfaces.ts +8 -0
  186. package/src/daemon/conversation-tool-setup.ts +49 -16
  187. package/src/daemon/conversation.ts +21 -2
  188. package/src/daemon/disk-pressure-guard.ts +12 -2
  189. package/src/daemon/event-loop-watchdog.ts +28 -1
  190. package/src/daemon/external-plugins-bootstrap.ts +4 -34
  191. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +25 -0
  192. package/src/daemon/handlers/__tests__/config-channels.test.ts +225 -0
  193. package/src/daemon/handlers/config-a2a.ts +6 -14
  194. package/src/daemon/handlers/config-channels.ts +78 -22
  195. package/src/daemon/handlers/conversations.ts +77 -0
  196. package/src/daemon/host-cu-proxy.ts +102 -11
  197. package/src/daemon/lifecycle.ts +4 -0
  198. package/src/daemon/memory-v2-startup.test.ts +72 -0
  199. package/src/daemon/memory-v2-startup.ts +87 -19
  200. package/src/daemon/server.ts +0 -4
  201. package/src/daemon/shutdown-handlers.ts +20 -0
  202. package/src/daemon/tool-setup-types.ts +9 -0
  203. package/src/ipc/__tests__/clients-list-ipc.test.ts +1 -1
  204. package/src/ipc/assistant-server.ts +2 -2
  205. package/src/memory/__tests__/301-create-watchdog-events.test.ts +110 -0
  206. package/src/memory/__tests__/memory-retrospective-job.test.ts +8 -0
  207. package/src/memory/__tests__/prompt-override.test.ts +192 -0
  208. package/src/memory/__tests__/watchdog-events-store.test.ts +161 -0
  209. package/src/memory/conversation-crud.ts +38 -0
  210. package/src/memory/db-connection.ts +22 -3
  211. package/src/memory/db-init.ts +36 -502
  212. package/src/memory/db-singleton.ts +6 -4
  213. package/src/memory/jobs-worker.ts +58 -0
  214. package/src/memory/llm-usage-store.ts +48 -20
  215. package/src/memory/memory-retrospective-job.ts +9 -8
  216. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +13 -3
  217. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -27
  218. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +130 -56
  219. package/src/memory/migrations/300-add-processing-started-at.ts +30 -0
  220. package/src/memory/migrations/301-create-watchdog-events.ts +45 -0
  221. package/src/memory/migrations/__tests__/014-backfill-inbox-thread-state.test.ts +108 -0
  222. package/src/memory/migrations/__tests__/136-drop-assistant-id-columns.test.ts +82 -0
  223. package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +224 -0
  224. package/src/memory/migrations/__tests__/run-migrations.test.ts +2 -2
  225. package/src/memory/migrations/run-migrations.ts +90 -6
  226. package/src/memory/migrations/schema-introspection.ts +14 -0
  227. package/src/memory/migrations/validate-migration-state.ts +101 -66
  228. package/src/memory/prompt-override.ts +129 -0
  229. package/src/memory/schema/conversations.ts +9 -0
  230. package/src/memory/schema/infrastructure.ts +20 -0
  231. package/src/memory/steps.ts +573 -0
  232. package/src/memory/v2/__tests__/cli-command-store.test.ts +25 -0
  233. package/src/memory/v2/__tests__/skill-store.test.ts +80 -0
  234. package/src/memory/v2/cli-command-store.ts +75 -38
  235. package/src/memory/v2/prompts/consolidation.ts +13 -82
  236. package/src/memory/v2/prompts/router.ts +21 -93
  237. package/src/memory/v2/skill-store.ts +68 -31
  238. package/src/memory/watchdog-events-store.ts +87 -0
  239. package/src/memory/worker-control.ts +118 -0
  240. package/src/memory/worker-process.ts +72 -0
  241. package/src/notifications/__tests__/broadcaster.test.ts +16 -8
  242. package/src/notifications/__tests__/connected-channels.test.ts +114 -0
  243. package/src/notifications/__tests__/decision-engine.test.ts +78 -9
  244. package/src/notifications/__tests__/destination-resolver.test.ts +256 -0
  245. package/src/notifications/broadcaster.ts +8 -1
  246. package/src/notifications/decision-engine.ts +15 -7
  247. package/src/notifications/destination-resolver.ts +68 -24
  248. package/src/notifications/emit-signal.ts +39 -14
  249. package/src/onboarding/checkin-event.test.ts +220 -0
  250. package/src/onboarding/checkin-event.ts +321 -0
  251. package/src/onboarding/schedule-checkin.ts +190 -0
  252. package/src/permissions/question-prompter.test.ts +1 -1
  253. package/src/permissions/question-prompter.ts +7 -4
  254. package/src/plugin-api/index.ts +6 -6
  255. package/src/plugin-api/types.ts +3 -5
  256. package/src/plugin-api/vision-support.test.ts +28 -4
  257. package/src/plugin-api/vision-support.ts +66 -31
  258. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +161 -0
  259. package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +106 -0
  260. package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +60 -0
  261. package/src/plugins/defaults/advisor/consult.ts +110 -6
  262. package/src/plugins/defaults/advisor/context-pack.ts +288 -0
  263. package/src/plugins/defaults/advisor/steering.ts +14 -2
  264. package/src/plugins/defaults/advisor/tools/advisor.ts +32 -5
  265. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +47 -7
  266. package/src/plugins/defaults/image-fallback/hooks/post-tool-use.ts +10 -11
  267. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +12 -20
  268. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +42 -11
  269. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +11 -2
  270. package/src/plugins/defaults/memory-v3-shadow/pool-select.test.ts +146 -0
  271. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +29 -1
  272. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +8 -1
  273. package/src/plugins/mtime-cache.ts +7 -2
  274. package/src/plugins/types.ts +0 -2
  275. package/src/providers/anthropic/client.ts +5 -0
  276. package/src/providers/call-site-routing.ts +4 -0
  277. package/src/providers/model-catalog.ts +16 -0
  278. package/src/providers/openai/responses-provider.ts +5 -0
  279. package/src/providers/openrouter/client.ts +5 -0
  280. package/src/providers/provider-send-message.ts +4 -0
  281. package/src/providers/ratelimit.ts +4 -0
  282. package/src/providers/retry.ts +4 -0
  283. package/src/providers/types.ts +9 -0
  284. package/src/providers/usage-tracking.ts +4 -0
  285. package/src/runtime/__tests__/channel-verification-service.test.ts +133 -0
  286. package/src/runtime/__tests__/guardian-vellum-migration.test.ts +181 -0
  287. package/src/runtime/__tests__/is-guardian-bound-for-channel.test.ts +66 -0
  288. package/src/runtime/__tests__/local-principal-trust.test.ts +164 -0
  289. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +335 -3
  290. package/src/runtime/access-request-helper.ts +19 -39
  291. package/src/runtime/actor-trust-resolver.ts +2 -2
  292. package/src/runtime/anchored-guardian.test.ts +156 -0
  293. package/src/runtime/anchored-guardian.ts +135 -0
  294. package/src/runtime/assistant-event-hub.ts +1 -1
  295. package/src/runtime/assistant-stream-state.ts +9 -2
  296. package/src/runtime/auth/__tests__/require-bound-guardian.test.ts +99 -0
  297. package/src/runtime/auth/require-bound-guardian.ts +21 -11
  298. package/src/runtime/channel-verification-service.ts +56 -31
  299. package/src/runtime/confirmation-request-guardian-bridge.ts +3 -3
  300. package/src/runtime/guardian-vellum-migration.ts +66 -7
  301. package/src/runtime/invite-redemption-service.ts +50 -18
  302. package/src/runtime/local-actor-identity.ts +76 -11
  303. package/src/runtime/local-principal-trust.ts +52 -0
  304. package/src/runtime/pending-interactions.ts +11 -1
  305. package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +56 -5
  306. package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
  307. package/src/runtime/routes/__tests__/contact-routes.test.ts +212 -0
  308. package/src/runtime/routes/__tests__/global-search-routes.test.ts +93 -0
  309. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +215 -1
  310. package/src/runtime/routes/browser-routes.ts +1 -1
  311. package/src/runtime/routes/channel-verification-routes.ts +3 -3
  312. package/src/runtime/routes/contact-routes.ts +8 -32
  313. package/src/runtime/routes/conversation-cli-routes.ts +4 -5
  314. package/src/runtime/routes/conversation-list-routes.ts +4 -7
  315. package/src/runtime/routes/conversation-routes.ts +74 -81
  316. package/src/runtime/routes/events-routes.ts +2 -2
  317. package/src/runtime/routes/global-search-routes.ts +3 -1
  318. package/src/runtime/routes/guardian-action-routes.ts +4 -5
  319. package/src/runtime/routes/host-app-control-routes.ts +5 -4
  320. package/src/runtime/routes/host-bash-routes.ts +5 -4
  321. package/src/runtime/routes/host-browser-routes.ts +9 -11
  322. package/src/runtime/routes/host-cu-routes.ts +5 -4
  323. package/src/runtime/routes/host-file-routes.ts +5 -4
  324. package/src/runtime/routes/host-transfer-routes.ts +6 -6
  325. package/src/runtime/routes/http-adapter.ts +1 -1
  326. package/src/runtime/routes/identity-routes.ts +3 -2
  327. package/src/runtime/routes/inbound-message-handler.ts +5 -5
  328. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +97 -5
  329. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +61 -49
  330. package/src/runtime/routes/inbound-stages/background-dispatch.ts +16 -4
  331. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +7 -7
  332. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +21 -8
  333. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -3
  334. package/src/runtime/routes/index.ts +2 -0
  335. package/src/runtime/routes/llm-context-normalization.ts +71 -0
  336. package/src/runtime/routes/mcp-auth-routes.ts +38 -15
  337. package/src/runtime/routes/migration-rollback-routes.ts +4 -3
  338. package/src/runtime/routes/migration-routes.ts +4 -1
  339. package/src/runtime/routes/onboarding-checkin-routes.ts +86 -0
  340. package/src/runtime/routes/subagents-routes.ts +5 -0
  341. package/src/runtime/routes/surface-action-routes.ts +51 -55
  342. package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
  343. package/src/runtime/services/conversation-serializer.ts +7 -9
  344. package/src/runtime/tool-grant-request-helper.ts +3 -3
  345. package/src/runtime/trust-verdict-consumer.ts +85 -9
  346. package/src/runtime/verification-outbound-actions.ts +18 -18
  347. package/src/signals/user-message.ts +16 -0
  348. package/src/subagent/manager.ts +9 -0
  349. package/src/telemetry/types.ts +34 -1
  350. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  351. package/src/telemetry/usage-telemetry-reporter.ts +87 -3
  352. package/src/tools/ask-question/ask-question-tool.test.ts +29 -0
  353. package/src/tools/ask-question/ask-question-tool.ts +13 -0
  354. package/src/tools/computer-use/definitions.ts +8 -2
  355. package/src/tools/executor.ts +4 -4
  356. package/src/tools/registry.ts +18 -0
  357. package/src/tools/tool-approval-handler.ts +1 -1
  358. package/src/tools/tool-defaults.ts +9 -2
  359. package/src/tools/types.ts +17 -2
  360. package/src/tools/workspace-tools/loader.ts +348 -244
  361. package/src/util/platform.ts +5 -0
  362. package/src/util/telemetry-db-path.ts +24 -0
  363. package/src/workspace/migrations/017-seed-persona-dirs.ts +3 -34
  364. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +3 -24
  365. package/src/__tests__/workspace-tools-watcher-flag.test.ts +0 -70
  366. package/src/daemon/workspace-tools-watcher.ts +0 -328
  367. package/src/memory/migrations/registry.ts +0 -573
@@ -0,0 +1,110 @@
1
+ import { mkdtempSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { Database } from "bun:sqlite";
5
+ import { describe, expect, mock, test } from "bun:test";
6
+
7
+ import { drizzle } from "drizzle-orm/bun-sqlite";
8
+
9
+ // The migration opens the telemetry connection via getTelemetrySqlite(), which
10
+ // routes through the singleton. Mock the path helper so the connection opens a
11
+ // temp file we control, then mock the db-connection getter so it returns that
12
+ // same file's Database.
13
+ const tmpDir = mkdtempSync(join(tmpdir(), "wd-migration-"));
14
+ const telemetryPath = join(tmpDir, "assistant-telemetry.db");
15
+ let openedRaw: Database | null = null;
16
+
17
+ mock.module("../../util/telemetry-db-path.js", () => ({
18
+ getTelemetryDbPath: () => telemetryPath,
19
+ }));
20
+
21
+ mock.module("../db-connection.js", () => ({
22
+ // The migration calls getTelemetrySqlite(); return our temp Database so the
23
+ // DDL runs against it. getSqliteFrom is used in other contexts but not by
24
+ // this migration when the mock is active.
25
+ getTelemetrySqlite: () => openedRaw,
26
+ getSqliteFrom: (db: unknown) =>
27
+ (db as unknown as { $client: Database }).$client,
28
+ }));
29
+
30
+ import { createWatchdogEventsTable } from "../migrations/301-create-watchdog-events.js";
31
+ import * as schema from "../schema.js";
32
+
33
+ function createTestDb() {
34
+ const sqlite = new Database(":memory:");
35
+ return { sqlite, db: drizzle(sqlite, { schema }) };
36
+ }
37
+
38
+ function openTelemetryRaw(): Database {
39
+ openedRaw = new Database(telemetryPath);
40
+ return openedRaw;
41
+ }
42
+
43
+ function columnNames(raw: Database): string[] {
44
+ return (
45
+ raw.query("PRAGMA table_info(watchdog_events)").all() as Array<{
46
+ name: string;
47
+ }>
48
+ ).map((c) => c.name);
49
+ }
50
+
51
+ function indexNames(raw: Database): string[] {
52
+ return (
53
+ raw.query("PRAGMA index_list(watchdog_events)").all() as Array<{
54
+ name: string;
55
+ }>
56
+ ).map((i) => i.name);
57
+ }
58
+
59
+ describe("migration 300: watchdog_events table on telemetry DB", () => {
60
+ test("creates the table with the expected columns", () => {
61
+ const raw = openTelemetryRaw();
62
+ const { db } = createTestDb();
63
+
64
+ createWatchdogEventsTable(db);
65
+
66
+ expect(columnNames(raw)).toEqual([
67
+ "id",
68
+ "created_at",
69
+ "check_name",
70
+ "value",
71
+ "detail",
72
+ ]);
73
+ });
74
+
75
+ test("creates the (created_at, id) cursor index", () => {
76
+ const raw = openTelemetryRaw();
77
+ const { db } = createTestDb();
78
+
79
+ createWatchdogEventsTable(db);
80
+
81
+ expect(indexNames(raw)).toContain("idx_watchdog_events_created_at_id");
82
+ const columns = (
83
+ raw
84
+ .query("PRAGMA index_info(idx_watchdog_events_created_at_id)")
85
+ .all() as Array<{ name: string }>
86
+ ).map((c) => c.name);
87
+ expect(columns).toEqual(["created_at", "id"]);
88
+ });
89
+
90
+ test("is idempotent — re-run is a no-op and preserves existing rows", () => {
91
+ const raw = openTelemetryRaw();
92
+ const { db } = createTestDb();
93
+
94
+ createWatchdogEventsTable(db);
95
+ raw.exec(/*sql*/ `
96
+ INSERT INTO watchdog_events (id, created_at, check_name, value)
97
+ VALUES ('wd-1', 1000, 'event_loop_blocked', 60000)
98
+ `);
99
+
100
+ expect(() => createWatchdogEventsTable(db)).not.toThrow();
101
+
102
+ const rows = raw.query("SELECT id FROM watchdog_events").all();
103
+ expect(rows).toEqual([{ id: "wd-1" }]);
104
+ expect(
105
+ indexNames(raw).filter(
106
+ (name) => name === "idx_watchdog_events_created_at_id",
107
+ ),
108
+ ).toHaveLength(1);
109
+ });
110
+ });
@@ -161,6 +161,14 @@ mock.module("../conversation-crud.js", () => ({
161
161
  title: "Source conversation",
162
162
  };
163
163
  },
164
+ // Mirrors the real `isConversationProcessing` which reads the persisted
165
+ // `processing_started_at` column. In tests, the mock registry
166
+ // (`loadedConversations`) is the source of truth — a conversation id
167
+ // present with `processing: true` simulates a mid-turn conversation.
168
+ isConversationProcessing: (id: string) => {
169
+ const entry = loadedConversations[id];
170
+ return entry?.processing ?? false;
171
+ },
164
172
  forkConversationForRetrospective: async (params: {
165
173
  conversationId: string;
166
174
  throughMessageId?: string;
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Tests for the shared prompt-override loader
3
+ * (`assistant/src/memory/prompt-override.ts`) and the memory-v3
4
+ * `resolveSelectorPrompt` built on it. Mirrors the v2 router/consolidation
5
+ * prompt-path suites: a configured file replaces the bundled prompt, and any
6
+ * missing / empty / oversized / non-regular / unreadable file degrades to the
7
+ * bundled prompt with a diagnostic warning.
8
+ */
9
+ import { execFileSync } from "node:child_process";
10
+ import { mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
11
+ import { homedir, tmpdir } from "node:os";
12
+ import { join } from "node:path";
13
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
14
+
15
+ import { resolveSelectorPrompt } from "../../plugins/defaults/memory-v3-shadow/pool-select.js";
16
+ import {
17
+ loadPromptOverride,
18
+ MAX_PROMPT_OVERRIDE_BYTES,
19
+ resolveOverridePath,
20
+ } from "../prompt-override.js";
21
+
22
+ const warnCalls: Array<{ data: Record<string, unknown>; msg: string }> = [];
23
+ const recordingLogger = {
24
+ warn: (data: object, msg: string) => {
25
+ warnCalls.push({ data: data as Record<string, unknown>, msg });
26
+ },
27
+ };
28
+
29
+ let tmpDir: string;
30
+
31
+ beforeEach(() => {
32
+ warnCalls.length = 0;
33
+ tmpDir = mkdtempSync(join(tmpdir(), "prompt-override-"));
34
+ });
35
+
36
+ afterEach(() => {
37
+ rmSync(tmpDir, { recursive: true, force: true });
38
+ });
39
+
40
+ /** Load against the per-test temp dir with the recording logger. */
41
+ const load = (
42
+ overridePath: string | null | undefined,
43
+ label = "test prompt",
44
+ ): string | null =>
45
+ loadPromptOverride({
46
+ overridePath,
47
+ workspaceDir: tmpDir,
48
+ log: recordingLogger,
49
+ label,
50
+ });
51
+
52
+ describe("resolveOverridePath", () => {
53
+ test("expands a leading ~/ to the home directory", () => {
54
+ expect(resolveOverridePath("~/sub/x.md", tmpDir)).toBe(
55
+ join(homedir(), "sub/x.md"),
56
+ );
57
+ });
58
+
59
+ test("uses an absolute path as-is", () => {
60
+ expect(resolveOverridePath("/abs/x.md", tmpDir)).toBe("/abs/x.md");
61
+ });
62
+
63
+ test("resolves a relative path under the workspace dir", () => {
64
+ expect(resolveOverridePath("rel/x.md", tmpDir)).toBe(
65
+ join(tmpDir, "rel/x.md"),
66
+ );
67
+ });
68
+ });
69
+
70
+ describe("loadPromptOverride — usable override", () => {
71
+ test("null overridePath returns null without touching the filesystem", () => {
72
+ expect(load(null)).toBeNull();
73
+ expect(warnCalls).toHaveLength(0);
74
+ });
75
+
76
+ test("undefined overridePath (unset config field) returns null without throwing", () => {
77
+ expect(load(undefined)).toBeNull();
78
+ expect(warnCalls).toHaveLength(0);
79
+ });
80
+
81
+ test("returns an absolute-path file's contents verbatim", () => {
82
+ const p = join(tmpDir, "abs.md");
83
+ writeFileSync(p, "absolute body\n");
84
+ expect(load(p)).toBe("absolute body\n");
85
+ expect(warnCalls).toHaveLength(0);
86
+ });
87
+
88
+ test("resolves a relative path under the workspace dir", () => {
89
+ writeFileSync(join(tmpDir, "rel.md"), "relative body\n");
90
+ expect(load("rel.md")).toBe("relative body\n");
91
+ expect(warnCalls).toHaveLength(0);
92
+ });
93
+
94
+ test("expands a leading ~/ to the home directory", () => {
95
+ const filename = `.vellum-prompt-override-test-${process.pid}.md`;
96
+ const p = join(homedir(), filename);
97
+ writeFileSync(p, "home body\n");
98
+ try {
99
+ expect(load(`~/${filename}`)).toBe("home body\n");
100
+ expect(warnCalls).toHaveLength(0);
101
+ } finally {
102
+ rmSync(p, { force: true });
103
+ }
104
+ });
105
+ });
106
+
107
+ describe("loadPromptOverride — fallback to null with a diagnostic warning", () => {
108
+ test("missing file logs ENOENT and returns null", () => {
109
+ expect(load(join(tmpDir, "missing.md"))).toBeNull();
110
+ expect(warnCalls).toHaveLength(1);
111
+ expect(warnCalls[0].data.code).toBe("ENOENT");
112
+ expect(warnCalls[0].data.fallback).toBe("bundled");
113
+ });
114
+
115
+ test("empty file is rejected", () => {
116
+ const p = join(tmpDir, "empty.md");
117
+ writeFileSync(p, "");
118
+ expect(load(p)).toBeNull();
119
+ expect(warnCalls[0].data.reason).toBe("empty_override");
120
+ });
121
+
122
+ test("whitespace-only file is rejected", () => {
123
+ const p = join(tmpDir, "ws.md");
124
+ writeFileSync(p, " \n\t\n");
125
+ expect(load(p)).toBeNull();
126
+ expect(warnCalls[0].data.reason).toBe("empty_override");
127
+ });
128
+
129
+ test("oversized file is rejected with its size", () => {
130
+ const p = join(tmpDir, "huge.md");
131
+ // 1 MiB + 1 byte — just over the cap so we don't waste test memory.
132
+ writeFileSync(p, Buffer.alloc(MAX_PROMPT_OVERRIDE_BYTES + 1, 0x61));
133
+ expect(load(p)).toBeNull();
134
+ expect(warnCalls[0].data.reason).toBe("oversized_override");
135
+ expect(warnCalls[0].data.size).toBe(MAX_PROMPT_OVERRIDE_BYTES + 1);
136
+ });
137
+
138
+ test("a directory is not a regular file", () => {
139
+ expect(load(tmpDir)).toBeNull();
140
+ expect(warnCalls[0].data.reason).toBe("not_regular_file");
141
+ });
142
+
143
+ test("a symlink is not a regular file (lstat does not follow it)", () => {
144
+ const target = join(tmpDir, "target.md");
145
+ writeFileSync(target, "real body\n");
146
+ const link = join(tmpDir, "link.md");
147
+ symlinkSync(target, link);
148
+ expect(load(link)).toBeNull();
149
+ expect(warnCalls[0].data.reason).toBe("not_regular_file");
150
+ });
151
+
152
+ test("a FIFO is not a regular file", () => {
153
+ const fifoPath = join(tmpDir, "fifo");
154
+ try {
155
+ execFileSync("mkfifo", [fifoPath]);
156
+ } catch {
157
+ // mkfifo unavailable on this platform — skip without failing.
158
+ return;
159
+ }
160
+ expect(load(fifoPath)).toBeNull();
161
+ expect(warnCalls[0].data.reason).toBe("not_regular_file");
162
+ });
163
+
164
+ test("the label names the prompt in the warning message", () => {
165
+ load(join(tmpDir, "missing.md"), "router prompt");
166
+ expect(warnCalls[0].msg).toContain("router prompt override");
167
+ });
168
+ });
169
+
170
+ describe("resolveSelectorPrompt", () => {
171
+ // A stable phrase from the bundled selector prompt (`SYSTEM_PROMPT`).
172
+ const BUNDLED_PHRASE =
173
+ "Select EVERY candidate whose content the upcoming reply would draw on";
174
+
175
+ test("null path returns the bundled selector prompt", () => {
176
+ expect(resolveSelectorPrompt(null, tmpDir)).toContain(BUNDLED_PHRASE);
177
+ });
178
+
179
+ test("a configured file replaces the bundled prompt verbatim — no placeholder substitution", () => {
180
+ const body = "Custom selector instructions {{NOT_A_PLACEHOLDER}}\n";
181
+ writeFileSync(join(tmpDir, "selector.md"), body);
182
+ const out = resolveSelectorPrompt("selector.md", tmpDir);
183
+ expect(out).toBe(body);
184
+ expect(out).not.toContain(BUNDLED_PHRASE);
185
+ });
186
+
187
+ test("a missing override falls back to the bundled prompt", () => {
188
+ expect(resolveSelectorPrompt(join(tmpDir, "nope.md"), tmpDir)).toContain(
189
+ BUNDLED_PHRASE,
190
+ );
191
+ });
192
+ });
@@ -0,0 +1,161 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ // Silence the logger.
4
+ mock.module("../../util/logger.js", () => ({
5
+ getLogger: () =>
6
+ new Proxy({} as Record<string, unknown>, {
7
+ get: () => () => {},
8
+ }),
9
+ }));
10
+
11
+ let shareAnalytics = true;
12
+
13
+ mock.module("../../platform/consent-cache.js", () => ({
14
+ getCachedShareAnalytics: () => shareAnalytics,
15
+ }));
16
+
17
+ import { getTelemetryDb } from "../db-connection.js";
18
+ import { initializeDb } from "../db-init.js";
19
+ import { watchdogEvents } from "../schema.js";
20
+ import {
21
+ queryUnreportedWatchdogEvents,
22
+ recordWatchdogEvent,
23
+ } from "../watchdog-events-store.js";
24
+
25
+ await initializeDb();
26
+
27
+ function insertEvent(
28
+ id: string,
29
+ createdAt: number,
30
+ checkName = "event_loop_blocked",
31
+ ): void {
32
+ const db = getTelemetryDb();
33
+ if (!db) throw new Error("telemetry DB unavailable in test");
34
+ db.insert(watchdogEvents).values({ id, createdAt, checkName }).run();
35
+ }
36
+
37
+ function clearEvents(): void {
38
+ const db = getTelemetryDb();
39
+ if (!db) throw new Error("telemetry DB unavailable in test");
40
+ db.delete(watchdogEvents).run();
41
+ }
42
+
43
+ describe("watchdog-events-store", () => {
44
+ beforeEach(() => {
45
+ shareAnalytics = true;
46
+ clearEvents();
47
+ });
48
+
49
+ test("honors the share_analytics opt-out (records nothing)", () => {
50
+ shareAnalytics = false;
51
+ recordWatchdogEvent({ checkName: "event_loop_blocked", value: 60000 });
52
+ expect(queryUnreportedWatchdogEvents(0, undefined, 10)).toHaveLength(0);
53
+ });
54
+
55
+ test("record + query round-trips all fields", () => {
56
+ recordWatchdogEvent({
57
+ checkName: "event_loop_blocked",
58
+ value: 12345,
59
+ detail: { reason: "no_bytes_60s", threshold_ms: 5000 },
60
+ });
61
+
62
+ const rows = queryUnreportedWatchdogEvents(0, undefined, 10);
63
+ expect(rows).toHaveLength(1);
64
+ const row = rows[0]!;
65
+ expect(row.id).toBeString();
66
+ expect(row.createdAt).toBeGreaterThan(0);
67
+ expect(row.checkName).toBe("event_loop_blocked");
68
+ expect(row.value).toBe(12345);
69
+ expect(row.detail).toBe(
70
+ JSON.stringify({ reason: "no_bytes_60s", threshold_ms: 5000 }),
71
+ );
72
+ });
73
+
74
+ test("optional fields persist as null", () => {
75
+ recordWatchdogEvent({ checkName: "stream_idle" });
76
+
77
+ const rows = queryUnreportedWatchdogEvents(0, undefined, 10);
78
+ expect(rows).toHaveLength(1);
79
+ expect(rows[0]).toMatchObject({
80
+ checkName: "stream_idle",
81
+ value: null,
82
+ detail: null,
83
+ });
84
+ });
85
+
86
+ test("explicit null value and detail persist as null", () => {
87
+ recordWatchdogEvent({
88
+ checkName: "restart",
89
+ value: null,
90
+ detail: null,
91
+ });
92
+
93
+ const rows = queryUnreportedWatchdogEvents(0, undefined, 10);
94
+ expect(rows).toHaveLength(1);
95
+ expect(rows[0]?.value).toBeNull();
96
+ expect(rows[0]?.detail).toBeNull();
97
+ });
98
+
99
+ test("returns rows in (createdAt, id) order", () => {
100
+ insertEvent("wd-b", 2000);
101
+ insertEvent("wd-a", 1000);
102
+
103
+ const rows = queryUnreportedWatchdogEvents(0, undefined, 100);
104
+ expect(rows.map((r) => r.id)).toEqual(["wd-a", "wd-b"]);
105
+ });
106
+
107
+ test("query advances past the compound (createdAt, id) cursor", () => {
108
+ // Two rows in the same millisecond: pagination must use the id
109
+ // tiebreaker to make forward progress, not loop.
110
+ insertEvent("wd-1", 5000);
111
+ insertEvent("wd-2", 5000);
112
+ insertEvent("wd-3", 6000);
113
+
114
+ const first = queryUnreportedWatchdogEvents(0, undefined, 1);
115
+ expect(first.map((r) => r.id)).toEqual(["wd-1"]);
116
+
117
+ const second = queryUnreportedWatchdogEvents(
118
+ first[0]!.createdAt,
119
+ first[0]!.id,
120
+ 100,
121
+ );
122
+ expect(second.map((r) => r.id)).toEqual(["wd-2", "wd-3"]);
123
+
124
+ // Without an id cursor the timestamp-only branch is used.
125
+ expect(
126
+ queryUnreportedWatchdogEvents(5000, undefined, 100).map((r) => r.id),
127
+ ).toEqual(["wd-3"]);
128
+
129
+ // Cursor past the last row returns nothing.
130
+ const last = second[second.length - 1]!;
131
+ expect(
132
+ queryUnreportedWatchdogEvents(last.createdAt, last.id, 100).length,
133
+ ).toBe(0);
134
+ });
135
+
136
+ test("resumes from a persisted watermark without re-reporting", () => {
137
+ insertEvent("wd-w1", 1000);
138
+ insertEvent("wd-w2", 2000);
139
+
140
+ const batch = queryUnreportedWatchdogEvents(0, undefined, 100);
141
+ const watermark = batch[batch.length - 1]!;
142
+
143
+ insertEvent("wd-w3", 3000);
144
+
145
+ const resumed = queryUnreportedWatchdogEvents(
146
+ watermark.createdAt,
147
+ watermark.id,
148
+ 100,
149
+ );
150
+ expect(resumed.map((r) => r.id)).toEqual(["wd-w3"]);
151
+ });
152
+
153
+ test("honors the limit", () => {
154
+ insertEvent("wd-l1", 1000);
155
+ insertEvent("wd-l2", 2000);
156
+ insertEvent("wd-l3", 3000);
157
+
158
+ const rows = queryUnreportedWatchdogEvents(0, undefined, 2);
159
+ expect(rows.map((r) => r.id)).toEqual(["wd-l1", "wd-l2"]);
160
+ });
161
+ });
@@ -25,6 +25,7 @@ import { parseChannelId, parseInterfaceId } from "../channels/types.js";
25
25
  import { CHANNEL_IDS, isChannelId } from "../channels/types.js";
26
26
  import { getConfig } from "../config/loader.js";
27
27
  import { findDisplayTurnEndIndex } from "../conversations/message-consolidation.js";
28
+ import { findConversation } from "../daemon/conversation-registry.js";
28
29
  import { conversationMetadataSyncTag } from "../daemon/message-types/sync.js";
29
30
  import type { TrustContext } from "../daemon/trust-context.js";
30
31
  import { clearAllConversationIds } from "../home/feed-writer.js";
@@ -304,6 +305,7 @@ export interface ConversationRow {
304
305
  inferenceProfileSessionId: string | null;
305
306
  inferenceProfileExpiresAt: number | null;
306
307
  lastNotifiedInferenceProfile: string | null;
308
+ processingStartedAt: number | null;
307
309
  }
308
310
 
309
311
  export const parseConversation = createRowMapper<
@@ -339,6 +341,7 @@ export const parseConversation = createRowMapper<
339
341
  inferenceProfileSessionId: "inferenceProfileSessionId",
340
342
  inferenceProfileExpiresAt: "inferenceProfileExpiresAt",
341
343
  lastNotifiedInferenceProfile: "lastNotifiedInferenceProfile",
344
+ processingStartedAt: "processingStartedAt",
342
345
  });
343
346
 
344
347
  /** Allowed values for the `role` column on `messages`. */
@@ -2180,6 +2183,41 @@ export function unarchiveConversation(id: string): boolean {
2180
2183
  return true;
2181
2184
  }
2182
2185
 
2186
+ /**
2187
+ * Persist the processing-start timestamp for a conversation. Called by
2188
+ * `Conversation.setProcessing(true)` so out-of-process callers can detect
2189
+ * mid-turn state by reading the `conversations` row directly. Pass `null`
2190
+ * to clear (turn ended).
2191
+ */
2192
+ export function setConversationProcessingStartedAt(
2193
+ id: string,
2194
+ startedAt: number | null,
2195
+ ): void {
2196
+ rawRun(
2197
+ "UPDATE conversations SET processing_started_at = ? WHERE id = ?",
2198
+ startedAt,
2199
+ id,
2200
+ );
2201
+ }
2202
+
2203
+ /**
2204
+ * Read whether a conversation is currently processing. Checks the in-memory
2205
+ * `Conversation._processing` flag first (hot path for resident conversations),
2206
+ * falling back to the persisted `processing_started_at` column for cold
2207
+ * (evicted / never-loaded) conversations. This is the single entry point for
2208
+ * processing state — callers don't need to layer `findConversation` themselves.
2209
+ * Returns `false` when the conversation row doesn't exist.
2210
+ */
2211
+ export function isConversationProcessing(id: string): boolean {
2212
+ const inMemory = findConversation(id)?.isProcessing();
2213
+ if (inMemory != null) return inMemory;
2214
+ const row = rawGet<{ processing_started_at: number | null }>(
2215
+ "SELECT processing_started_at FROM conversations WHERE id = ?",
2216
+ id,
2217
+ );
2218
+ return row?.processing_started_at != null;
2219
+ }
2220
+
2183
2221
  /**
2184
2222
  * Set or clear the `surfaced_at` promotion marker for a conversation.
2185
2223
  *
@@ -8,6 +8,7 @@ import { drizzle } from "drizzle-orm/bun-sqlite";
8
8
  import { getLogsDbPath } from "../util/logs-db-path.js";
9
9
  import { getMemoryDbPath } from "../util/memory-db-path.js";
10
10
  import { ensureDataDir, getDbPath } from "../util/platform.js";
11
+ import { getTelemetryDbPath } from "../util/telemetry-db-path.js";
11
12
  import { clearStoredDb, getStoredDb, setStoredDb } from "./db-singleton.js";
12
13
  import * as schema from "./schema.js";
13
14
 
@@ -136,7 +137,7 @@ export function getSqliteFrom(drizzleDb: DrizzleDb): Database {
136
137
  * path so this file stays import-light. Callers tolerate a `null` result.
137
138
  */
138
139
  function openDedicatedDb(
139
- key: "logs" | "memory",
140
+ key: "logs" | "memory" | "telemetry",
140
141
  dbPath: string,
141
142
  ): DrizzleDb | null {
142
143
  assertTestDbIsIsolated();
@@ -192,11 +193,28 @@ export function getMemorySqlite(): Database | null {
192
193
  return db ? getSqliteFrom(db) : null;
193
194
  }
194
195
 
196
+ /**
197
+ * The telemetry database (`assistant-telemetry.db`), opened lazily as its own
198
+ * connection. Houses telemetry event tables (starting with `watchdog_events`).
199
+ * Returns `null` if the file cannot be opened (logged; the daemon stays up).
200
+ */
201
+ export function getTelemetryDb(): DrizzleDb | null {
202
+ const existing = getStoredDb<DrizzleDb>("telemetry");
203
+ if (existing) return existing;
204
+ return openDedicatedDb("telemetry", getTelemetryDbPath());
205
+ }
206
+
207
+ /** Underlying bun:sqlite Database for the telemetry connection, or `null`. */
208
+ export function getTelemetrySqlite(): Database | null {
209
+ const db = getTelemetryDb();
210
+ return db ? getSqliteFrom(db) : null;
211
+ }
212
+
195
213
  /**
196
214
  * Reset all DB singletons. Used by production callers that need to close the
197
215
  * live connections so the files can be replaced (post-migration, post-restore,
198
- * post-vbundle-import) and on graceful shutdown. Clears the main, logs, and
199
- * memory slots together so none lingers open against a swapped-out file.
216
+ * post-vbundle-import) and on graceful shutdown. Clears the main, logs, memory,
217
+ * and telemetry slots together so none lingers open against a swapped-out file.
200
218
  *
201
219
  * Tests should use `resetDbForTesting()` from
202
220
  * `__tests__/db-test-helpers.ts` instead so they don't depend on this
@@ -206,4 +224,5 @@ export function resetDb(): void {
206
224
  clearStoredDb("main");
207
225
  clearStoredDb("logs");
208
226
  clearStoredDb("memory");
227
+ clearStoredDb("telemetry");
209
228
  }