@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
@@ -16,8 +16,11 @@
16
16
  //
17
17
  // Skill entries are kept in a small in-process cache so the render path can
18
18
  // fetch a `SkillEntry` synchronously by id without round-tripping to Qdrant.
19
- // The cache is replaced atomically at the end of a successful seed run; on
20
- // error the prior cache stays intact (skills are best-effort).
19
+ // The cache is replaced atomically from the local catalog at the end of a seed
20
+ // run — including when the dense embedding backend is unavailable, in which
21
+ // case only the dense Qdrant write is deferred so the cache (and the v3 needle
22
+ // lane it feeds) still reflects the current skills. An unexpected error in
23
+ // another step leaves the prior cache intact (skills are best-effort).
21
24
 
22
25
  import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
23
26
  import { getConfig } from "../../config/loader.js";
@@ -228,26 +231,26 @@ async function runSeedV2SkillEntries(generation: number): Promise<void> {
228
231
  );
229
232
  }
230
233
 
231
- // Embed all content strings in one batched call when there is anything to
232
- // embed. Skipping the call when `seeds` is empty avoids throwing on an
233
- // unavailable embedding backend in the all-disabled case, so pruning and
234
- // cache replacement still run and clear stale state.
234
+ // Build the dense + sparse vectors for the Qdrant write. Sparse (BM25/TF)
235
+ // encoding is computed locally and needs no backend; only the dense vectors
236
+ // require `embedWithBackend`, which is unconfigured during the cold-start
237
+ // window before a managed-proxy embedding credential is provisioned.
238
+ //
239
+ // A dense-embed failure is non-fatal to the in-memory cache: the v3 needle
240
+ // finder lane and always-candidate skill pinning read skills from `entries`
241
+ // / the page index, NOT from Qdrant, so the cache is populated from the
242
+ // local catalog regardless of backend state and skills stay discoverable
243
+ // from first boot. Only the dense Qdrant upsert is skipped; the managed-
244
+ // credential reseed (`maybeReseedCapabilitiesAfterManagedCredential`) and
245
+ // the v3 maintain pass backfill the dense vectors once the backend recovers.
235
246
  const nextEntries = new Map<string, SkillEntry>();
236
247
  let denseVectors: number[][] = [];
248
+ let denseAvailable = false;
249
+ let denseError: unknown = null;
237
250
  let encodeSparse: (
238
251
  input: string,
239
252
  ) => ReturnType<typeof generateSparseEmbedding> = generateSparseEmbedding;
240
253
  if (seeds.length > 0) {
241
- const embedded = await embedWithBackend(
242
- config,
243
- seeds.map((s) => s.content),
244
- );
245
- denseVectors = await Promise.all(
246
- embedded.vectors.map((v) =>
247
- applyCorrectionIfCalibrated(v, embedded.provider, embedded.model),
248
- ),
249
- );
250
-
251
254
  // Skills share the concept-page Qdrant collection, so the sparse vector
252
255
  // must use the same stemmed BM25 encoding the concept-page documents
253
256
  // carry — otherwise the stemmed BM25 query vectors used by callers (see
@@ -263,6 +266,24 @@ async function runSeedV2SkillEntries(generation: number): Promise<void> {
263
266
  b: config.memory.v2.bm25_b,
264
267
  })
265
268
  : generateSparseEmbedding(input);
269
+ try {
270
+ const embedded = await embedWithBackend(
271
+ config,
272
+ seeds.map((s) => s.content),
273
+ );
274
+ denseVectors = await Promise.all(
275
+ embedded.vectors.map((v) =>
276
+ applyCorrectionIfCalibrated(v, embedded.provider, embedded.model),
277
+ ),
278
+ );
279
+ denseAvailable = true;
280
+ } catch (err) {
281
+ denseError = err;
282
+ log.warn(
283
+ { err },
284
+ "Embedding backend unavailable — seeding skill cache without dense Qdrant vectors; the needle lane surfaces skills from the cache and the dense lane backfills when the backend recovers",
285
+ );
286
+ }
266
287
  }
267
288
 
