@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
@@ -272,7 +272,6 @@ describe("WhatsApp channel ingress attachment resolution", () => {
272
272
  const trustVerdict = resolveLocalTrustVerdict({
273
273
  channelType: "whatsapp",
274
274
  actorExternalId: WHATSAPP_USER_ID,
275
- externalChatId: "whatsapp-chat-1",
276
275
  });
277
276
  return {
278
277
  sourceChannel: "whatsapp",
@@ -96,6 +96,8 @@ const addMessageMock = mock(
96
96
  );
97
97
 
98
98
  mock.module("../memory/conversation-crud.js", () => ({
99
+ setConversationProcessingStartedAt: () => {},
100
+ isConversationProcessing: () => false,
99
101
  addMessage: (
100
102
  conversationId: string,
101
103
  role: string,
@@ -127,6 +129,18 @@ mock.module("../runtime/trust-context-resolver.js", () => ({
127
129
  }),
128
130
  }));
129
131
 
132
+ mock.module("../contacts/guardian-delivery-reader.js", () => ({
133
+ getGuardianDelivery: async () => [
134
+ {
135
+ channelType: "vellum",
136
+ contactId: "guardian-contact",
137
+ principalId: "test-user",
138
+ address: "test-user",
139
+ status: "active",
140
+ },
141
+ ],
142
+ }));
143
+
130
144
  mock.module("../runtime/guardian-reply-router.js", () => ({
131
145
  routeGuardianReply: async () => ({
132
146
  consumed: false,
@@ -109,17 +109,38 @@ mock.module("../daemon/approval-generators.js", () => ({
109
109
  createApprovalConversationGenerator: () => _approvalGenerator,
110
110
  }));
111
111
 
112
- // Mock local-actor-identity to return a stable guardian context that uses
113
- // the same principal as the canonical requests created in tests.
112
+ // Dev-bypass resolves the real guardian principal, then runs the real
113
+ // local-principal trust mapper against the gateway delivery read.
114
114
  mock.module("../runtime/local-actor-identity.js", () => ({
115
- resolveLocalTrustContext: () => ({
116
- sourceChannel: "vellum",
117
- trustClass: "guardian",
118
- guardianPrincipalId: "test-principal-id",
119
- guardianExternalUserId: "test-principal-id",
120
- }),
115
+ findLocalGuardianPrincipalId: async () => "test-principal-id",
116
+ }));
117
+
118
+ // Mock the IPC transport rather than local-principal-trust.js so the sibling
119
+ // resolver unit test (which mocks guardian-delivery-reader, not ipcCall) isn't
120
+ // shadowed when both files run in one Bun process. resolve_guardian_delivery
121
+ // returns a single active vellum guardian whose principal matches the
122
+ // dev-bypass-resolved id; any other method throws so unexpected IPC surfaces.
123
+ mock.module("../ipc/gateway-client.js", () => ({
124
+ ipcCall: async (method: string) => {
125
+ if (method === "resolve_guardian_delivery") {
126
+ return {
127
+ guardians: [
128
+ {
129
+ channelType: "vellum",
130
+ contactId: "test-contact-id",
131
+ principalId: "test-principal-id",
132
+ address: "test-principal-id",
133
+ externalChatId: "test-principal-id",
134
+ status: "active",
135
+ },
136
+ ],
137
+ };
138
+ }
139
+ throw new Error(`Unexpected ipcCall in test: ${method}`);
140
+ },
121
141
  }));
122
142
 
143
+ import { __resetGuardianDeliveryCacheForTest } from "../contacts/guardian-delivery-reader.js";
123
144
  import { getDb } from "../memory/db-connection.js";
124
145
  import { initializeDb } from "../memory/db-init.js";
125
146
  import type { AssistantEvent } from "../runtime/assistant-event.js";
@@ -343,6 +364,7 @@ describe("POST /v1/messages — queue-if-busy and hub publishing", () => {
343
364
  db.run("DELETE FROM contact_channels");
344
365
  db.run("DELETE FROM contacts");
345
366
  pendingInteractions.clear();
367
+ __resetGuardianDeliveryCacheForTest();
346
368
 
347
369
  createGuardianBinding({
348
370
  channel: "vellum",
@@ -351,6 +351,50 @@ describe("plugin-resident skills", () => {
351
351
  expect(skill).toBeUndefined();
352
352
  });
353
353
 
354
+ test("warns when a plugin directory is missing package.json", () => {
355
+ const warnings: unknown[][] = [];
356
+ const originalWarn = noopLogger.warn;
357
+ noopLogger.warn = (...args: unknown[]) => {
358
+ warnings.push(args);
359
+ };
360
+ try {
361
+ writePluginSkill(
362
+ "the-force",
363
+ "software-engineering",
364
+ "Software Engineering",
365
+ "Engineering workflow",
366
+ "body",
367
+ { withPackageJson: false },
368
+ );
369
+
370
+ const skill = loadSkillCatalog().find(
371
+ (s) => s.id === "software-engineering",
372
+ );
373
+ expect(skill).toBeUndefined();
374
+
375
+ const warnedForDir = warnings.some(
376
+ (args) =>
377
+ typeof args[0] === "object" &&
378
+ args[0] !== null &&
379
+ "pluginDir" in args[0] &&
380
+ (args[0] as { pluginDir: string }).pluginDir.endsWith("the-force") &&
381
+ typeof args[1] === "string" &&
382
+ args[1].includes("missing package.json"),
383
+ );
384
+ expect(warnedForDir).toBe(true);
385
+ } finally {
386
+ noopLogger.warn = originalWarn;
387
+ }
388
+ });
389
+
390
+ test("does not load resident skills from a plugin disabled via .disabled", () => {
391
+ writePluginSkill("caveman", "caveman", "Caveman", "Terse mode");
392
+ writeFileSync(join(pluginsDir, "caveman", ".disabled"), "");
393
+
394
+ const skill = loadSkillCatalog().find((s) => s.id === "caveman");
395
+ expect(skill).toBeUndefined();
396
+ });
397
+
354
398
  test("workspace skill overrides a plugin-resident skill with the same id", () => {
355
399
  const WORKSPACE_DIR = join(
356
400
  tmpdir(),
@@ -55,6 +55,35 @@ mock.module("../runtime/gateway-client.js", () => ({
55
55
  },
56
56
  }));
57
57
 
58
+ // Guardian identity resolves via the gateway delivery reader, not the local
59
+ // contacts DB. Seed it to mirror the createGuardianBinding calls below.
60
+ interface GatewayGuardian {
61
+ channelType: string;
62
+ contactId: string;
63
+ principalId?: string | null;
64
+ displayName?: string | null;
65
+ address: string;
66
+ externalChatId?: string | null;
67
+ status: string;
68
+ verifiedAt?: number | null;
69
+ }
70
+ let gatewayGuardians: GatewayGuardian[] = [];
71
+ mock.module("../contacts/guardian-delivery-reader.js", () => ({
72
+ getGuardianDelivery: async () => gatewayGuardians,
73
+ guardianForChannel: (list: GatewayGuardian[], channelType: string) =>
74
+ list.find((g) => g.channelType === channelType && g.status === "active"),
75
+ }));
76
+
77
+ function seedGatewayGuardian(
78
+ g: Partial<GatewayGuardian> & { channelType: string; address: string },
79
+ ): void {
80
+ gatewayGuardians.push({
81
+ contactId: `c-${g.channelType}`,
82
+ status: "active",
83
+ ...g,
84
+ });
85
+ }
86
+
58
87
  import { getDb } from "../memory/db-connection.js";
59
88
  import { initializeDb } from "../memory/db-init.js";
60
89
  import { findActiveSession } from "../runtime/channel-verification-service.js";
@@ -80,7 +109,16 @@ function resetState(): void {
80
109
  db.run("DELETE FROM canonical_guardian_deliveries");
81
110
  db.run("DELETE FROM contact_channels");
82
111
  db.run("DELETE FROM contacts");
83
- // Seed the vellum guardian binding (gateway does this at startup in production)
112
+ gatewayGuardians = [];
113
+ // Seed the vellum guardian binding (gateway does this at startup in
114
+ // production). The gateway list is the source of truth for guardian
115
+ // resolution; the DB write mirrors it for any local INFO reads.
116
+ seedGatewayGuardian({
117
+ channelType: "vellum",
118
+ address: "guardian-principal",
119
+ principalId: "guardian-principal",
120
+ displayName: "guardian-principal",
121
+ });
84
122
  createGuardianBinding({
85
123
  channel: "vellum",
86
124
  guardianExternalUserId: "guardian-principal",
@@ -162,7 +200,14 @@ describe("Slack inbound trusted contact verification", () => {
162
200
  });
163
201
 
164
202
  test("guardian is notified of the access attempt alongside verification", async () => {
165
- // Set up a guardian binding so the notification can target it
203
+ // Set up a guardian binding so the notification can target it. The gateway
204
+ // list resolves guardian identity; the DB write mirrors it.
205
+ seedGatewayGuardian({
206
+ channelType: "slack",
207
+ address: "U_GUARDIAN",
208
+ externalChatId: "D_GUARDIAN_DM",
209
+ principalId: "guardian-principal",
210
+ });
166
211
  createGuardianBinding({
167
212
  channel: "slack",
168
213
  guardianExternalUserId: "U_GUARDIAN",
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Regression: the SSE subscribe path must resolve the actor principal from the
3
+ * SAME guardian source as the send/result routes.
4
+ *
5
+ * The send/result routes resolve the actor principal via the async,
6
+ * gateway-first `findLocalGuardianPrincipalId`. The SSE eager-subscribe path
7
+ * cannot await and uses the sync `findLocalGuardianPrincipalIdFromStore`. When
8
+ * the gateway binding is canonical but the local contact row is stale/missing
9
+ * (after a guardian reset or gateway-owned binding update), the sync path must
10
+ * still land on the gateway principal — otherwise the event hub registers the
11
+ * SSE client under a DIFFERENT principal than the turn/result paths use, and
12
+ * targeted result submissions 403.
13
+ *
14
+ * These tests pin the invariant by priming the gateway-delivery cache with a
15
+ * principal that differs from the stale local store and asserting both
16
+ * resolvers agree; and that a cold cache falls back to the local store.
17
+ */
18
+
19
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
20
+
21
+ import type { GuardianDelivery } from "@vellumai/gateway-client";
22
+
23
+ // ── Controllable IPC mock (drives the gateway-delivery cache) ────────────────
24
+
25
+ let ipcGuardians: GuardianDelivery[] = [];
26
+
27
+ mock.module("../ipc/gateway-client.js", () => ({
28
+ ipcCall: async () => ({ guardians: ipcGuardians }),
29
+ ipcCallPersistent: async () => undefined,
30
+ resetPersistentClient: () => {},
31
+ }));
32
+
33
+ // ── Local store mock (the stale fallback source) ─────────────────────────────
34
+
35
+ let storePrincipalId: string | undefined;
36
+
37
+ mock.module("../contacts/contact-store.js", () => ({
38
+ findGuardianForChannel: (channelType: string) =>
39
+ storePrincipalId && channelType === "vellum"
40
+ ? { contact: { principalId: storePrincipalId }, channel: {} }
41
+ : null,
42
+ }));
43
+
44
+ import {
45
+ __resetGuardianDeliveryCacheForTest,
46
+ getGuardianDelivery,
47
+ } from "../contacts/guardian-delivery-reader.js";
48
+ import {
49
+ findLocalGuardianPrincipalId,
50
+ findLocalGuardianPrincipalIdFromStore,
51
+ } from "../runtime/local-actor-identity.js";
52
+
53
+ const gatewayVellumGuardian: GuardianDelivery = {
54
+ channelType: "vellum",
55
+ contactId: "contact-gw",
56
+ principalId: "guardian-from-gateway",
57
+ address: "vellum:self",
58
+ status: "active",
59
+ };
60
+
61
+ describe("SSE actor principal resolves from the same guardian source as send/result routes", () => {
62
+ beforeEach(() => {
63
+ __resetGuardianDeliveryCacheForTest();
64
+ ipcGuardians = [];
65
+ storePrincipalId = undefined;
66
+ });
67
+
68
+ test("warm gateway cache: sync (SSE) and async (send/result) resolve the SAME principal despite a stale local store", async () => {
69
+ // Gateway binding is canonical; local store row is stale (different id).
70
+ ipcGuardians = [gatewayVellumGuardian];
71
+ storePrincipalId = "guardian-stale-local";
72
+
73
+ // Prime the cache the way the async hot paths do.
74
+ const asyncPrincipalId = await findLocalGuardianPrincipalId();
75
+ expect(asyncPrincipalId).toBe("guardian-from-gateway");
76
+
77
+ // SSE's sync resolver reads the same cached gateway snapshot, NOT the
78
+ // stale store — so the principals match and host-proxy targeting works.
79
+ expect(findLocalGuardianPrincipalIdFromStore()).toBe(asyncPrincipalId);
80
+ });
81
+
82
+ test("cold cache: sync resolver falls back to the local store as before", () => {
83
+ // Nothing primed the cache; only the local store has a binding.
84
+ storePrincipalId = "guardian-stale-local";
85
+
86
+ expect(findLocalGuardianPrincipalIdFromStore()).toBe("guardian-stale-local");
87
+ });
88
+
89
+ test("cold cache with no store binding: sync resolver returns undefined", () => {
90
+ expect(findLocalGuardianPrincipalIdFromStore()).toBeUndefined();
91
+ });
92
+
93
+ test("warm gateway cache primes via the vellum-filtered read the async path uses", async () => {
94
+ ipcGuardians = [gatewayVellumGuardian];
95
+
96
+ // The async path filters by channelType vellum; the sync peek must read
97
+ // the same cache key, not the unfiltered "ALL" entry.
98
+ await getGuardianDelivery({ channelTypes: ["vellum"] });
99
+
100
+ expect(findLocalGuardianPrincipalIdFromStore()).toBe("guardian-from-gateway");
101
+ });
102
+ });
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Enqueue-steer contract: when a chat message is enqueued while an
3
+ * `ask_question` prompt is open for the same conversation, the user has chosen
4
+ * to move on rather than answer it. `steerOnEnqueuedMessageIfQuestionParked`
5
+ * steers to that message — aborting the parked turn (which settles the open
6
+ * question via its turn-abort signal) and draining the message — instead of
7
+ * stranding it behind a prompt no one will answer.
8
+ *
9
+ * Only `kind: "question"` interactions trigger the steer; pending confirmations
10
+ * are handled separately by the enqueue path's auto-deny.
11
+ */
12
+ import { afterEach, describe, expect, test } from "bun:test";
13
+
14
+ import type { Conversation } from "../daemon/conversation.js";
15
+ import {
16
+ deleteConversation,
17
+ setConversation,
18
+ } from "../daemon/conversation-registry.js";
19
+ import {
20
+ steerOnEnqueuedMessageIfQuestionParked,
21
+ supersedePendingInteractionsOnEnqueue,
22
+ } from "../daemon/handlers/conversations.js";
23
+ import * as pendingInteractions from "../runtime/pending-interactions.js";
24
+
25
+ interface ParkedTurn {
26
+ abortCount: () => number;
27
+ fake: { pendingSteerRepair: boolean };
28
+ }
29
+
30
+ /**
31
+ * Register a fake conversation whose in-flight turn is parked. The fake exposes
32
+ * just the surface `steerToMessage` touches: a processing flag, a queue whose
33
+ * head can be promoted, an abort controller that records aborts, and the
34
+ * confirmation-deny hook.
35
+ */
36
+ function registerParkedTurn(id: string): ParkedTurn {
37
+ let abortCount = 0;
38
+ const fake = {
39
+ isProcessing: () => true,
40
+ queue: {
41
+ promoteToHead: (requestId: string) => ({ requestId }),
42
+ },
43
+ pendingSteerRepair: false,
44
+ abortController: {
45
+ abort: () => {
46
+ abortCount += 1;
47
+ },
48
+ },
49
+ hasAnyPendingConfirmation: () => false,
50
+ denyAllPendingConfirmations: () => {},
51
+ };
52
+ setConversation(id, fake as unknown as Conversation);
53
+ return { abortCount: () => abortCount, fake };
54
+ }
55
+
56
+ const registeredRequestIds: string[] = [];
57
+ function registerInteraction(
58
+ conversationId: string,
59
+ kind: "question" | "confirmation",
60
+ ): void {
61
+ const requestId = `pending-${kind}-${conversationId}`;
62
+ pendingInteractions.register(requestId, { conversationId, kind });
63
+ registeredRequestIds.push(requestId);
64
+ }
65
+
66
+ const QUESTION_CONV = "steer-enqueue-question";
67
+ const CONFIRMATION_CONV = "steer-enqueue-confirmation";
68
+ const NONE_CONV = "steer-enqueue-none";
69
+
70
+ describe("steerOnEnqueuedMessageIfQuestionParked", () => {
71
+ afterEach(() => {
72
+ for (const id of registeredRequestIds) {
73
+ pendingInteractions.resolve(id, "cancelled");
74
+ }
75
+ registeredRequestIds.length = 0;
76
+ deleteConversation(QUESTION_CONV);
77
+ deleteConversation(CONFIRMATION_CONV);
78
+ deleteConversation(NONE_CONV);
79
+ });
80
+
81
+ test("steers to the enqueued message when an ask_question is parked", () => {
82
+ const conv = registerParkedTurn(QUESTION_CONV);
83
+ registerInteraction(QUESTION_CONV, "question");
84
+
85
+ const steered = steerOnEnqueuedMessageIfQuestionParked(
86
+ QUESTION_CONV,
87
+ "msg-1",
88
+ );
89
+
90
+ // The parked turn is aborted (which settles the open question) and marked
91
+ // for tool-result repair so the drain path can pick up the new message.
92
+ expect(steered).toBe(true);
93
+ expect(conv.abortCount()).toBe(1);
94
+ expect(conv.fake.pendingSteerRepair).toBe(true);
95
+ });
96
+
97
+ test("steers for a parked question even when a confirmation is also pending", () => {
98
+ // A single model response can open an ask_question and a confirmation
99
+ // concurrently (tools run via Promise.all), so both interactions can be
100
+ // registered at once. The steer must still fire for the question — the
101
+ // enqueue path runs it before the confirmation auto-deny clears entries.
102
+ const conv = registerParkedTurn(QUESTION_CONV);
103
+ registerInteraction(QUESTION_CONV, "confirmation");
104
+ registerInteraction(QUESTION_CONV, "question");
105
+
106
+ const steered = steerOnEnqueuedMessageIfQuestionParked(
107
+ QUESTION_CONV,
108
+ "msg-1",
109
+ );
110
+
111
+ expect(steered).toBe(true);
112
+ expect(conv.abortCount()).toBe(1);
113
+ });
114
+
115
+ test("does not steer for a pending confirmation (not a question)", () => {
116
+ const conv = registerParkedTurn(CONFIRMATION_CONV);
117
+ registerInteraction(CONFIRMATION_CONV, "confirmation");
118
+
119
+ const steered = steerOnEnqueuedMessageIfQuestionParked(
120
+ CONFIRMATION_CONV,
121
+ "msg-1",
122
+ );
123
+
124
+ expect(steered).toBe(false);
125
+ expect(conv.abortCount()).toBe(0);
126
+ });
127
+
128
+ test("does not steer when no prompt is parked", () => {
129
+ const conv = registerParkedTurn(NONE_CONV);
130
+
131
+ const steered = steerOnEnqueuedMessageIfQuestionParked(NONE_CONV, "msg-1");
132
+
133
+ expect(steered).toBe(false);
134
+ expect(conv.abortCount()).toBe(0);
135
+ });
136
+
137
+ test("supersedePendingInteractionsOnEnqueue steers a parked question", () => {
138
+ // The centralized routine is invoked by both the HTTP send handler and the
139
+ // CLI signal path. With no pending confirmation it steers a parked question
140
+ // — the behavior the CLI signal path previously lacked.
141
+ const conv = registerParkedTurn(QUESTION_CONV);
142
+ registerInteraction(QUESTION_CONV, "question");
143
+
144
+ supersedePendingInteractionsOnEnqueue(QUESTION_CONV, "msg-1");
145
+
146
+ expect(conv.abortCount()).toBe(1);
147
+ expect(conv.fake.pendingSteerRepair).toBe(true);
148
+ });
149
+ });
150
+
151
+ describe("removeByConversation preserves question interactions", () => {
152
+ // The enqueue path's confirmation auto-deny calls removeByConversation before
153
+ // the steer runs. Questions must survive it — they are settled instead by the
154
+ // steer's turn abort — or, when an ask_question and a confirmation are pending
155
+ // concurrently, the queued message would strand behind a question whose entry
156
+ // was cleared (and whose Promise was never settled) before the steer fired.
157
+ const CONV = "remove-by-conv-preserve-question";
158
+ const ids: string[] = [];
159
+ function register(kind: "question" | "confirmation"): void {
160
+ const requestId = `rbc-${kind}`;
161
+ pendingInteractions.register(requestId, { conversationId: CONV, kind });
162
+ ids.push(requestId);
163
+ }
164
+
165
+ afterEach(() => {
166
+ for (const id of ids) pendingInteractions.resolve(id, "cancelled");
167
+ ids.length = 0;
168
+ });
169
+
170
+ test("removes confirmations but leaves questions registered", () => {
171
+ register("confirmation");
172
+ register("question");
173
+
174
+ pendingInteractions.removeByConversation(CONV);
175
+
176
+ const remaining = pendingInteractions
177
+ .getByConversation(CONV)
178
+ .map((interaction) => interaction.kind);
179
+ expect(remaining).toEqual(["question"]);
180
+ });
181
+ });
@@ -13,6 +13,15 @@ let mockRecentContacts: ContactWithChannels[] = [];
13
13
  let mockFindContactByAddressThrows = false;
14
14
  let mockListContactsThrows = false;
15
15
 
16
+ // Guardian deliveries returned by the mocked gateway reader. resolveGuardianName
17
+ // is mocked to echo mockGuardianName, so the displayName here is captured below.
18
+ let mockGuardianDelivery: Array<{
19
+ channelType: string;
20
+ status: string;
21
+ displayName: string | null;
22
+ }> | null = null;
23
+ let lastGuardianDisplayNameSeen: string | null | undefined;
24
+
16
25
  const logWarnFn = mock(() => {});
17
26
 
18
27
  // ---------------------------------------------------------------------------
@@ -25,7 +34,10 @@ mock.module("../daemon/identity-helpers.js", () => ({
25
34
 
26
35
  mock.module("../prompts/user-reference.js", () => ({
27
36
  DEFAULT_USER_REFERENCE: "my human",
28
- resolveGuardianName: () => mockGuardianName,
37
+ resolveGuardianName: (displayName?: string | null) => {
38
+ lastGuardianDisplayNameSeen = displayName;
39
+ return mockGuardianName;
40
+ },
29
41
  }));
30
42
 
31
43
  mock.module("../contacts/contact-store.js", () => ({
@@ -35,8 +47,6 @@ mock.module("../contacts/contact-store.js", () => ({
35
47
  }
36
48
  return mockTargetContact;
37
49
  },
38
- findGuardianForChannel: () => null,
39
- listGuardianChannels: () => null,
40
50
  listContacts: (_limit?: number) => {
41
51
  if (mockListContactsThrows) {
42
52
  throw new Error("DB error: listContacts");
@@ -45,6 +55,15 @@ mock.module("../contacts/contact-store.js", () => ({
45
55
  },
46
56
  }));
47
57
 
58
+ mock.module("../contacts/guardian-delivery-reader.js", () => ({
59
+ getGuardianDelivery: async () => mockGuardianDelivery,
60
+ guardianForChannel: (
61
+ list: Array<{ channelType: string; status: string }>,
62
+ channelType: string,
63
+ ) => list.find((g) => g.channelType === channelType && g.status === "active"),
64
+ anyGuardian: (list: unknown[]) => list[0],
65
+ }));
66
+
48
67
  // Bun's mock.module for "../util/logger.js" doesn't intercept the transitive
49
68
  // import in stt-hints.ts due to a Bun limitation. Mocking pino at the package
50
69
  // level works because getLogger uses a Proxy that lazily creates a pino child
@@ -319,10 +338,22 @@ describe("resolveCallHints", () => {
319
338
  mockRecentContacts = [];
320
339
  mockFindContactByAddressThrows = false;
321
340
  mockListContactsThrows = false;
341
+ mockGuardianDelivery = null;
342
+ lastGuardianDisplayNameSeen = undefined;
322
343
  logWarnFn.mockClear();
323
344
  });
324
345
 
325
- test("happy path wires all sources correctly", () => {
346
+ test("guardian displayName for hints comes from the gateway binding", async () => {
347
+ mockGuardianDelivery = [
348
+ { channelType: "phone", status: "active", displayName: "GatewayGuardian" },
349
+ ];
350
+
351
+ await resolveCallHints(null, []);
352
+
353
+ expect(lastGuardianDisplayNameSeen).toBe("GatewayGuardian");
354
+ });
355
+
356
+ test("happy path wires all sources correctly", async () => {
326
357
  mockTargetContact = makeContact("Alice");
327
358
  mockRecentContacts = [makeContact("Bob"), makeContact("Charlie")];
328
359
 
@@ -335,7 +366,7 @@ describe("resolveCallHints", () => {
335
366
  inviteGuardianName: "Eve",
336
367
  };
337
368
 
338
- const result = resolveCallHints(session, ["StaticHint"]);
369
+ const result = await resolveCallHints(session, ["StaticHint"]);
339
370
  const parts = result.split(",");
340
371
 
341
372
  expect(parts).toContain("StaticHint");
@@ -351,7 +382,7 @@ describe("resolveCallHints", () => {
351
382
  expect(logWarnFn).not.toHaveBeenCalled();
352
383
  });
353
384
 
354
- test("findContactByAddress failure is caught and logged without throwing", () => {
385
+ test("findContactByAddress failure is caught and logged without throwing", async () => {
355
386
  mockFindContactByAddressThrows = true;
356
387
  mockRecentContacts = [makeContact("Bob")];
357
388
 
@@ -365,7 +396,7 @@ describe("resolveCallHints", () => {
365
396
  };
366
397
 
367
398
  // Should not throw
368
- const result = resolveCallHints(session, []);
399
+ const result = await resolveCallHints(session, []);
369
400
  const parts = result.split(",");
370
401
 
371
402
  // Target contact should be absent (lookup failed)
@@ -376,7 +407,7 @@ describe("resolveCallHints", () => {
376
407
  expect(logWarnFn).toHaveBeenCalled();
377
408
  });
378
409
 
379
- test("listContacts failure is caught and logged without throwing", () => {
410
+ test("listContacts failure is caught and logged without throwing", async () => {
380
411
  mockListContactsThrows = true;
381
412
  mockTargetContact = makeContact("Alice");
382
413
 
@@ -390,7 +421,7 @@ describe("resolveCallHints", () => {
390
421
  };
391
422
 
392
423
  // Should not throw
393
- const result = resolveCallHints(session, []);
424
+ const result = await resolveCallHints(session, []);
394
425
  const parts = result.split(",");
395
426
 
396
427
  // Recent contacts should be absent (listing failed)
@@ -401,7 +432,7 @@ describe("resolveCallHints", () => {
401
432
  expect(logWarnFn).toHaveBeenCalled();
402
433
  });
403
434
 
404
- test("inbound call resolves caller contact from fromNumber", () => {
435
+ test("inbound call resolves caller contact from fromNumber", async () => {
405
436
  mockTargetContact = makeContact("Alice");
406
437
  mockRecentContacts = [makeContact("Bob")];
407
438
 
@@ -414,7 +445,7 @@ describe("resolveCallHints", () => {
414
445
  inviteGuardianName: null,
415
446
  };
416
447
 
417
- const result = resolveCallHints(session, []);
448
+ const result = await resolveCallHints(session, []);
418
449
  const parts = result.split(",");
419
450
 
420
451
  // For inbound, the contact found via fromNumber should appear as caller, not target
@@ -425,10 +456,10 @@ describe("resolveCallHints", () => {
425
456
  expect(logWarnFn).not.toHaveBeenCalled();
426
457
  });
427
458
 
428
- test("null session produces hints from assistant name, guardian name, and recent contacts", () => {
459
+ test("null session produces hints from assistant name, guardian name, and recent contacts", async () => {
429
460
  mockRecentContacts = [makeContact("RecentOne"), makeContact("RecentTwo")];
430
461
 
431
- const result = resolveCallHints(null, ["Static"]);
462
+ const result = await resolveCallHints(null, ["Static"]);
432
463
  const parts = result.split(",");
433
464
 
434
465
  expect(parts).toContain("Static");
@@ -130,6 +130,33 @@ describe("parseSubagentMessages", () => {
130
130
  expect(result.objective).toBe("Research vampire lore");
131
131
  });
132
132
 
133
+ test("emits toolUseId and raw input on tool_use, toolUseId on tool_result", () => {
134
+ const messages = [
135
+ msg("user", [{ type: "text", text: "Do something" }]),
136
+ msg("assistant", [
137
+ {
138
+ type: "tool_use",
139
+ id: "t-abc",
140
+ name: "bash",
141
+ input: { command: "ls -la" },
142
+ },
143
+ ]),
144
+ msg("user", [
145
+ { type: "tool_result", tool_use_id: "t-abc", content: "total 0" },
146
+ ]),
147
+ ];
148
+
149
+ const result = parseSubagentMessages("sub-1", messages);
150
+ const toolUse = result.events.find((e) => e.type === "tool_use");
151
+ expect(toolUse).toBeDefined();
152
+ expect(toolUse!.toolUseId).toBe("t-abc");
153
+ expect(toolUse!.input).toEqual({ command: "ls -la" });
154
+
155
+ const toolResult = result.events.find((e) => e.type === "tool_result");
156
+ expect(toolResult).toBeDefined();
157
+ expect(toolResult!.toolUseId).toBe("t-abc");
158
+ });
159
+
133
160
  test("includes messageId on text events from assistant messages", () => {
134
161
  const messages = [
135
162
  msg("user", [{ type: "text", text: "Do something" }]),