@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
@@ -32,6 +32,8 @@ mock.module("../util/logger.js", () => ({
32
32
  }));
33
33
 
34
34
  mock.module("../memory/conversation-crud.js", () => ({
35
+ setConversationProcessingStartedAt: () => {},
36
+ isConversationProcessing: () => false,
35
37
  getMessages: () => [],
36
38
  }));
37
39
 
@@ -22,6 +22,8 @@ mock.module("../util/logger.js", () => ({
22
22
  }));
23
23
 
24
24
  mock.module("../memory/conversation-crud.js", () => ({
25
+ setConversationProcessingStartedAt: () => {},
26
+ isConversationProcessing: () => false,
25
27
  getMessages: () => [],
26
28
  reserveMessage: mock(async () => ({ id: "msg-reserve" })),
27
29
  }));
@@ -32,6 +32,8 @@ mock.module("../util/logger.js", () => ({
32
32
  }));
33
33
 
34
34
  mock.module("../memory/conversation-crud.js", () => ({
35
+ setConversationProcessingStartedAt: () => {},
36
+ isConversationProcessing: () => false,
35
37
  getMessages: () => [],
36
38
  }));
37
39
 
@@ -22,6 +22,8 @@ mock.module("../util/logger.js", () => ({
22
22
  }));
23
23
 
24
24
  mock.module("../memory/conversation-crud.js", () => ({
25
+ setConversationProcessingStartedAt: () => {},
26
+ isConversationProcessing: () => false,
25
27
  getMessages: () => [],
26
28
  reserveMessage: mock(async () => ({ id: "msg-reserve" })),
27
29
  }));
@@ -6,6 +6,7 @@ import {
6
6
  computerUseDoneTool,
7
7
  computerUseDragTool,
8
8
  computerUseKeyTool,
9
+ computerUseObserveTool,
9
10
  computerUseOpenAppTool,
10
11
  computerUseRespondTool,
11
12
  computerUseRunAppleScriptTool,
@@ -58,6 +59,18 @@ describe("computer-use tool definitions", () => {
58
59
  });
59
60
  });
60
61
 
62
+ // ── observe ─────────────────────────────────────────────────────────
63
+
64
+ describe("computer_use_observe", () => {
65
+ test("supports target_client_id", () => {
66
+ const props = schema(computerUseObserveTool).properties as Record<
67
+ string,
68
+ { type: string }
69
+ >;
70
+ expect(props.target_client_id.type).toBe("string");
71
+ });
72
+ });
73
+
61
74
  // ── Unified click tool ──────────────────────────────────────────────
62
75
 