268
289
  if (generation !== requestedSeedGeneration) {
@@ -274,7 +295,17 @@ async function runSeedV2SkillEntries(generation: number): Promise<void> {
274
295
  return;
275
296
  }
276
297
 
277
- if (seeds.length > 0) {
298
+ // Populate the in-memory cache (and therefore the page index / needle lane)
299
+ // from the local catalog regardless of dense availability.
300
+ for (const seed of seeds) {
301
+ nextEntries.set(seed.id, seed);
302
+ }
303
+
304
+ // Write Qdrant points only when dense vectors were produced. In the
305
+ // degraded (backend-unavailable) path we skip Qdrant mutation entirely —
306
+ // both the upsert and the prune below — so we never write half-formed
307
+ // points or reconcile the collection against a set we did not persist.
308
+ if (seeds.length > 0 && denseAvailable) {
278
309
  const now = Date.now();
279
310
  await Promise.all(
280
311
  seeds.map((seed, i) =>
@@ -287,16 +318,15 @@ async function runSeedV2SkillEntries(generation: number): Promise<void> {
287
318
  }),
288
319
  ),
289
320
  );
290
- for (const seed of seeds) {
291
- nextEntries.set(seed.id, seed);
292
- }
293
321
  }
294
322
 
295
- // Prune stale skill slugs. When the catalog is unavailable (empty array
296
- // from network failure or cold cache), we cannot enumerate which
297
- // uninstalled catalog skills should exist, so skip pruning entirely to
298
- // avoid aggressively removing previously-seeded catalog skill embeddings.
299
- if (catalogAvailable) {
323
+ // Prune stale skill slugs. Skip when the catalog is unavailable (empty array
324
+ // from network failure or cold cache we cannot enumerate which uninstalled
325
+ // catalog skills should exist) OR when dense vectors were not written this
326
+ // run (don't reconcile Qdrant against points we did not refresh). The
327
+ // `seeds.length === 0` branch still prunes under an available catalog so the
328
+ // all-disabled case clears stale rows.
329
+ if (catalogAvailable && (denseAvailable || seeds.length === 0)) {
300
330
  // Tag legacy skill points missing `payload.kind` before pruning so the
301
331
  // kind-scoped prune can see them. Once-per-process; the backfill is
302
332
  // idempotent (server-side `is_empty` filter), so a partial failure
@@ -321,19 +351,26 @@ async function runSeedV2SkillEntries(generation: number): Promise<void> {
321
351
  seeds.map((s) => s.id),
322
352
  { kind: SKILL_PAYLOAD_KIND },
323
353
  );
324
- } else {
354
+ } else if (!catalogAvailable) {
325
355
  log.info(
326
356
  "Catalog unavailable — skipping skill pruning to preserve prior catalog embeddings",
327
357
  );
328
358
  }
329
359
 
330
- // Atomically replace the cache only after every step above succeeds.
331
- entries = nextEntries;
332
- // Drop the page-index cache so the next router invocation observes the
333
- // freshly seeded skill set (skill entries share the unified concept-page
360
+ // Atomically replace the cache from the freshly enumerated skills. The local
361
+ // resolution (`resolveSkillStates`) is authoritative, so a skill the config
362
+ // just disabled or removed drops out here even when the remote catalog is
363
+ // unavailable. Drop the page-index cache so the next router invocation
364
+ // observes the new skill set (skill entries share the unified concept-page
334
365
  // collection and surface in the same index).
366
+ entries = nextEntries;
335
367
  invalidatePageIndex();
336
- lastSeedError = null;
368
+
369
+ // Surface a dense-embed failure to `throwOnError` callers (the managed-
370
+ // credential reseed and the operator reembed route) so the existing retry +
371
+ // maintain machinery backfills the dense lane. The in-memory cache is
372
+ // already updated above, so the needle lane is fixed regardless.
373
+ lastSeedError = denseError;
337
374
  } catch (err) {
338
375
  lastSeedError = err;
339
376
  log.warn({ err }, "Failed to seed v2 skill entries");
@@ -0,0 +1,87 @@
1
+ import { and, asc, eq, gt, or } from "drizzle-orm";
2
+ import { v4 as uuid } from "uuid";
3
+
4
+ import { getCachedShareAnalytics } from "../platform/consent-cache.js";
5
+ import { getTelemetryDb } from "./db-connection.js";
6
+ import { watchdogEvents } from "./schema.js";
7
+
8
+ /**
9
+ * Input for one `watchdog` telemetry event. Metadata only — never conversation
10
+ * content. `value` and `detail` are optional; omitting both yields the minimal
11
+ * event (check_name only). `detail` is a JSON bag serialized to text on persist
12
+ * and forwarded verbatim on flush.
13
+ */
14
+ export interface WatchdogEventRecord {
15
+ checkName: string;
16
+ /** Measured magnitude (block ms, idle ms, ...). Null when the check carries no scalar. */
17
+ value?: number | null;
18
+ /** Open JSON bag for extra fields (reason codes, secondary numbers, ...). */
19
+ detail?: Record<string, unknown> | null;
20
+ }
21
+
22
+ /** A persisted watchdog event row. */
23
+ export interface WatchdogEvent {
24
+ id: string;
25
+ createdAt: number;
26
+ checkName: string;
27
+ value: number | null;
28
+ /** Raw `detail` JSON text from the row, or null. Parsed by the reporter on flush. */
29
+ detail: string | null;
30
+ }
31
+
32
+ /**
33
+ * Record a `watchdog` telemetry event for a watchdog check firing. No-ops when
34
+ * usage data collection is disabled (the event is dropped to honor the opt-out,
35
+ * matching the rest of telemetry) — so opt-out rows never exist and the
36
+ * reporter's standard 0 watermark default is safe.
37
+ */
38
+ export function recordWatchdogEvent(record: WatchdogEventRecord): void {
39
+ if (!getCachedShareAnalytics()) return;
40
+ const db = getTelemetryDb();
41
+ if (!db) return;
42
+ db.insert(watchdogEvents)
43
+ .values({
44
+ id: uuid(),
45
+ createdAt: Date.now(),
46
+ checkName: record.checkName,
47
+ value: record.value ?? null,
48
+ detail: record.detail != null ? JSON.stringify(record.detail) : null,
49
+ })
50
+ .run();
51
+ }
52
+
53
+ /**
54
+ * Query watchdog events that haven't been reported to telemetry yet.
55
+ * Uses a compound cursor (createdAt + id) for reliable watermarking.
56
+ */
57
+ export function queryUnreportedWatchdogEvents(
58
+ afterCreatedAt: number,
59
+ afterId: string | undefined,
60
+ limit: number,
61
+ ): WatchdogEvent[] {
62
+ const db = getTelemetryDb();
63
+ if (!db) return [];
64
+ return db
65
+ .select({
66
+ id: watchdogEvents.id,
67
+ createdAt: watchdogEvents.createdAt,
68
+ checkName: watchdogEvents.checkName,
69
+ value: watchdogEvents.value,
70
+ detail: watchdogEvents.detail,
71
+ })
72
+ .from(watchdogEvents)
73
+ .where(
74
+ afterId
75
+ ? or(
76
+ gt(watchdogEvents.createdAt, afterCreatedAt),
77
+ and(
78
+ eq(watchdogEvents.createdAt, afterCreatedAt),
79
+ gt(watchdogEvents.id, afterId),
80
+ ),
81
+ )
82
+ : gt(watchdogEvents.createdAt, afterCreatedAt),
83
+ )
84
+ .orderBy(asc(watchdogEvents.createdAt), asc(watchdogEvents.id))
85
+ .limit(limit)
86
+ .all();
87
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Shared control surface for the memory jobs worker *process* — the detached
3
+ * OS process whose entry point is `worker-process.ts`.
4
+ *
5
+ * Both the `assistant memory worker` CLI and the daemon lifecycle (when
6
+ * `memory.worker.enabled` is set) need to probe, spawn, and stop this process,
7
+ * so the PID-file bookkeeping lives here in one place.
8
+ */
9
+
10
+ import { existsSync, readFileSync, unlinkSync } from "node:fs";
11
+
12
+ import { getMemoryWorkerPidPath } from "../util/platform.js";
13
+
14
+ export interface MemoryWorkerStatus {
15
+ status: "running" | "not_running";
16
+ pid?: number;
17
+ }
18
+
19
+ /**
20
+ * Inspect the PID file to determine whether the worker process is alive.
21
+ * A stale PID file (pointing at a dead process) is cleaned up and reported
22
+ * as not_running.
23
+ */
24
+ export function probeMemoryWorker(): MemoryWorkerStatus {
25
+ const pidPath = getMemoryWorkerPidPath();
26
+ if (!existsSync(pidPath)) return { status: "not_running" };
27
+
28
+ const raw = readFileSync(pidPath, "utf-8").trim();
29
+ const pid = parseInt(raw, 10);
30
+ if (!Number.isFinite(pid) || pid <= 0) return { status: "not_running" };
31
+
32
+ try {
33
+ process.kill(pid, 0);
34
+ return { status: "running", pid };
35
+ } catch (err: unknown) {
36
+ if (
37
+ err &&
38
+ typeof err === "object" &&
39
+ "code" in err &&
40
+ err.code === "ESRCH"
41
+ ) {
42
+ // Stale PID file — clean it up.
43
+ try {
44
+ unlinkSync(pidPath);
45
+ } catch {
46
+ // best-effort
47
+ }
48
+ return { status: "not_running" };
49
+ }
50
+ throw err;
51
+ }
52
+ }
53
+
54
+ export class MemoryWorkerSpawnError extends Error {}
55
+
56
+ /**
57
+ * Spawn the memory worker as a detached background process.
58
+ *
59
+ * If a worker is already running, returns its PID with `alreadyRunning: true`
60
+ * rather than spawning a second one. Throws {@link MemoryWorkerSpawnError} if
61
+ * the child is spawned but never writes its PID file (i.e. failed to start).
62
+ */
63
+ export async function spawnMemoryWorkerProcess(): Promise<{
64
+ pid: number;
65
+ alreadyRunning: boolean;
66
+ }> {
67
+ const current = probeMemoryWorker();
68
+ if (current.status === "running" && current.pid != null) {
69
+ return { pid: current.pid, alreadyRunning: true };
70
+ }
71
+
72
+ const pidPath = getMemoryWorkerPidPath();
73
+ const entry = new URL("./worker-process.ts", import.meta.url);
74
+
75
+ // Spawn detached so the worker survives the spawning process exiting.
76
+ const child = Bun.spawn({
77
+ cmd: ["bun", "run", entry.pathname],
78
+ stdio: ["ignore", "ignore", "ignore"],
79
+ detached: true,
80
+ });
81
+
82
+ // Unreference so the spawning process doesn't wait for the child.
83
+ child.unref();
84
+
85
+ // Wait briefly for the PID file to appear (the worker writes it on startup).
86
+ let pidWritten = false;
87
+ for (let i = 0; i < 10; i++) {
88
+ await Bun.sleep(100);
89
+ if (existsSync(pidPath)) {
90
+ pidWritten = true;
91
+ break;
92
+ }
93
+ }
94
+
95
+ if (!pidWritten) {
96
+ throw new MemoryWorkerSpawnError(
97
+ "Memory worker was spawned but PID file did not appear within 1s",
98
+ );
99
+ }
100
+
101
+ const pid = parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
102
+ return { pid, alreadyRunning: false };
103
+ }
104
+
105
+ /**
106
+ * Send SIGTERM to the worker process if it is actually running.
107
+ *
108
+ * Returns the status observed before signalling, so callers can report
109
+ * whether anything was stopped. Only throws if `process.kill` itself fails
110
+ * (e.g. EPERM) — a not-running worker is a no-op.
111
+ */
112
+ export function stopMemoryWorkerProcess(): MemoryWorkerStatus {
113
+ const current = probeMemoryWorker();
114
+ if (current.status === "running" && current.pid != null) {
115
+ process.kill(current.pid, "SIGTERM");
116
+ }
117
+ return current;
118
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Standalone entry point for the memory jobs worker as its own OS process.
3
+ *
4
+ * Spawned by `assistant memory worker start`. Loads config, starts the
5
+ * worker loop, writes a PID file, and stays alive until SIGTERM/SIGINT.
6
+ *
7
+ * The worker's internal `setTimeout` calls `.unref()`, which is correct
8
+ * inside the daemon (don't keep the daemon alive for the worker) but would
9
+ * cause this standalone process to exit immediately. A ref'd keep-alive
10
+ * interval prevents that.
11
+ */
12
+
13
+ import { existsSync, unlinkSync, writeFileSync } from "node:fs";
14
+
15
+ import { getConfig } from "../config/loader.js";
16
+ import { getLogger } from "../util/logger.js";
17
+ import { getMemoryWorkerPidPath } from "../util/platform.js";
18
+ import { startInProcessMemoryJobsWorker } from "./jobs-worker.js";
19
+
20
+ const log = getLogger("memory-worker-process");
21
+
22
+ function cleanupPidFile(): void {
23
+ const pidPath = getMemoryWorkerPidPath();
24
+ try {
25
+ if (existsSync(pidPath)) unlinkSync(pidPath);
26
+ } catch {
27
+ // best-effort
28
+ }
29
+ }
30
+
31
+ async function main(): Promise<void> {
32
+ const config = getConfig();
33
+ const pidPath = getMemoryWorkerPidPath();
34
+
35
+ if (config.memory.enabled === false) {
36
+ log.info("Memory is disabled in config; worker process exiting");
37
+ process.exit(0);
38
+ }
39
+
40
+ // Write PID file so `status` and `stop` can find us.
41
+ writeFileSync(pidPath, String(process.pid), { flag: "w" });
42
+ log.info({ pid: process.pid, pidPath }, "Memory worker process started");
43
+
44
+ const worker = startInProcessMemoryJobsWorker();
45
+
46
+ // Keep-alive: the worker's setTimeout timers are unref'd, so without
47
+ // this interval the process would exit immediately.
48
+ const keepAlive = setInterval(() => {}, 60_000);
49
+
50
+ const shutdown = (signal: string) => {
51
+ log.info({ signal }, "Memory worker process shutting down");
52
+ worker.stop();
53
+ clearInterval(keepAlive);
54
+ cleanupPidFile();
55
+ process.exit(0);
56
+ };
57
+
58
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
59
+ process.on("SIGINT", () => shutdown("SIGINT"));
60
+
61
+ // Clean up if the process exits unexpectedly through any other path.
62
+ process.on("exit", () => {
63
+ worker.stop();
64
+ cleanupPidFile();
65
+ });
66
+ }
67
+
68
+ void main().catch((err) => {
69
+ log.error({ err }, "Memory worker process failed to start");
70
+ cleanupPidFile();
71
+ process.exit(1);
72
+ });
@@ -27,14 +27,22 @@ mock.module("../copy-composer.js", () => ({
27
27
  composeFallbackCopy: () => composeFallbackReturn,
28
28
  }));
29
29
 
30
- mock.module("../destination-resolver.js", () => ({
31
- resolveDestinations: (channels: string[]) => {
32
- const map = new Map<string, ChannelDestination>();
33
- for (const ch of channels) {
34
- map.set(ch, { channel: ch as ChannelDestination["channel"] });
35
- }
36
- return map;
37
- },
30
+ // Stub only getGuardianDelivery; keep the real selectors so this mock is
31
+ // harmless if it leaks into destination-resolver.test.ts under a shared run.
32
+ const realGuardianReader = await import(
33
+ "../../contacts/guardian-delivery-reader.js"
34
+ );
35
+ mock.module("../../contacts/guardian-delivery-reader.js", () => ({
36
+ ...realGuardianReader,
37
+ getGuardianDelivery: async () => null,
38
+ }));
39
+
40
+ // Use the real destination-resolver (DB-free via the local-read stub below)
41
+ // so this mock does not leak into destination-resolver.test.ts under a shared
42
+ // bun-test invocation. With no guardian, the resolver still yields a vellum
43
+ // destination, which is all these tests exercise.
44
+ mock.module("../../contacts/contact-store.js", () => ({
45
+ findGuardianForChannel: () => null,
38
46
  }));
39
47
 
40
48
  mock.module("../conversation-pairing.js", () => ({
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Tests for getConnectedChannels connectivity resolution.
3
+ *
4
+ * Connectivity must mirror destination-resolver's `resolveGuardian`:
5
+ * gateway-first, with a LOCAL contacts fallback on ANY per-channel no-match
6
+ * (gateway list null OR no active gateway entry for the channel). This keeps a
7
+ * channel from being marked connected when it can't be delivered (and
8
+ * vice-versa).
9
+ */
10
+
11
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
12
+
13
+ import type { GuardianDelivery } from "@vellumai/gateway-client";
14
+
15
+ mock.module("../../util/logger.js", () => ({
16
+ getLogger: () =>
17
+ new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
18
+ truncateForLog: (value: string) => value,
19
+ }));
20
+
21
+ let deliverableChannels: string[] = [];
22
+ let gatewayGuardians: GuardianDelivery[] | null = null;
23
+ let localChatId: string | null = null;
24
+
25
+ const realConfig = await import("../../channels/config.js");
26
+
27
+ mock.module("../../channels/config.js", () => ({
28
+ ...realConfig,
29
+ getDeliverableChannels: () => deliverableChannels,
30
+ }));
31
+
32
+ const realReader = await import("../../contacts/guardian-delivery-reader.js");
33
+
34
+ mock.module("../../contacts/guardian-delivery-reader.js", () => ({
35
+ ...realReader,
36
+ getGuardianDelivery: async () => gatewayGuardians,
37
+ }));
38
+
39
+ const realContactStore = await import("../../contacts/contact-store.js");
40
+
41
+ mock.module("../../contacts/contact-store.js", () => ({
42
+ ...realContactStore,
43
+ findGuardianForChannel: (_channelType: string) =>
44
+ localChatId === null
45
+ ? null
46
+ : { contact: { principalId: "p1" }, channel: { externalChatId: localChatId } },
47
+ }));
48
+
49
+ const { getConnectedChannels } = await import("../emit-signal.js");
50
+
51
+ function gatewayBinding(channelType: string, externalChatId: string): GuardianDelivery {
52
+ return { channelType, contactId: "c1", address: "addr", externalChatId, status: "active" };
53
+ }
54
+
55
+ beforeEach(() => {
56
+ deliverableChannels = [];
57
+ gatewayGuardians = null;
58
+ localChatId = null;
59
+ });
60
+
61
+ describe("getConnectedChannels gateway-first-then-local connectivity", () => {
62
+ test("marks telegram connected from a gateway-only binding", async () => {
63
+ deliverableChannels = ["telegram"];
64
+ gatewayGuardians = [gatewayBinding("telegram", "123")];
65
+ localChatId = null;
66
+
67
+ expect(await getConnectedChannels()).toContain("telegram");
68
+ });
69
+
70
+ test("falls back to a local binding when the gateway is unreachable (null)", async () => {
71
+ deliverableChannels = ["telegram"];
72
+ gatewayGuardians = null;
73
+ localChatId = "456";
74
+
75
+ expect(await getConnectedChannels()).toContain("telegram");
76
+ });
77
+
78
+ test("marks telegram disconnected when neither source has a binding", async () => {
79
+ deliverableChannels = ["telegram"];
80
+ gatewayGuardians = null;
81
+ localChatId = null;
82
+
83
+ expect(await getConnectedChannels()).not.toContain("telegram");
84
+ });
85
+
86
+ test("falls back to local when the gateway responds without that channel", async () => {
87
+ // Gateway present but with no active telegram entry ⇒ per-channel no-match,
88
+ // so connectivity falls back to the local mirror (mirrors
89
+ // destination-resolver's per-channel fallback).
90
+ deliverableChannels = ["telegram"];
91
+ gatewayGuardians = [];
92
+ localChatId = "789";
93
+
94
+ expect(await getConnectedChannels()).toContain("telegram");
95
+ });
96
+
97
+ test("only marks slack connected for D-prefixed (DM) chat IDs", async () => {
98
+ deliverableChannels = ["slack"];
99
+ gatewayGuardians = [gatewayBinding("slack", "C-public")];
100
+ expect(await getConnectedChannels()).not.toContain("slack");
101
+
102
+ gatewayGuardians = [gatewayBinding("slack", "D-dm")];
103
+ expect(await getConnectedChannels()).toContain("slack");
104
+ });
105
+
106
+ test("always reports vellum and platform connected", async () => {
107
+ deliverableChannels = ["vellum", "platform"];
108
+ gatewayGuardians = null;
109
+
110
+ const connected = await getConnectedChannels();
111
+ expect(connected).toContain("vellum");
112
+ expect(connected).toContain("platform");
113
+ });
114
+ });
@@ -34,19 +34,38 @@ mock.module("../../prompts/system-prompt.js", () => ({
34
34
  buildCoreIdentityContext: () => null,
35
35
  }));
36
36
 
37
+ // Guardian binding (ACL) is resolved via the gateway pull; notes (INFO) are
38
+ // joined locally by contactId. Tests drive both via mutable slots.
39
+ let guardianDeliveryFixture: Array<{ contactId: string }> = [];
40
+ let contactInfoFixture: Record<string, { notes: string | null } | null> = {};
41
+
42
+ mock.module("../../contacts/guardian-delivery-reader.js", () => ({
43
+ getGuardianDelivery: async () => guardianDeliveryFixture,
44
+ anyGuardian: (list: Array<{ contactId: string }>) => list[0],
45
+ }));
46
+
37
47
  mock.module("../../contacts/contact-store.js", () => ({
38
- listGuardianChannels: () => null,
48
+ findContactInfoById: (contactId: string) =>
49
+ contactInfoFixture[contactId] ?? null,
39
50
  }));
40
51
 
41
- // Provider mock if `getConfiguredProvider` is ever called by the
42
- // assistant_tool pass-through path, this throw makes the test fail
43
- // loudly instead of silently exercising the LLM path.
52
+ // Provider mock. By default `sendMessage` throws so the assistant_tool
53
+ // pass-through path (which must skip the LLM) fails loudly if it reaches the
54
+ // provider. LLM-path tests override `providerSendMessage` to capture inputs.
55
+ let providerSendMessage: (
56
+ messages: unknown[],
57
+ opts: { systemPrompt?: string },
58
+ ) => Promise<unknown> = () => {
59
+ throw new Error(
60
+ "provider.sendMessage should NOT be invoked for assistant_tool pass-through",
61
+ );
62
+ };
63
+
44
64
  mock.module("../../providers/provider-send-message.js", () => ({
45
- getConfiguredProvider: async () => {
46
- throw new Error(
47
- "getConfiguredProvider should NOT be invoked for assistant_tool pass-through",
48
- );
49
- },
65
+ getConfiguredProvider: async () => ({
66
+ sendMessage: (messages: unknown[], opts: { systemPrompt?: string }) =>
67
+ providerSendMessage(messages, opts),
68
+ }),
50
69
  createTimeout: () => ({
51
70
  signal: new AbortController().signal,
52
71
  cleanup: () => {},
@@ -281,3 +300,53 @@ describe("assistant_tool pass-through in notification decision engine", () => {
281
300
  expect(decision.renderedCopy.vellum?.body).toBe("fyi");
282
301
  });
283
302
  });
303
+
304
+ describe("recipient notes injection (ACL from gateway, notes joined locally)", () => {
305
+ function makeLlmSignal(): NotificationSignal {
306
+ return {
307
+ signalId: "sig-llm-notes-1",
308
+ createdAt: Date.now(),
309
+ sourceChannel: "scheduler",
310
+ sourceContextId: "schedule-1",
311
+ sourceEventName: "schedule.notify",
312
+ contextPayload: {},
313
+ attentionHints: {
314
+ requiresAction: false,
315
+ urgency: "low",
316
+ isAsyncBackground: false,
317
+ visibleInSourceNow: false,
318
+ },
319
+ };
320
+ }
321
+
322
+ test("injects the guardian's local notes, resolved via the gateway contactId", async () => {
323
+ guardianDeliveryFixture = [{ contactId: "contact-42" }];
324
+ contactInfoFixture = { "contact-42": { notes: "Prefers terse updates." } };
325
+
326
+ let capturedSystemPrompt: string | undefined;
327
+ providerSendMessage = async (_messages, opts) => {
328
+ capturedSystemPrompt = opts.systemPrompt;
329
+ return {};
330
+ };
331
+
332
+ await evaluateSignal(makeLlmSignal(), ["vellum"] as NotificationChannel[]);
333
+
334
+ expect(capturedSystemPrompt).toContain("<recipient-context>");
335
+ expect(capturedSystemPrompt).toContain("Prefers terse updates.");
336
+ });
337
+
338
+ test("omits recipient context when no guardian is bound", async () => {
339
+ guardianDeliveryFixture = [];
340
+ contactInfoFixture = {};
341
+
342
+ let capturedSystemPrompt: string | undefined;
343
+ providerSendMessage = async (_messages, opts) => {
344
+ capturedSystemPrompt = opts.systemPrompt;
345
+ return {};
346
+ };
347
+
348
+ await evaluateSignal(makeLlmSignal(), ["vellum"] as NotificationChannel[]);
349
+
350
+ expect(capturedSystemPrompt).not.toContain("<recipient-context>");
351
+ });
352
+ });