@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
@@ -33,16 +33,17 @@ export interface TrustVerdictTransport {
33
33
  }
34
34
 
35
35
  /**
36
- * Build a {@link TrustContext} from a gateway verdict + transport identity.
36
+ * Reassemble an {@link ActorTrustContext} from a gateway verdict + transport
37
+ * identity (mirroring `resolveActorTrust`), without any local DB/IPC reads.
37
38
  *
38
- * Reassembles an {@link ActorTrustContext} (mirroring `resolveActorTrust`) and
39
- * routes it through {@link toTrustContext}, so the output is byte-identical to
40
- * the local resolution path.
39
+ * Pure: the voice path consumes this directly for routing on
40
+ * `actorTrust.trustClass`; {@link trustContextFromVerdict} routes it through
41
+ * {@link toTrustContext}.
41
42
  */
42
- export function trustContextFromVerdict(
43
+ export function actorTrustContextFromVerdict(
43
44
  verdict: TrustVerdict,
44
45
  input: TrustVerdictTransport,
45
- ): TrustContext {
46
+ ): ActorTrustContext {
46
47
  const canonicalSenderId = verdict.canonicalSenderId;
47
48
  const memberDisplayName = verdict.memberDisplayName;
48
49
  const senderDisplayName = input.actorDisplayName;
@@ -51,7 +52,7 @@ export function trustContextFromVerdict(
51
52
  ? `@${username}`
52
53
  : (canonicalSenderId ?? undefined);
53
54
 
54
- const actorTrustContext: ActorTrustContext = {
55
+ return {
55
56
  canonicalSenderId,
56
57
  guardianBindingMatch: verdict.guardianExternalUserId
57
58
  ? {
@@ -60,7 +61,12 @@ export function trustContextFromVerdict(
60
61
  }
61
62
  : null,
62
63
  guardianPrincipalId: verdict.guardianPrincipalId,
63
- memberRecord: null,
64
+ // Populate from the verdict so the voice path's ACL gates (which read
65
+ // actorTrust.memberRecord.channel status/policy) enforce blocked/revoked/
66
+ // deny/escalate. Null for memberless verdicts. Text path is unaffected:
67
+ // toTrustContext derives the same member fields trustContextFromVerdict
68
+ // already stamps.
69
+ memberRecord: resolvedMemberFromVerdict(verdict),
64
70
  trustClass: verdict.trustClass,
65
71
  actorMetadata: {
66
72
  identifier,
@@ -72,9 +78,21 @@ export function trustContextFromVerdict(
72
78
  trustStatus: verdict.trustClass,
73
79
  },
74
80
  };
81
+ }
75
82
 
83
+ /**
84
+ * Build a {@link TrustContext} from a gateway verdict + transport identity.
85
+ *
86
+ * Reassembles an {@link ActorTrustContext} (mirroring `resolveActorTrust`) and
87
+ * routes it through {@link toTrustContext}, so the output is byte-identical to
88
+ * the local resolution path.
89
+ */
90
+ export function trustContextFromVerdict(
91
+ verdict: TrustVerdict,
92
+ input: TrustVerdictTransport,
93
+ ): TrustContext {
76
94
  const context = toTrustContext(
77
- actorTrustContext,
95
+ actorTrustContextFromVerdict(verdict, input),
78
96
  input.conversationExternalId,
79
97
  );
80
98
 
@@ -91,6 +109,26 @@ export function trustContextFromVerdict(
91
109
  return context;
92
110
  }
93
111
 
112
+ /**
113
+ * True when the verdict carries a member identity (contactId or channelId),
114
+ * regardless of whether that member resolves to a usable {@link ResolvedMember}.
115
+ */
116
+ export function verdictHasMemberIdentity(verdict: TrustVerdict): boolean {
117
+ return !!(verdict.contactId || verdict.channelId);
118
+ }
119
+
120
+ /**
121
+ * True when the verdict claims a member identity but that member can't be
122
+ * synthesized (partial/mixed-version verdict). Such a verdict is unusable —
123
+ * callers fall back to local resolution.
124
+ */
125
+ export function verdictMemberUnresolvable(verdict: TrustVerdict): boolean {
126
+ return (
127
+ verdictHasMemberIdentity(verdict) &&
128
+ resolvedMemberFromVerdict(verdict) === null
129
+ );
130
+ }
131
+
94
132
  // Allowed ACL enum values, kept in sync with the ContactChannel union types.
95
133
  const CHANNEL_STATUS_VALUES: readonly ChannelStatus[] = [
96
134
  "active",
@@ -113,6 +151,44 @@ function isChannelPolicy(value: string): value is ChannelPolicy {
113
151
  return (CHANNEL_POLICY_VALUES as readonly string[]).includes(value);
114
152
  }
115
153
 
154
+ /**
155
+ * The ACL fields a gateway verdict carries for a resolved member, decoupled
156
+ * from the schema-derived {@link ContactChannel}.
157
+ */
158
+ export interface VerdictMember {
159
+ contactId: string;
160
+ channelId: string;
161
+ status: ChannelStatus;
162
+ policy: ChannelPolicy;
163
+ verifiedAt: number | null;
164
+ displayName: string | null;
165
+ }
166
+
167
+ /**
168
+ * Extract the narrow {@link VerdictMember} ACL view from a gateway verdict.
169
+ *
170
+ * Mirrors {@link resolvedMemberFromVerdict}'s guards (contactId/channelId
171
+ * present + known status/policy enums), failing closed to null otherwise.
172
+ */
173
+ export function verdictMemberFromVerdict(
174
+ verdict: TrustVerdict,
175
+ ): VerdictMember | null {
176
+ if (!verdict.contactId || !verdict.channelId) return null;
177
+ if (!verdict.status || !verdict.policy) return null;
178
+ if (!isChannelStatus(verdict.status) || !isChannelPolicy(verdict.policy)) {
179
+ return null;
180
+ }
181
+
182
+ return {
183
+ contactId: verdict.contactId,
184
+ channelId: verdict.channelId,
185
+ status: verdict.status,
186
+ policy: verdict.policy,
187
+ verifiedAt: verdict.verifiedAt ?? null,
188
+ displayName: verdict.memberDisplayName ?? null,
189
+ };
190
+ }
191
+
116
192
  /**
117
193
  * Build a synthetic {@link ResolvedMember} from a gateway verdict.
118
194
  *
@@ -21,7 +21,7 @@ import {
21
21
  countRecentSendsToDestination,
22
22
  createOutboundSession,
23
23
  findActiveSession,
24
- getGuardianBinding,
24
+ isGuardianBoundForChannel,
25
25
  updateSessionDelivery,
26
26
  updateSessionStatus,
27
27
  } from "./channel-verification-service.js";
@@ -224,7 +224,7 @@ export async function startOutbound(
224
224
  originConversationId,
225
225
  );
226
226
  } else if (channel === "phone") {
227
- return startOutboundVoice(
227
+ return await startOutboundVoice(
228
228
  params.destination,
229
229
  assistantId,
230
230
  channel,
@@ -232,7 +232,7 @@ export async function startOutbound(
232
232
  originConversationId,
233
233
  );
234
234
  } else if (channel === "slack") {
235
- return startOutboundSlack(
235
+ return await startOutboundSlack(
236
236
  params.destination,
237
237
  assistantId,
238
238
  channel,
@@ -240,7 +240,7 @@ export async function startOutbound(
240
240
  originConversationId,
241
241
  );
242
242
  } else if (channel === "email") {
243
- return startOutboundEmail(
243
+ return await startOutboundEmail(
244
244
  params.destination,
245
245
  assistantId,
246
246
  channel,
@@ -274,8 +274,8 @@ async function startOutboundTelegram(
274
274
  };
275
275
  }
276
276
 
277
- const existingBinding = getGuardianBinding(assistantId, channel);
278
- if (existingBinding && !rebind) {
277
+ const alreadyBound = await isGuardianBoundForChannel(channel);
278
+ if (alreadyBound && !rebind) {
279
279
  return {
280
280
  success: false,
281
281
  error: "already_bound",
@@ -394,13 +394,13 @@ async function startOutboundTelegram(
394
394
  };
395
395
  }
396
396
 
397
- function startOutboundVoice(
397
+ async function startOutboundVoice(
398
398
  rawDestination: string | undefined,
399
399
  assistantId: string,
400
400
  channel: ChannelId,
401
401
  rebind?: boolean,
402
402
  originConversationId?: string,
403
- ): OutboundActionResult {
403
+ ): Promise<OutboundActionResult> {
404
404
  if (!rawDestination) {
405
405
  return {
406
406
  success: false,
@@ -422,8 +422,8 @@ function startOutboundVoice(
422
422
  };
423
423
  }
424
424
 
425
- const existingBinding = getGuardianBinding(assistantId, channel);
426
- if (existingBinding && !rebind) {
425
+ const alreadyBound = await isGuardianBoundForChannel(channel);
426
+ if (alreadyBound && !rebind) {
427
427
  return {
428
428
  success: false,
429
429
  error: "already_bound",
@@ -591,13 +591,13 @@ export function deliverVerificationEmail(
591
591
  })();
592
592
  }
593
593
 
594
- function startOutboundSlack(
594
+ async function startOutboundSlack(
595
595
  destination: string | undefined,
596
596
  assistantId: string,
597
597
  channel: ChannelId,
598
598
  rebind?: boolean,
599
599
  originConversationId?: string,
600
- ): OutboundActionResult {
600
+ ): Promise<OutboundActionResult> {
601
601
  if (!destination) {
602
602
  return {
603
603
  success: false,
@@ -607,8 +607,8 @@ function startOutboundSlack(
607
607
  };
608
608
  }
609
609
 
610
- const existingBinding = getGuardianBinding(assistantId, channel);
611
- if (existingBinding && !rebind) {
610
+ const alreadyBound = await isGuardianBoundForChannel(channel);
611
+ if (alreadyBound && !rebind) {
612
612
  return {
613
613
  success: false,
614
614
  error: "already_bound",
@@ -669,13 +669,13 @@ function startOutboundSlack(
669
669
  };
670
670
  }
671
671
 
672
- function startOutboundEmail(
672
+ async function startOutboundEmail(
673
673
  destination: string | undefined,
674
674
  assistantId: string,
675
675
  channel: ChannelId,
676
676
  rebind?: boolean,
677
677
  originConversationId?: string,
678
- ): OutboundActionResult {
678
+ ): Promise<OutboundActionResult> {
679
679
  if (!destination) {
680
680
  return {
681
681
  success: false,
@@ -688,8 +688,8 @@ function startOutboundEmail(
688
688
 
689
689
  const normalizedEmail = destination.trim().toLowerCase();
690
690
 
691
- const existingBinding = getGuardianBinding(assistantId, channel);
692
- if (existingBinding && !rebind) {
691
+ const alreadyBound = await isGuardianBoundForChannel(channel);
692
+ if (alreadyBound && !rebind) {
693
693
  return {
694
694
  success: false,
695
695
  error: "already_bound",
@@ -15,6 +15,7 @@ import { readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
15
15
  import { join } from "node:path";
16
16
 
17
17
  import { getOrCreateConversation } from "../daemon/conversation-store.js";
18
+ import { supersedePendingInteractionsOnEnqueue } from "../daemon/handlers/conversations.js";
18
19
  import type { UserMessageAttachment } from "../daemon/message-types/shared.js";
19
20
  import {
20
21
  processMessageInBackground,
@@ -131,6 +132,21 @@ async function dispatchUserMessage(params: {
131
132
  assistantMessageInterface: resolvedInterface,
132
133
  },
133
134
  });
135
+ if (!result.rejected) {
136
+ // Mirror the HTTP send path: a follow-up enqueued while the turn is busy
137
+ // auto-denies pending confirmations and supersedes a parked ask_question
138
+ // so it isn't stranded behind the prompt until the response backstop.
139
+ // Best-effort — the message is already queued, so a cleanup failure must
140
+ // not surface as an error that makes the CLI retry and enqueue a duplicate.
141
+ try {
142
+ supersedePendingInteractionsOnEnqueue(conversationId, requestId);
143
+ } catch (err) {
144
+ log.warn(
145
+ { err, conversationId },
146
+ "Post-enqueue supersession failed — queued message unaffected",
147
+ );
148
+ }
149
+ }
134
150
  return { accepted: !result.rejected };
135
151
  }
136
152
 
@@ -553,6 +553,15 @@ export class SubagentManager {
553
553
  ),
554
554
  );
555
555
  managed.state.completedAt = Date.now();
556
+ // Capture the conversation's latest usage before emitting the terminal
557
+ // status. `subagent_status_changed` ships `state.usage`, and the abort path
558
+ // (unlike the completion/failure paths, which sync at agent-loop exit) would
559
+ // otherwise send the {0,0,0} init usage — zeroing the client's token counts
560
+ // even though those tokens were already spent. `usageStats` accrues per LLM
561
+ // turn (see conversation-usage.ts), so this is the most recent total.
562
+ if (managed.conversation) {
563
+ managed.state.usage = { ...managed.conversation.usageStats };
564
+ }
556
565
  if (parentSendToClient) {
557
566
  // Route the status update through the stored parent sender so the
558
567
  // owning conversation's UI chip updates, even when the abort comes from a
@@ -391,6 +391,38 @@ export interface SkillLoadedTelemetryEvent extends ModelTelemetryEventBase {
391
391
  conversation_id: string | null;
392
392
  }
393
393
 
394
+ /**
395
+ * Watchdog health event — one per watchdog check firing. The daemon's
396
+ * watchdog observes liveness/health signals (event-loop block, stream-idle
397
+ * stalls, restarts, ...) and emits one event per check firing.
398
+ *
399
+ * Deliberately minimal and forward-compatible, mirroring the platform
400
+ * `WatchdogTelemetryEventSerializer`:
401
+ *
402
+ * - `check_name` — which watchdog check fired. Open string set (not a
403
+ * closed enum) so the daemon can introduce a new check without a
404
+ * coordinated serializer release; it is the primary group-by dimension
405
+ * downstream. The infrastructure admin chart filters this to
406
+ * `event_loop_blocked`.
407
+ * - `value` — the single measured magnitude for the check (block ms, idle
408
+ * ms, ...). Nullable: not every check carries a scalar. The platform
409
+ * coerces ints to float, so the daemon need not distinguish.
410
+ * - `detail` — open JSON bag for any extra fields the daemon attaches
411
+ * (reason codes, secondary numbers, a human message) without a
412
+ * platform-coordinated schema change. Null when the daemon attaches
413
+ * nothing. Bounded server-side (4096 bytes serialized); an oversize bag
414
+ * rejects only the single event, never the batch.
415
+ *
416
+ * Metadata only — no conversation content. Dedupe downstream on
417
+ * `daemon_event_id` (the daemon retries a batch on transient POST failure).
418
+ */
419
+ export interface WatchdogTelemetryEvent extends TelemetryEventBase {
420
+ type: "watchdog";
421
+ check_name: string;
422
+ value: number | null;
423
+ detail: Record<string, unknown> | null;
424
+ }
425
+
394
426
  /** Discriminated union of all telemetry event types. */
395
427
  export type TelemetryEvent =
396
428
  | LlmUsageTelemetryEvent
@@ -399,4 +431,5 @@ export type TelemetryEvent =
399
431
  | OnboardingTelemetryEvent
400
432
  | AuthFallbackTelemetryEvent
401
433
  | ToolExecutedTelemetryEvent
402
- | SkillLoadedTelemetryEvent;
434
+ | SkillLoadedTelemetryEvent
435
+ | WatchdogTelemetryEvent;
@@ -1547,11 +1547,11 @@ describe("UsageTelemetryReporter", () => {
1547
1547
  // No HTTP call should have been made
1548
1548
  expect(mockFetch).not.toHaveBeenCalled();
1549
1549
 
1550
- // All 7 timestamp watermarks should have been advanced, and all 7 ID
1550
+ // All 8 timestamp watermarks should have been advanced, and all 8 ID
1551
1551
  // watermarks pinned to the high-sorting sentinel (a truthy value keeps
1552
1552
  // the compound-cursor branch active while closing its same-millisecond
1553
1553
  // arm against opt-out rows).
1554
- expect(mockSetMemoryCheckpoint).toHaveBeenCalledTimes(14);
1554
+ expect(mockSetMemoryCheckpoint).toHaveBeenCalledTimes(16);
1555
1555
 
1556
1556
  const calls = mockSetMemoryCheckpoint.mock.calls;
1557
1557
  const keys = calls.map((c) => c[0]);
@@ -1563,6 +1563,7 @@ describe("UsageTelemetryReporter", () => {
1563
1563
  "auth_fallback",
1564
1564
  "tool_executed",
1565
1565
  "skill_loaded",
1566
+ "watchdog",
1566
1567
  ];
1567
1568
  for (const eventType of eventTypes) {
1568
1569
  expect(keys).toContain(`telemetry:${eventType}:last_reported_at`);
@@ -28,6 +28,7 @@ import {
28
28
  assembleBoundedTurnTrace,
29
29
  isTurnSettled,
30
30
  } from "../memory/turn-trace-store.js";
31
+ import { queryUnreportedWatchdogEvents } from "../memory/watchdog-events-store.js";
31
32
  import { VellumPlatformClient } from "../platform/client.js";
32
33
  import {
33
34
  getCachedShareAnalytics,
@@ -76,6 +77,9 @@ const CHECKPOINT_KEY_SKILL_LOADED_WATERMARK =
76
77
  "telemetry:skill_loaded:last_reported_at";
77
78
  const CHECKPOINT_KEY_SKILL_LOADED_WATERMARK_ID =
78
79
  "telemetry:skill_loaded:last_reported_id";
80
+ const CHECKPOINT_KEY_WATCHDOG_WATERMARK = "telemetry:watchdog:last_reported_at";
81
+ const CHECKPOINT_KEY_WATCHDOG_WATERMARK_ID =
82
+ "telemetry:watchdog:last_reported_id";
79
83
  // Written into the `*_id` watermark checkpoints by the opt-out flush branch.
80
84
  // Sorts lexicographically above every real row ID (all event stores generate
81
85
  // lowercase v4 UUIDs), so the compound cursor's same-millisecond arm
@@ -100,6 +104,7 @@ const WATERMARK_KEY_PAIRS: ReadonlyArray<readonly [string, string]> = [
100
104
  CHECKPOINT_KEY_SKILL_LOADED_WATERMARK,
101
105
  CHECKPOINT_KEY_SKILL_LOADED_WATERMARK_ID,
102
106
  ],
107
+ [CHECKPOINT_KEY_WATCHDOG_WATERMARK, CHECKPOINT_KEY_WATCHDOG_WATERMARK_ID],
103
108
  ];
104
109
  const REPORT_INTERVAL_MS = 5 * 60 * 1000;
105
110
  const INITIAL_FLUSH_DELAY_MS = 30_000; // Delay first flush to let CES handshake complete
@@ -123,6 +128,37 @@ export function setUsageTelemetryReporter(
123
128
  _instance = reporter;
124
129
  }
125
130
 
131
+ // ---------------------------------------------------------------------------
132
+ // Helpers
133
+ // ---------------------------------------------------------------------------
134
+
135
+ /**
136
+ * Parse a stored `watchdog_events.detail` JSON text column into the object the
137
+ * platform expects. Returns null for a null column or an unparseable/corrupted
138
+ * blob (mirroring the turn `client` metadata parse: a bad blob emits null
139
+ * rather than failing the batch). A non-object (e.g. a bare number or string)
140
+ * also resolves to null, since the platform serializer treats `detail` as a
141
+ * JSON object bag.
142
+ */
143
+ function parseWatchdogDetail(
144
+ raw: string | null,
145
+ ): Record<string, unknown> | null {
146
+ if (!raw) return null;
147
+ try {
148
+ const parsed = JSON.parse(raw) as unknown;
149
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
150
+ return parsed as Record<string, unknown>;
151
+ }
152
+ return null;
153
+ } catch {
154
+ log.warn(
155
+ { rawDetail: raw.slice(0, 200) },
156
+ "Telemetry watchdog: failed to parse detail; emitting null",
157
+ );
158
+ return null;
159
+ }
160
+ }
161
+
126
162
  // ---------------------------------------------------------------------------
127
163
  // Reporter
128
164
  // ---------------------------------------------------------------------------
@@ -307,7 +343,8 @@ export class UsageTelemetryReporter {
307
343
  undefined;
308
344
 
309
345
  // Read skill-loaded watermark (compound cursor: createdAt + id).
310
- // Brand-new table, so the standard 0 default is safe.
346
+ // Writes are gated on share_analytics consent, so opted-out rows
347
+ // cannot exist and the standard 0 default is safe.
311
348
  const skillLoadedWatermark = Number(
312
349
  getMemoryCheckpoint(CHECKPOINT_KEY_SKILL_LOADED_WATERMARK) ?? "0",
313
350
  );
@@ -315,6 +352,15 @@ export class UsageTelemetryReporter {
315
352
  getMemoryCheckpoint(CHECKPOINT_KEY_SKILL_LOADED_WATERMARK_ID) ??
316
353
  undefined;
317
354
 
355
+ // Read watchdog watermark (compound cursor: createdAt + id).
356
+ // Writes are gated on share_analytics consent, so opted-out rows
357
+ // cannot exist and the standard 0 default is safe.
358
+ const watchdogWatermark = Number(
359
+ getMemoryCheckpoint(CHECKPOINT_KEY_WATCHDOG_WATERMARK) ?? "0",
360
+ );
361
+ const watchdogWatermarkId =
362
+ getMemoryCheckpoint(CHECKPOINT_KEY_WATCHDOG_WATERMARK_ID) ?? undefined;
363
+
318
364
  // Query unreported events
319
365
  const events = queryUnreportedUsageEvents(
320
366
  watermark,
@@ -351,6 +397,11 @@ export class UsageTelemetryReporter {
351
397
  skillLoadedWatermarkId,
352
398
  BATCH_SIZE,
353
399
  );
400
+ const watchdogEvents = queryUnreportedWatchdogEvents(
401
+ watchdogWatermark,
402
+ watchdogWatermarkId,
403
+ BATCH_SIZE,
404
+ );
354
405
 
355
406
  // Trace completeness barrier (trace-eligible owners only).
356
407
  //
@@ -418,7 +469,8 @@ export class UsageTelemetryReporter {
418
469
  onboardingEvents.length === 0 &&
419
470
  authFallbackEvents.length === 0 &&
420
471
  toolExecutedEvents.length === 0 &&
421
- skillLoadedEvents.length === 0
472
+ skillLoadedEvents.length === 0 &&
473
+ watchdogEvents.length === 0
422
474
  )
423
475
  return;
424
476
 
@@ -442,6 +494,7 @@ export class UsageTelemetryReporter {
442
494
  authFallbackCount: authFallbackEvents.length,
443
495
  toolExecutedCount: toolExecutedEvents.length,
444
496
  skillLoadedCount: skillLoadedEvents.length,
497
+ watchdogCount: watchdogEvents.length,
445
498
  },
446
499
  "Telemetry flush: resolved auth context",
447
500
  );
@@ -674,6 +727,23 @@ export class UsageTelemetryReporter {
674
727
  assistant_version: APP_VERSION,
675
728
  }),
676
729
  ),
730
+ ...watchdogEvents.map(
731
+ (e): TelemetryEvent => ({
732
+ type: "watchdog",
733
+ daemon_event_id: e.id,
734
+ recorded_at: e.createdAt,
735
+ check_name: e.checkName,
736
+ value: e.value,
737
+ // `detail` is stored as JSON text; parse defensively so a
738
+ // corrupted blob never fails the batch flush. A parse failure
739
+ // emits null rather than dropping the event.
740
+ detail: parseWatchdogDetail(e.detail),
741
+ // `watchdog_events` has no record-time version column — same
742
+ // upload-time APP_VERSION stamping as the other non-llm_usage
743
+ // event types.
744
+ assistant_version: APP_VERSION,
745
+ }),
746
+ ),
677
747
  ];
678
748
 
679
749
  const organizationId = getPlatformOrganizationId() || undefined;
@@ -796,6 +866,19 @@ export class UsageTelemetryReporter {
796
866
  );
797
867
  }
798
868
 
869
+ // Advance watchdog watermark (compound cursor)
870
+ if (watchdogEvents.length > 0) {
871
+ const lastWatchdog = watchdogEvents[watchdogEvents.length - 1];
872
+ setMemoryCheckpoint(
873
+ CHECKPOINT_KEY_WATCHDOG_WATERMARK,
874
+ String(lastWatchdog.createdAt),
875
+ );
876
+ setMemoryCheckpoint(
877
+ CHECKPOINT_KEY_WATCHDOG_WATERMARK_ID,
878
+ lastWatchdog.id,
879
+ );
880
+ }
881
+
799
882
  // If we got a full batch of any type, there may be more — recurse.
800
883
  // Turns use the REPORTED count: when the completeness barrier truncates
801
884
  // the batch, the deferred turns must wait for a later flush (by which
@@ -808,7 +891,8 @@ export class UsageTelemetryReporter {
808
891
  onboardingEvents.length === BATCH_SIZE ||
809
892
  authFallbackEvents.length === BATCH_SIZE ||
810
893
  toolExecutedEvents.length === BATCH_SIZE ||
811
- skillLoadedEvents.length === BATCH_SIZE
894
+ skillLoadedEvents.length === BATCH_SIZE ||
895
+ watchdogEvents.length === BATCH_SIZE
812
896
  ) {
813
897
  await this._doFlush(batchCount + 1);
814
898
  }
@@ -206,6 +206,35 @@ describe("AskQuestionTool.execute", () => {
206
206
  expect(result.content).toBe("Question aborted");
207
207
  });
208
208
 
209
+ test("short-circuits without prompting when no interactive user is present", async () => {
210
+ setNextResult(singleCompleted({ decision: "option", optionId: "a" }));
211
+
212
+ const result = await askQuestionTool.execute(
213
+ validInput,
214
+ makeContext({ isInteractive: false }),
215
+ );
216
+
217
+ // The prompter must never be invoked — there is no one to answer, so the
218
+ // turn proceeds with defaults instead of parking on the response backstop.
219
+ expect(calls).toHaveLength(0);
220
+ expect(result.isError).toBe(false);
221
+ expect(result.content.toLowerCase()).toContain("no interactive user");
222
+ });
223
+
224
+ test("still prompts when isInteractive is true or unset", async () => {
225
+ setNextResult(singleCompleted({ decision: "option", optionId: "a" }));
226
+
227
+ // The short-circuit keys off an explicit `false`, not a missing flag, so an
228
+ // interactive turn (or one that never set the flag) still prompts.
229
+ await askQuestionTool.execute(
230
+ validInput,
231
+ makeContext({ isInteractive: true }),
232
+ );
233
+ await askQuestionTool.execute(validInput, makeContext());
234
+
235
+ expect(calls).toHaveLength(2);
236
+ });
237
+
209
238
  test("rejects a question with fewer than 2 options", async () => {
210
239
  setNextResult(singleCompleted({ decision: "option", optionId: "a" }));
211
240
  const result = await askQuestionTool.execute(
@@ -182,6 +182,19 @@ export const askQuestionTool = {
182
182
 
183
183
  const questions: SingleQuestion[] = parsed.data.questions;
184
184
 
185
+ // No interactive user is present to answer (scheduled/headless/background
186
+ // turn). Don't park the turn on a prompt no one can resolve — proceed with
187
+ // defaults immediately. Non-interactive turns are already instructed not to
188
+ // ask (NON_INTERACTIVE_CONTEXT_BLOCK); this is the backstop for when the
189
+ // model asks anyway, so it doesn't wait out the full response timeout.
190
+ if (context.isInteractive === false) {
191
+ return {
192
+ content:
193
+ "No interactive user is present to answer; proceeding with reasonable defaults.",
194
+ isError: false,
195
+ };
196
+ }
197
+
185
198
  const prompter = new QuestionPrompter();
186
199
  const result = await prompter.prompt({
187
200
  conversationId: context.conversationId,
@@ -437,7 +437,7 @@ export const computerUseRespondTool = {
437
437
  // observe
438
438
  // ---------------------------------------------------------------------------
439
439
 
440
- const computerUseObserveTool = {
440
+ export const computerUseObserveTool = {
441
441
  name: "computer_use_observe",
442
442
  description:
443
443
  "Capture the current screen state. Returns the accessibility tree with [ID] element references and optionally a screenshot.\n\nThe accessibility tree shows interactive elements like [3] AXButton 'Save' or [17] AXTextField 'Search'. Use element_id to target these elements in subsequent actions - this is much more reliable than pixel coordinates.\n\nCall this before your first computer use action, or to check screen state without acting.",
@@ -447,7 +447,13 @@ const computerUseObserveTool = {
447
447
 
448
448
  input_schema: {
449
449
  type: "object",
450
- properties: {},
450
+ properties: {
451
+ target_client_id: {
452
+ type: "string",
453
+ description:
454
+ "ID of the specific client to target. Required when multiple clients support host_cu; omit when only one is connected. Obtain IDs from `assistant clients list --capability host_cu`.",
455
+ },
456
+ },
451
457
  required: [],
452
458
  },
453
459
 
@@ -528,8 +528,8 @@ export { isSideEffectTool } from "./side-effects.js";
528
528
  * handles cleanup before the executor wrapper trips.
529
529
  *
530
530
  * `ask_question` blocks on user input inside `execute()` via `QuestionPrompter`,
531
- * which waits up to `permissionTimeoutSec`. We give the wrapper the same 5s
532
- * buffer over that deadline so the prompter's own timeout fires first and
531
+ * which waits up to `questionResponseTimeoutSec`. We give the wrapper the same
532
+ * 5s buffer over that deadline so the prompter's own timeout fires first and
533
533
  * returns its clean "User did not respond within timeout" result — otherwise
534
534
  * the shorter generic budget trips first, orphaning the still-pending prompt
535
535
  * behind the confusing "may still be running in the background" error.
@@ -556,8 +556,8 @@ export function computePerToolTimeoutMs(
556
556
  return (shellTimeoutSec + 5) * 1000;
557
557
  }
558
558
  if (name === "ask_question") {
559
- const { permissionTimeoutSec } = getConfig().timeouts;
560
- return (permissionTimeoutSec + 5) * 1000;
559
+ const { questionResponseTimeoutSec } = getConfig().timeouts;
560
+ return (questionResponseTimeoutSec + 5) * 1000;
561
561
  }
562
562
  const rawTimeoutSec = getConfig().timeouts.toolExecutionTimeoutSec;
563
563
  return safeTimeoutMs(rawTimeoutSec);