63
76
  describe("computer_use_click (unified)", () => {
@@ -745,7 +745,11 @@ describe("loadConfig startup behavior", () => {
745
745
  );
746
746
  expect(raw.llm.profiles.frontier.provider).toBe("anthropic");
747
747
  expect(raw.llm.profiles.frontier.model).toBe("claude-opus-4-8");
748
- expect(raw.llm.profiles["cost-optimized"].provider).toBe("anthropic");
748
+ // Speed is served by DeepSeek V4 Flash on Fireworks.
749
+ expect(raw.llm.profiles["cost-optimized"].provider).toBe("fireworks");
750
+ expect(raw.llm.profiles["cost-optimized"].model).toBe(
751
+ "accounts/fireworks/models/deepseek-v4-flash",
752
+ );
749
753
  });
750
754
 
751
755
  test("off-platform managed profiles are overwritten on every boot", () => {
@@ -120,6 +120,7 @@ describe("AssistantConfigSchema", () => {
120
120
  shellDefaultTimeoutSec: 120,
121
121
  shellMaxTimeoutSec: 600,
122
122
  permissionTimeoutSec: 300,
123
+ questionResponseTimeoutSec: 1800,
123
124
  toolExecutionTimeoutSec: 120,
124
125
  providerStreamTimeoutSec: 1800,
125
126
  backgroundTurnTimeoutSec: 1800,
@@ -135,11 +135,11 @@ describe("bridgeConfirmationRequestToGuardian", () => {
135
135
  mockOnConversationCreatedCallbacks.length = 0;
136
136
  });
137
137
 
138
- test("emits guardian.question for trusted-contact sessions", () => {
138
+ test("emits guardian.question for trusted-contact sessions", async () => {
139
139
  const canonicalRequest = makeCanonicalRequest();
140
140
  const trustContext = makeTrustedContactContext();
141
141
 
142
- const result = bridgeConfirmationRequestToGuardian({
142
+ const result = await bridgeConfirmationRequestToGuardian({
143
143
  canonicalRequest,
144
144
  trustContext,
145
145
  conversationId: "conv-1",
@@ -160,7 +160,7 @@ describe("bridgeConfirmationRequestToGuardian", () => {
160
160
  expect(payload.requesterIdentifier).toBe("@requester");
161
161
  });
162
162
 
163
- test("skips guardian actor sessions (self-approve)", () => {
163
+ test("skips guardian actor sessions (self-approve)", async () => {
164
164
  const canonicalRequest = makeCanonicalRequest();
165
165
  const trustContext: TrustContext = {
166
166
  sourceChannel: "telegram",
@@ -168,7 +168,7 @@ describe("bridgeConfirmationRequestToGuardian", () => {
168
168
  guardianExternalUserId: "guardian-1",
169
169
  };
170
170
 
171
- const result = bridgeConfirmationRequestToGuardian({
171
+ const result = await bridgeConfirmationRequestToGuardian({
172
172
  canonicalRequest,
173
173
  trustContext,
174
174
  conversationId: "conv-1",
@@ -182,14 +182,14 @@ describe("bridgeConfirmationRequestToGuardian", () => {
182
182
  expect(emittedSignals).toHaveLength(0);
183
183
  });
184
184
 
185
- test("skips unknown actor sessions", () => {
185
+ test("skips unknown actor sessions", async () => {
186
186
  const canonicalRequest = makeCanonicalRequest();
187
187
  const trustContext: TrustContext = {
188
188
  sourceChannel: "telegram",
189
189
  trustClass: "unknown",
190
190
  };
191
191
 
192
- const result = bridgeConfirmationRequestToGuardian({
192
+ const result = await bridgeConfirmationRequestToGuardian({
193
193
  canonicalRequest,
194
194
  trustContext,
195
195
  conversationId: "conv-1",
@@ -203,13 +203,13 @@ describe("bridgeConfirmationRequestToGuardian", () => {
203
203
  expect(emittedSignals).toHaveLength(0);
204
204
  });
205
205
 
206
- test("skips when guardian identity is missing", () => {
206
+ test("skips when guardian identity is missing", async () => {
207
207
  const canonicalRequest = makeCanonicalRequest();
208
208
  const trustContext = makeTrustedContactContext({
209
209
  guardianExternalUserId: undefined,
210
210
  });
211
211
 
212
- const result = bridgeConfirmationRequestToGuardian({
212
+ const result = await bridgeConfirmationRequestToGuardian({
213
213
  canonicalRequest,
214
214
  trustContext,
215
215
  conversationId: "conv-1",
@@ -223,13 +223,13 @@ describe("bridgeConfirmationRequestToGuardian", () => {
223
223
  expect(emittedSignals).toHaveLength(0);
224
224
  });
225
225
 
226
- test("skips when no guardian binding exists for channel", () => {
226
+ test("skips when no guardian binding exists for channel", async () => {
227
227
  const canonicalRequest = makeCanonicalRequest({ sourceChannel: "phone" });
228
228
  const trustContext = makeTrustedContactContext({
229
229
  sourceChannel: "phone",
230
230
  });
231
231
 
232
- const result = bridgeConfirmationRequestToGuardian({
232
+ const result = await bridgeConfirmationRequestToGuardian({
233
233
  canonicalRequest,
234
234
  trustContext,
235
235
  conversationId: "conv-1",
@@ -243,11 +243,11 @@ describe("bridgeConfirmationRequestToGuardian", () => {
243
243
  expect(emittedSignals).toHaveLength(0);
244
244
  });
245
245
 
246
- test("sets correct attention hints for urgency", () => {
246
+ test("sets correct attention hints for urgency", async () => {
247
247
  const canonicalRequest = makeCanonicalRequest();
248
248
  const trustContext = makeTrustedContactContext();
249
249
 
250
- bridgeConfirmationRequestToGuardian({
250
+ await bridgeConfirmationRequestToGuardian({
251
251
  canonicalRequest,
252
252
  trustContext,
253
253
  conversationId: "conv-1",
@@ -261,11 +261,11 @@ describe("bridgeConfirmationRequestToGuardian", () => {
261
261
  expect(hints.visibleInSourceNow).toBe(false);
262
262
  });
263
263
 
264
- test("uses dedupe key scoped to canonical request ID", () => {
264
+ test("uses dedupe key scoped to canonical request ID", async () => {
265
265
  const canonicalRequest = makeCanonicalRequest();
266
266
  const trustContext = makeTrustedContactContext();
267
267
 
268
- bridgeConfirmationRequestToGuardian({
268
+ await bridgeConfirmationRequestToGuardian({
269
269
  canonicalRequest,
270
270
  trustContext,
271
271
  conversationId: "conv-1",
@@ -277,11 +277,11 @@ describe("bridgeConfirmationRequestToGuardian", () => {
277
277
  );
278
278
  });
279
279
 
280
- test("creates vellum delivery row via onConversationCreated callback", () => {
280
+ test("creates vellum delivery row via onConversationCreated callback", async () => {
281
281
  const canonicalRequest = makeCanonicalRequest();
282
282
  const trustContext = makeTrustedContactContext();
283
283
 
284
- bridgeConfirmationRequestToGuardian({
284
+ await bridgeConfirmationRequestToGuardian({
285
285
  canonicalRequest,
286
286
  trustContext,
287
287
  conversationId: "conv-1",
@@ -298,18 +298,20 @@ describe("bridgeConfirmationRequestToGuardian", () => {
298
298
  });
299
299
 
300
300
  const deliveries = listCanonicalGuardianDeliveries(canonicalRequest.id);
301
- expect(deliveries).toHaveLength(1);
302
- expect(deliveries[0].destinationChannel).toBe("vellum");
303
- expect(deliveries[0].destinationConversationId).toBe(
301
+ const vellumDelivery = deliveries.find(
302
+ (d) => d.destinationChannel === "vellum",
303
+ );
304
+ expect(vellumDelivery).toBeDefined();
305
+ expect(vellumDelivery?.destinationConversationId).toBe(
304
306
  "guardian-conversation-1",
305
307
  );
306
308
  });
307
309
 
308
- test("uses custom assistantId when provided", () => {
310
+ test("uses custom assistantId when provided", async () => {
309
311
  const canonicalRequest = makeCanonicalRequest();
310
312
  const trustContext = makeTrustedContactContext();
311
313
 
312
- bridgeConfirmationRequestToGuardian({
314
+ await bridgeConfirmationRequestToGuardian({
313
315
  canonicalRequest,
314
316
  trustContext,
315
317
  conversationId: "conv-1",
@@ -324,13 +326,13 @@ describe("bridgeConfirmationRequestToGuardian", () => {
324
326
  expect(emittedSignals).toHaveLength(0);
325
327
  });
326
328
 
327
- test("does not pass assistantId to notification signal", () => {
329
+ test("does not pass assistantId to notification signal", async () => {
328
330
  const canonicalRequest = makeCanonicalRequest();
329
331
  const trustContext = makeTrustedContactContext();
330
332
 
331
333
  // assistantId is used internally for guardian binding lookup but is no
332
334
  // longer forwarded to the notification signal after the assistantId removal refactor.
333
- bridgeConfirmationRequestToGuardian({
335
+ await bridgeConfirmationRequestToGuardian({
334
336
  canonicalRequest,
335
337
  trustContext,
336
338
  conversationId: "conv-1",
@@ -340,13 +342,13 @@ describe("bridgeConfirmationRequestToGuardian", () => {
340
342
  expect(emittedSignals[0].assistantId).toBeUndefined();
341
343
  });
342
344
 
343
- test("includes requesterChatId as null when not provided", () => {
345
+ test("includes requesterChatId as null when not provided", async () => {
344
346
  const canonicalRequest = makeCanonicalRequest();
345
347
  const trustContext = makeTrustedContactContext({
346
348
  requesterChatId: undefined,
347
349
  });
348
350
 
349
- bridgeConfirmationRequestToGuardian({
351
+ await bridgeConfirmationRequestToGuardian({
350
352
  canonicalRequest,
351
353
  trustContext,
352
354
  conversationId: "conv-1",
@@ -357,7 +359,7 @@ describe("bridgeConfirmationRequestToGuardian", () => {
357
359
  expect(payload.requesterChatId).toBeNull();
358
360
  });
359
361
 
360
- test("skips when binding guardian identity does not match canonical request guardian", () => {
362
+ test("skips when binding guardian identity does not match canonical request guardian", async () => {
361
363
  // Create a canonical request where guardianExternalUserId differs from the
362
364
  // binding's guardianExternalUserId ('guardian-1' in the mock).
363
365
  const canonicalRequest = makeCanonicalRequest({
@@ -365,7 +367,7 @@ describe("bridgeConfirmationRequestToGuardian", () => {
365
367
  });
366
368
  const trustContext = makeTrustedContactContext();
367
369
 
368
- const result = bridgeConfirmationRequestToGuardian({
370
+ const result = await bridgeConfirmationRequestToGuardian({
369
371
  canonicalRequest,
370
372
  trustContext,
371
373
  conversationId: "conv-1",
@@ -379,7 +381,7 @@ describe("bridgeConfirmationRequestToGuardian", () => {
379
381
  expect(emittedSignals).toHaveLength(0);
380
382
  });
381
383
 
382
- test("does not skip when canonical request guardian identity is null", () => {
384
+ test("does not skip when canonical request guardian identity is null", async () => {
383
385
  // When guardianExternalUserId is null on the canonical request (e.g. desktop
384
386
  // flow), the identity check should be skipped and the bridge should proceed.
385
387
  const canonicalRequest = makeCanonicalRequest({
@@ -387,7 +389,7 @@ describe("bridgeConfirmationRequestToGuardian", () => {
387
389
  });
388
390
  const trustContext = makeTrustedContactContext();
389
391
 
390
- const result = bridgeConfirmationRequestToGuardian({
392
+ const result = await bridgeConfirmationRequestToGuardian({
391
393
  canonicalRequest,
392
394
  trustContext,
393
395
  conversationId: "conv-1",
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * handleListContacts (non-search) and handleGetContact relay to the gateway
5
5
  * via `ipcCallPersistent`. On the happy path they serve gateway-sourced data
6
- * and do NOT read the assistant DB. On IPC failure they fall back to the
7
- * assistant-DB read and log a warning. getContact still 404s for unknown ids.
6
+ * and do NOT read the assistant DB. On IPC failure they FAIL CLOSED the relay
7
+ * error propagates rather than falling back to the assistant DB. getContact
8
+ * surfaces a clean gateway not-found as a 404.
8
9
  */
9
10
 
10
11
  import { beforeEach, describe, expect, mock, test } from "bun:test";
@@ -213,16 +214,15 @@ describe("handleListContacts relay", () => {
213
214
  expect(result.contacts[0].displayName).toBe("Your Guardian");
214
215
  });
215
216
 
216
- test("falls back to the assistant DB on IPC failure", async () => {
217
+ test("fails closed on IPC failure (no assistant-DB fallback)", async () => {
217
218
  ipcStub = () => {
218
219
  throw new Error("gateway down");
219
220
  };
220
221
 
221
- const result = await handleListContacts({});
222
+ await expect(handleListContacts({})).rejects.toThrow("gateway down");
222
223
 
223
224
  expect(ipcCalls.map((c) => c.method)).toEqual(["contacts_list_rich"]);
224
- expect(localCalls).toContain("listContacts");
225
- expect(result.contacts[0].id).toBe("local-1");
225
+ expect(localCalls).toEqual([]);
226
226
  });
227
227
 
228
228
  test("search params stay daemon-native and log the boundary note", async () => {
@@ -384,26 +384,24 @@ describe("handleGetContact relay", () => {
384
384
  expect(localCalls).toEqual([]);
385
385
  });
386
386
 
387
- test("falls back to the assistant DB on IPC transport failure", async () => {
387
+ test("fails closed on IPC transport failure (no assistant-DB fallback)", async () => {
388
388
  ipcStub = () => {
389
389
  throw new Error("gateway down");
390
390
  };
391
391
 
392
- const result = await handleGetContact("gw-1");
392
+ await expect(handleGetContact("gw-1")).rejects.toThrow("gateway down");
393
393
 
394
394
  expect(ipcCalls.map((c) => c.method)).toEqual(["contacts_get_rich"]);
395
- expect(localCalls).toContain("getContact");
396
- expect(result.contact.id).toBe("gw-1");
395
+ expect(localCalls).toEqual([]);
397
396
  });
398
397
 
399
- test("fallback path still 404s for unknown ids", async () => {
400
- ipcStub = () => {
401
- throw new Error("gateway down");
402
- };
398
+ test("clean gateway not-found surfaces as a 404 for unknown ids", async () => {
399
+ ipcStub = () => null;
403
400
 
404
401
  await expect(handleGetContact("missing")).rejects.toThrow(
405
402
  'Contact "missing" not found',
406
403
  );
407
- expect(localCalls).toContain("getContact");
404
+ expect(ipcCalls.map((c) => c.method)).toEqual(["contacts_get_rich"]);
405
+ expect(localCalls).toEqual([]);
408
406
  });
409
407
  });
@@ -112,6 +112,8 @@ let reservedRowContent: Map<string, string> = new Map();
112
112
  let reserveCounter = 0;
113
113
 
114
114
  mock.module("../memory/conversation-crud.js", () => ({
115
+ setConversationProcessingStartedAt: () => {},
116
+ isConversationProcessing: () => false,
115
117
  setConversationOriginChannelIfUnset: () => {},
116
118
  updateConversationContextWindow: () => {},
117
119
  deleteMessageById: () => {},
@@ -87,6 +87,8 @@ mock.module("../daemon/disk-pressure-guard.js", () => ({
87
87
  }));
88
88
 
89
89
  mock.module("../memory/conversation-crud.js", () => ({
90
+ setConversationProcessingStartedAt: () => {},
91
+ isConversationProcessing: () => false,
90
92
  getConversation: () => ({
91
93
  id: "conv-123",
92
94
  conversationType: "background",
@@ -147,6 +147,8 @@ let mockConversationRow: {
147
147
  };
148
148
 
149
149
  mock.module("../memory/conversation-crud.js", () => ({
150
+ setConversationProcessingStartedAt: () => {},
151
+ isConversationProcessing: () => false,
150
152
  setConversationOriginChannelIfUnset: () => {},
151
153
  setConversationHistoryStrippedAt: () => {},
152
154
  updateConversationUsage: () => {},
@@ -257,6 +257,8 @@ mock.module("../plugins/defaults/compaction/overflow-policy.js", () => ({
257
257
  }));
258
258
 
259
259
  mock.module("../memory/conversation-crud.js", () => ({
260
+ setConversationProcessingStartedAt: () => {},
261
+ isConversationProcessing: () => false,
260
262
  setConversationOriginChannelIfUnset: () => {},
261
263
  setConversationHistoryStrippedAt: () => {},
262
264
  updateConversationUsage: () => {},
@@ -273,6 +273,8 @@ const deleteMessageByIdMock = mock(() => ({
273
273
  const reserveMessageMock = mock(async () => ({ id: "msg-reserve" }));
274
274
  const updateMessageContentMock = mock(() => {});
275
275
  mock.module("../memory/conversation-crud.js", () => ({
276
+ setConversationProcessingStartedAt: () => {},
277
+ isConversationProcessing: () => false,
276
278
  setConversationOriginChannelIfUnset: () => {},
277
279
  updateConversationUsage: () => {},
278
280
  updateMessageMetadata: updateMessageMetadataMock,
@@ -700,6 +702,7 @@ function makeCtx(
700
702
  mockConversationRow?.slackContextCompactionWatermarkTs ?? null,
701
703
  lastNotifiedInferenceProfile:
702
704
  mockConversationRow?.lastNotifiedInferenceProfile ?? null,
705
+ processingStartedAt: mockConversationRow?.processingStartedAt ?? null,
703
706
 
704
707
  memoryPolicy: { scopeId: "default", includeDefaultFallback: true },
705
708
 
@@ -1705,6 +1708,10 @@ describe("session-agent-loop", () => {
1705
1708
  expect(ctx.abortController).toBeNull();
1706
1709
  expect(ctx.currentRequestId).toBeUndefined();
1707
1710
  expect(ctx.commandIntent).toBeUndefined();
1711
+ // Turn-scoped interactivity is stamped during the run and must be cleared
1712
+ // so paths that bypass this loop (e.g. opportunity wakes) don't inherit a
1713
+ // stale value instead of falling back to live client state.
1714
+ expect(ctx.currentTurnIsNonInteractive).toBeUndefined();
1708
1715
  });
1709
1716
 
1710
1717
  test("clears state and surfaces a processing error when the provider call fails", async () => {
@@ -22,6 +22,8 @@ mock.module("../memory/conversation-key-store.js", () => ({
22
22
  }));
23
23
 
24
24
  mock.module("../memory/conversation-crud.js", () => ({
25
+ setConversationProcessingStartedAt: () => {},
26
+ isConversationProcessing: () => false,
25
27
  getConversation: mockGetConversation,
26
28
  getMessages: mockGetMessages,
27
29
  createConversation: mockCreateConversation,
@@ -76,6 +76,8 @@ mock.module("../security/secret-allowlist.js", () => ({
76
76
  }));
77
77
 
78
78
  mock.module("../memory/conversation-crud.js", () => ({
79
+ setConversationProcessingStartedAt: () => {},
80
+ isConversationProcessing: () => false,
79
81
  updateConversationContextWindow: () => {},
80
82
  deleteMessageById: () => {},
81
83
  updateConversationTitle: () => {},
@@ -112,6 +112,8 @@ mock.module("../security/secret-allowlist.js", () => ({
112
112
  }));
113
113
 
114
114
  mock.module("../memory/conversation-crud.js", () => ({
115
+ setConversationProcessingStartedAt: () => {},
116
+ isConversationProcessing: () => false,
115
117
  setConversationOriginChannelIfUnset: () => {},
116
118
  updateConversationContextWindow: () => {},
117
119
  deleteMessageById: () => {},
@@ -36,6 +36,8 @@ let deletedMessageIds: string[] = [];
36
36
  let updatedMessages: Array<{ id: string; content: string }> = [];
37
37
 
38
38
  mock.module("../memory/conversation-crud.js", () => ({
39
+ setConversationProcessingStartedAt: () => {},
40
+ isConversationProcessing: () => false,
39
41
  getMessages: (conversationId: string) =>
40
42
  dbMessages.filter((m) => m.conversationId === conversationId),
41
43
  deleteMessageById: (messageId: string) => {
@@ -74,6 +74,8 @@ let mockConversation: Record<string, unknown> | null = null;
74
74
  let nextMockMessageId = 1;
75
75
 
76
76
  mock.module("../memory/conversation-crud.js", () => ({
77
+ setConversationProcessingStartedAt: () => {},
78
+ isConversationProcessing: () => false,
77
79
  updateConversationContextWindow: () => {},
78
80
  deleteMessageById: () => {},
79
81
  updateConversationTitle: () => {},
@@ -70,6 +70,8 @@ let mockDbMessages: Array<{
70
70
  let mockConversation: Record<string, unknown> | null = null;
71
71
 
72
72
  mock.module("../memory/conversation-crud.js", () => ({
73
+ setConversationProcessingStartedAt: () => {},
74
+ isConversationProcessing: () => false,
73
75
  updateConversationContextWindow: () => {},
74
76
  deleteMessageById: () => {},
75
77
  updateConversationTitle: () => {},
@@ -51,6 +51,8 @@ const getConversationMock = mock((id: string) => {
51
51
  });
52
52
 
53
53
  mock.module("../memory/conversation-crud.js", () => ({
54
+ setConversationProcessingStartedAt: () => {},
55
+ isConversationProcessing: () => false,
54
56
  setConversationOriginChannelIfUnset: () => {},
55
57
  updateConversationContextWindow: () => {},
56
58
  deleteMessageById: () => {},
@@ -46,6 +46,8 @@ mock.module("../runtime/assistant-event-hub.js", () => ({
46
46
  }));
47
47
 
48
48
  mock.module("../memory/conversation-crud.js", () => ({
49
+ setConversationProcessingStartedAt: () => {},
50
+ isConversationProcessing: () => false,
49
51
  setConversationOriginChannelIfUnset: () => {},
50
52
  setConversationOriginInterfaceIfUnset: () => {},
51
53
  provenanceFromTrustContext: () => ({
@@ -157,6 +157,8 @@ let mockDbMessages: Array<{ id: string; role: string; content: string }> = [];
157
157
  let mockConversation: Record<string, unknown> | null = null;
158
158
 
159
159
  mock.module("../memory/conversation-crud.js", () => ({
160
+ setConversationProcessingStartedAt: () => {},
161
+ isConversationProcessing: () => false,
160
162
  setConversationOriginChannelIfUnset: () => {},
161
163
  setConversationOriginInterfaceIfUnset: () => {},
162
164
  updateConversationContextWindow: () => {},
@@ -98,6 +98,8 @@ mock.module("../security/secret-allowlist.js", () => ({
98
98
  }));
99
99
 
100
100
  mock.module("../memory/conversation-crud.js", () => ({
101
+ setConversationProcessingStartedAt: () => {},
102
+ isConversationProcessing: () => false,
101
103
  setConversationOriginChannelIfUnset: () => {},
102
104
  deleteMessageById: () => {},
103
105
  getMessages: () => [],
@@ -149,6 +149,8 @@ mock.module("../security/secret-allowlist.js", () => ({
149
149
  }));
150
150
 
151
151
  mock.module("../memory/conversation-crud.js", () => ({
152
+ setConversationProcessingStartedAt: () => {},
153
+ isConversationProcessing: () => false,
152
154
  setConversationOriginChannelIfUnset: () => {},
153
155
  setConversationOriginInterfaceIfUnset: () => {},
154
156
  updateConversationContextWindow: () => {},
@@ -2911,3 +2913,92 @@ describe("MessageQueue byte budget", () => {
2911
2913
  ).toBe(false);
2912
2914
  });
2913
2915
  });
2916
+
2917
+ describe("subagent notification user_message_echo suppression", () => {
2918
+ beforeEach(() => {
2919
+ pendingRuns = [];
2920
+ capturedAddMessages.length = 0;
2921
+ });
2922
+
2923
+ test("drained subagent-notification message persists and wakes the agent but emits no user_message_echo", async () => {
2924
+ const conversation = makeConversation();
2925
+ await conversation.loadFromDb();
2926
+
2927
+ const events1: ServerMessage[] = [];
2928
+ const eventsNotif: ServerMessage[] = [];
2929
+
2930
+ // Occupy the conversation so the injected notification queues.
2931
+ const p1 = conversation.processMessage({
2932
+ content: "msg-1",
2933
+ attachments: [],
2934
+ onEvent: (e) => events1.push(e),
2935
+ requestId: "req-1",
2936
+ });
2937
+ await waitForPendingRun(1);
2938
+ expect(conversation.isProcessing()).toBe(true);
2939
+
2940
+ // A daemon-injected subagent completion notification carries
2941
+ // `subagentNotification` metadata.
2942
+ conversation.enqueueMessage({
2943
+ content: '[Subagent "research" completed]',
2944
+ onEvent: (e) => eventsNotif.push(e),
2945
+ requestId: "req-notif",
2946
+ metadata: {
2947
+ subagentNotification: {
2948
+ subagentId: "sub-1",
2949
+ label: "research",
2950
+ status: "completed",
2951
+ },
2952
+ },
2953
+ });
2954
+
2955
+ // Resolving the first run drains the queued notification.
2956
+ await resolveRun(0);
2957
+ await p1;
2958
+ await waitForPendingRun(2);
2959
+
2960
+ // It is still persisted (so the orchestrator LLM sees it in the transcript)
2961
+ // and still wakes the agent (a run was created for the drained message)...
2962
+ expect(
2963
+ capturedAddMessages.some((m) => m.content.includes("Subagent")),
2964
+ ).toBe(true);
2965
+ expect(pendingRuns.length).toBe(2);
2966
+ // ...but no user_message_echo is broadcast, so the client never renders it
2967
+ // as a live user bubble.
2968
+ expect(eventsNotif.some((e) => e.type === "user_message_echo")).toBe(false);
2969
+
2970
+ await resolveRun(1);
2971
+ await new Promise((r) => setTimeout(r, 10));
2972
+ });
2973
+
2974
+ test("drained ordinary message still emits user_message_echo", async () => {
2975
+ const conversation = makeConversation();
2976
+ await conversation.loadFromDb();
2977
+
2978
+ const events1: ServerMessage[] = [];
2979
+ const eventsNormal: ServerMessage[] = [];
2980
+
2981
+ const p1 = conversation.processMessage({
2982
+ content: "msg-1",
2983
+ attachments: [],
2984
+ onEvent: (e) => events1.push(e),
2985
+ requestId: "req-1",
2986
+ });
2987
+ await waitForPendingRun(1);
2988
+
2989
+ conversation.enqueueMessage({
2990
+ content: "ordinary message",
2991
+ onEvent: (e) => eventsNormal.push(e),
2992
+ requestId: "req-normal",
2993
+ });
2994
+
2995
+ await resolveRun(0);
2996
+ await p1;
2997
+ await waitForPendingRun(2);
2998
+
2999
+ expect(eventsNormal.some((e) => e.type === "user_message_echo")).toBe(true);
3000
+
3001
+ await resolveRun(1);
3002
+ await new Promise((r) => setTimeout(r, 10));
3003
+ });
3004
+ });