@vellumai/assistant 0.8.2 → 0.8.4

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 (503) hide show
  1. package/ARCHITECTURE.md +11 -12
  2. package/docker-entrypoint.sh +13 -2
  3. package/docker-init-apt-root.sh +79 -6
  4. package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
  5. package/openapi.yaml +945 -36
  6. package/package.json +1 -1
  7. package/src/__tests__/agent-loop-exit-reason.test.ts +271 -0
  8. package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
  9. package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
  10. package/src/__tests__/agent-loop.test.ts +88 -3
  11. package/src/__tests__/anthropic-provider.test.ts +272 -0
  12. package/src/__tests__/approval-cascade.test.ts +1 -1
  13. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
  14. package/src/__tests__/channel-delivery-store.test.ts +193 -0
  15. package/src/__tests__/channel-reply-delivery.test.ts +284 -5
  16. package/src/__tests__/channel-retry-sweep.test.ts +274 -1
  17. package/src/__tests__/compaction-events.test.ts +1 -1
  18. package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
  19. package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
  20. package/src/__tests__/config-get-vision-flag.test.ts +136 -0
  21. package/src/__tests__/config-loader-backfill.test.ts +115 -18
  22. package/src/__tests__/config-watcher.test.ts +1 -1
  23. package/src/__tests__/context-token-estimator.test.ts +112 -57
  24. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  25. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +54 -3
  26. package/src/__tests__/conversation-agent-loop-overflow.test.ts +31 -6
  27. package/src/__tests__/conversation-agent-loop.test.ts +77 -3
  28. package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
  29. package/src/__tests__/conversation-clean-command.test.ts +137 -0
  30. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
  31. package/src/__tests__/conversation-fork-crud.test.ts +161 -0
  32. package/src/__tests__/conversation-lifecycle.test.ts +1 -1
  33. package/src/__tests__/conversation-load-cleaned-at.test.ts +279 -0
  34. package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
  35. package/src/__tests__/conversation-media-retry.test.ts +19 -8
  36. package/src/__tests__/conversation-pairing.test.ts +2 -2
  37. package/src/__tests__/conversation-process-callsite.test.ts +1 -1
  38. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
  39. package/src/__tests__/conversation-queue.test.ts +1 -1
  40. package/src/__tests__/conversation-runtime-assembly.test.ts +290 -85
  41. package/src/__tests__/conversation-seed-composer.test.ts +66 -4
  42. package/src/__tests__/conversation-slash-commands.test.ts +36 -8
  43. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  44. package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
  45. package/src/__tests__/conversation-speed-override.test.ts +1 -1
  46. package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
  47. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
  48. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  49. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  50. package/src/__tests__/credential-security-invariants.test.ts +6 -0
  51. package/src/__tests__/cu-unified-flow.test.ts +10 -1
  52. package/src/__tests__/date-context.test.ts +45 -0
  53. package/src/__tests__/dm-backfill.test.ts +64 -0
  54. package/src/__tests__/dm-persistence.test.ts +33 -0
  55. package/src/__tests__/document-find-replace.test.ts +501 -0
  56. package/src/__tests__/external-plugin-loader.test.ts +91 -19
  57. package/src/__tests__/first-greeting.test.ts +23 -2
  58. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
  59. package/src/__tests__/guardian-dispatch.test.ts +1 -0
  60. package/src/__tests__/headless-browser-navigate.test.ts +172 -0
  61. package/src/__tests__/heartbeat-service.test.ts +24 -164
  62. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  63. package/src/__tests__/host-app-control-proxy.test.ts +241 -0
  64. package/src/__tests__/host-bash-proxy.test.ts +6 -0
  65. package/src/__tests__/host-browser-proxy.test.ts +10 -0
  66. package/src/__tests__/host-cu-proxy.test.ts +8 -1
  67. package/src/__tests__/host-file-proxy.test.ts +8 -1
  68. package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
  69. package/src/__tests__/host-transfer-proxy.test.ts +8 -1
  70. package/src/__tests__/identity-routes.test.ts +57 -0
  71. package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
  72. package/src/__tests__/injector-background-turn.test.ts +153 -0
  73. package/src/__tests__/injector-chain.test.ts +7 -0
  74. package/src/__tests__/injector-document-comments.test.ts +378 -0
  75. package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
  76. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
  77. package/src/__tests__/list-messages-attachments.test.ts +21 -17
  78. package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
  79. package/src/__tests__/list-messages-page-latest.test.ts +130 -14
  80. package/src/__tests__/list-messages-tool-merge.test.ts +17 -16
  81. package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
  82. package/src/__tests__/llm-catalog-parity.test.ts +3 -0
  83. package/src/__tests__/llm-context-normalization.test.ts +0 -2
  84. package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
  85. package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
  86. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
  87. package/src/__tests__/llm-resolver.test.ts +340 -3
  88. package/src/__tests__/log-export-routes.test.ts +99 -2
  89. package/src/__tests__/managed-profile-guard.test.ts +10 -0
  90. package/src/__tests__/message-queue-steer.test.ts +114 -0
  91. package/src/__tests__/notification-decision-fallback.test.ts +0 -91
  92. package/src/__tests__/notification-decision-strategy.test.ts +14 -31
  93. package/src/__tests__/notification-deep-link.test.ts +15 -0
  94. package/src/__tests__/notification-guardian-path.test.ts +1 -2
  95. package/src/__tests__/notification-platform-adapter.test.ts +5 -4
  96. package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
  97. package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
  98. package/src/__tests__/openai-provider.test.ts +323 -3
  99. package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
  100. package/src/__tests__/openai-responses-provider.test.ts +4 -4
  101. package/src/__tests__/openrouter-provider-only.test.ts +51 -3
  102. package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
  103. package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
  104. package/src/__tests__/pending-interactions-resolved-event.test.ts +190 -0
  105. package/src/__tests__/platform-proxy-context.test.ts +6 -1
  106. package/src/__tests__/platform.test.ts +0 -3
  107. package/src/__tests__/plugin-source-watcher.test.ts +302 -0
  108. package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
  109. package/src/__tests__/plugin-types.test.ts +2 -2
  110. package/src/__tests__/process-message-background-slack.test.ts +1 -51
  111. package/src/__tests__/process-message-display-content.test.ts +21 -16
  112. package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
  113. package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
  114. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
  115. package/src/__tests__/server-history-render.test.ts +83 -4
  116. package/src/__tests__/steer-tool-repair.test.ts +249 -0
  117. package/src/__tests__/system-prompt.test.ts +57 -101
  118. package/src/__tests__/terminal-tools.test.ts +11 -1
  119. package/src/__tests__/thinking-block-replay.test.ts +113 -0
  120. package/src/__tests__/thread-backfill.test.ts +370 -22
  121. package/src/__tests__/tool-executor.test.ts +90 -1
  122. package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
  123. package/src/__tests__/twilio-routes.test.ts +1 -1
  124. package/src/__tests__/web-fetch.test.ts +2 -2
  125. package/src/__tests__/workspace-git-service.test.ts +88 -5
  126. package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
  127. package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
  128. package/src/a2a/__tests__/agent-card.test.ts +98 -0
  129. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
  130. package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
  131. package/src/a2a/__tests__/task-store.test.ts +246 -0
  132. package/src/a2a/agent-card.ts +58 -0
  133. package/src/a2a/feature-gate.ts +8 -0
  134. package/src/a2a/protocol-constants.ts +21 -0
  135. package/src/a2a/protocol-errors.ts +50 -0
  136. package/src/a2a/protocol-types.ts +162 -0
  137. package/src/a2a/task-store.ts +168 -0
  138. package/src/agent/attachments.ts +1 -0
  139. package/src/agent/loop.ts +208 -22
  140. package/src/background-wake/next-wake.test.ts +289 -0
  141. package/src/background-wake/next-wake.ts +172 -0
  142. package/src/browser/operations.ts +15 -0
  143. package/src/channels/config.ts +9 -0
  144. package/src/channels/types.ts +14 -0
  145. package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
  146. package/src/cli/commands/__tests__/memory-v2.test.ts +9 -12
  147. package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
  148. package/src/cli/commands/__tests__/schedules.test.ts +469 -0
  149. package/src/cli/commands/conversations.ts +128 -1
  150. package/src/cli/commands/inference-providers.ts +147 -1
  151. package/src/cli/commands/memory-v2.ts +308 -0
  152. package/src/cli/commands/notifications.ts +89 -37
  153. package/src/cli/commands/plugins.ts +67 -0
  154. package/src/cli/commands/schedules.ts +297 -5
  155. package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
  156. package/src/cli/lib/install-from-github.ts +8 -9
  157. package/src/cli/lib/search-plugins.ts +163 -0
  158. package/src/cli/program.ts +14 -0
  159. package/src/cli/utils/conversation-id.ts +17 -5
  160. package/src/config/assistant-feature-flags.ts +24 -54
  161. package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
  162. package/src/config/bundled-skills/document-editor/SKILL.md +115 -0
  163. package/src/config/bundled-skills/document-editor/TOOLS.json +240 -0
  164. package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
  165. package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
  166. package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
  167. package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
  168. package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
  169. package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
  170. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  171. package/src/config/bundled-skills/schedule/SKILL.md +8 -0
  172. package/src/config/bundled-tool-registry.ts +22 -12
  173. package/src/config/call-site-defaults.ts +124 -0
  174. package/src/config/feature-flag-registry.json +111 -23
  175. package/src/config/llm-resolver.ts +66 -1
  176. package/src/config/schema.ts +2 -0
  177. package/src/config/schemas/__tests__/memory-v2.test.ts +7 -3
  178. package/src/config/schemas/call-site-catalog.ts +21 -0
  179. package/src/config/schemas/channels.ts +9 -0
  180. package/src/config/schemas/conversations.ts +10 -0
  181. package/src/config/schemas/heartbeat.ts +14 -0
  182. package/src/config/schemas/llm.ts +4 -3
  183. package/src/config/schemas/memory-retrospective.ts +1 -1
  184. package/src/config/schemas/memory-v2.ts +51 -4
  185. package/src/config/schemas/memory.ts +3 -1
  186. package/src/config/seed-inference-profiles.ts +99 -29
  187. package/src/context/compactor.ts +80 -13
  188. package/src/context/token-estimator.ts +72 -31
  189. package/src/context/window-manager.ts +25 -0
  190. package/src/credential-health/credential-health-service.ts +34 -19
  191. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
  192. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
  193. package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
  194. package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
  195. package/src/daemon/conversation-agent-loop-handlers.ts +231 -23
  196. package/src/daemon/conversation-agent-loop.ts +252 -56
  197. package/src/daemon/conversation-lifecycle.ts +142 -116
  198. package/src/daemon/conversation-messaging.ts +3 -0
  199. package/src/daemon/conversation-process.ts +273 -0
  200. package/src/daemon/conversation-queue-manager.ts +14 -0
  201. package/src/daemon/conversation-runtime-assembly.ts +144 -75
  202. package/src/daemon/conversation-slash.ts +37 -5
  203. package/src/daemon/conversation-surfaces.ts +45 -2
  204. package/src/daemon/conversation-tool-setup.ts +7 -0
  205. package/src/daemon/conversation.ts +42 -12
  206. package/src/daemon/date-context.ts +40 -0
  207. package/src/daemon/first-greeting.ts +10 -0
  208. package/src/daemon/guardian-action-generators.ts +1 -125
  209. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
  210. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
  211. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
  212. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
  213. package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
  214. package/src/daemon/handlers/config-a2a.ts +449 -0
  215. package/src/daemon/handlers/config-model.test.ts +1 -0
  216. package/src/daemon/handlers/conversations.ts +80 -0
  217. package/src/daemon/handlers/shared.ts +92 -29
  218. package/src/daemon/host-app-control-proxy.ts +69 -18
  219. package/src/daemon/host-bash-proxy.ts +1 -1
  220. package/src/daemon/host-cu-proxy.ts +1 -1
  221. package/src/daemon/host-file-proxy.ts +1 -1
  222. package/src/daemon/host-proxy-preactivation.ts +85 -18
  223. package/src/daemon/host-transfer-proxy.ts +1 -1
  224. package/src/daemon/lifecycle.ts +67 -65
  225. package/src/daemon/memory-v2-startup.ts +49 -13
  226. package/src/daemon/message-protocol.ts +4 -0
  227. package/src/daemon/message-types/conversations.ts +8 -0
  228. package/src/daemon/message-types/document-comments.ts +50 -0
  229. package/src/daemon/message-types/messages.ts +68 -1
  230. package/src/daemon/message-types/notifications.ts +21 -0
  231. package/src/daemon/message-types/surfaces.ts +3 -1
  232. package/src/daemon/message-types/web-activity.ts +57 -0
  233. package/src/daemon/pkb-reminder-builder.test.ts +10 -53
  234. package/src/daemon/pkb-reminder-builder.ts +4 -19
  235. package/src/daemon/plugin-source-watcher.ts +135 -3
  236. package/src/daemon/process-message.ts +72 -12
  237. package/src/daemon/query-complexity-router.ts +75 -0
  238. package/src/daemon/skill-memory-refresh.ts +5 -1
  239. package/src/daemon/trust-context.ts +6 -0
  240. package/src/daemon/wake-target-adapter.ts +2 -0
  241. package/src/documents/document-comments-store.test.ts +338 -0
  242. package/src/documents/document-comments-store.ts +237 -0
  243. package/src/documents/document-store.ts +202 -0
  244. package/src/export/__tests__/transcript-formatter.test.ts +121 -0
  245. package/src/export/transcript-formatter.ts +54 -20
  246. package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -1
  247. package/src/heartbeat/heartbeat-service.ts +35 -191
  248. package/src/home/__tests__/feed-types.test.ts +40 -0
  249. package/src/home/__tests__/suggested-prompts.test.ts +33 -2
  250. package/src/home/feed-types.ts +20 -3
  251. package/src/home/home-content-refresh.ts +52 -0
  252. package/src/home/home-greeting-cache.ts +69 -0
  253. package/src/home/home-greeting.ts +94 -0
  254. package/src/home/suggested-prompts.ts +177 -9
  255. package/src/ipc/cli-client.ts +147 -45
  256. package/src/memory/__tests__/conversation-queries.test.ts +220 -0
  257. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
  258. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
  259. package/src/memory/__tests__/memory-retrospective-job.test.ts +407 -10
  260. package/src/memory/conversation-crud.ts +133 -43
  261. package/src/memory/conversation-queries.ts +87 -1
  262. package/src/memory/conversation-title-service.ts +26 -4
  263. package/src/memory/db-init.ts +22 -0
  264. package/src/memory/delivery-crud.ts +41 -0
  265. package/src/memory/delivery-status.ts +141 -15
  266. package/src/memory/external-conversation-store.ts +32 -1
  267. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
  268. package/src/memory/graph/conversation-graph-memory.ts +18 -6
  269. package/src/memory/graph/tools.ts +6 -37
  270. package/src/memory/invite-store.ts +53 -0
  271. package/src/memory/jobs-worker.ts +21 -1
  272. package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
  273. package/src/memory/llm-request-log-store.ts +92 -1
  274. package/src/memory/memory-retrospective-constants.ts +28 -0
  275. package/src/memory/memory-retrospective-enqueue.ts +4 -22
  276. package/src/memory/memory-retrospective-job.ts +438 -21
  277. package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
  278. package/src/memory/memory-v2-activation-log-store.ts +26 -8
  279. package/src/memory/migrations/100-core-tables.ts +1 -0
  280. package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
  281. package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
  282. package/src/memory/migrations/251-a2a-tasks.ts +49 -0
  283. package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
  284. package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
  285. package/src/memory/migrations/253-document-comments.ts +47 -0
  286. package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
  287. package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
  288. package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
  289. package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
  290. package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
  291. package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
  292. package/src/memory/migrations/index.ts +20 -0
  293. package/src/memory/migrations/registry.ts +33 -0
  294. package/src/memory/onboarding-events-store.ts +7 -0
  295. package/src/memory/schema/a2a.ts +15 -0
  296. package/src/memory/schema/calls.ts +1 -0
  297. package/src/memory/schema/conversations.ts +3 -0
  298. package/src/memory/schema/index.ts +1 -0
  299. package/src/memory/schema/inference.ts +2 -0
  300. package/src/memory/schema/infrastructure.ts +2 -0
  301. package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
  302. package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
  303. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
  304. package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
  305. package/src/memory/v2/__tests__/injection.test.ts +221 -17
  306. package/src/memory/v2/__tests__/page-index.test.ts +365 -1
  307. package/src/memory/v2/__tests__/router.test.ts +489 -1
  308. package/src/memory/v2/__tests__/static-context.test.ts +12 -1
  309. package/src/memory/v2/activation-store.ts +14 -16
  310. package/src/memory/v2/cli-command-content.ts +19 -0
  311. package/src/memory/v2/cli-command-store.ts +304 -0
  312. package/src/memory/v2/consolidation-job.ts +14 -0
  313. package/src/memory/v2/frontmatter-sweep.ts +7 -1
  314. package/src/memory/v2/injection-events.ts +101 -0
  315. package/src/memory/v2/injection.ts +69 -29
  316. package/src/memory/v2/page-index.ts +246 -19
  317. package/src/memory/v2/page-store.ts +18 -0
  318. package/src/memory/v2/router.ts +209 -55
  319. package/src/memory/v2/static-context.ts +4 -4
  320. package/src/memory/v2/types.ts +23 -0
  321. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
  322. package/src/messaging/providers/a2a/deliver.ts +156 -0
  323. package/src/messaging/providers/gmail/client.ts +9 -2
  324. package/src/messaging/providers/index.ts +18 -3
  325. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
  326. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
  327. package/src/messaging/providers/slack/adapter.ts +178 -25
  328. package/src/messaging/providers/slack/api.test.ts +54 -0
  329. package/src/messaging/providers/slack/api.ts +119 -3
  330. package/src/messaging/providers/slack/client.ts +12 -0
  331. package/src/messaging/providers/slack/deep-link.ts +20 -1
  332. package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
  333. package/src/messaging/providers/slack/message-metadata.ts +156 -0
  334. package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
  335. package/src/messaging/providers/slack/render-transcript.ts +176 -49
  336. package/src/messaging/providers/slack/send.test.ts +77 -0
  337. package/src/messaging/providers/slack/send.ts +8 -2
  338. package/src/messaging/providers/slack/types.ts +14 -0
  339. package/src/notifications/__tests__/broadcaster.test.ts +203 -0
  340. package/src/notifications/__tests__/decision-engine.test.ts +283 -0
  341. package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
  342. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +5 -1
  343. package/src/notifications/__tests__/home-feed-side-effect.test.ts +521 -36
  344. package/src/notifications/adapters/macos.ts +12 -2
  345. package/src/notifications/broadcaster.ts +29 -4
  346. package/src/notifications/conversation-seed-composer.ts +14 -2
  347. package/src/notifications/copy-composer.ts +17 -64
  348. package/src/notifications/decision-engine.ts +111 -44
  349. package/src/notifications/deferred-emit.ts +135 -0
  350. package/src/notifications/deterministic-checks.ts +96 -0
  351. package/src/notifications/emit-signal.ts +10 -1
  352. package/src/notifications/home-feed-side-effect.ts +136 -27
  353. package/src/notifications/signal.ts +0 -4
  354. package/src/notifications/types.ts +8 -0
  355. package/src/oauth/connect-orchestrator.ts +3 -0
  356. package/src/oauth/credential-token-resolver.ts +2 -0
  357. package/src/oauth/manual-token-connection.ts +19 -0
  358. package/src/oauth/oauth-store.ts +12 -0
  359. package/src/oauth/platform-connection.test.ts +43 -3
  360. package/src/oauth/platform-connection.ts +13 -4
  361. package/src/oauth/seed-providers.ts +22 -0
  362. package/src/permissions/prompter.ts +5 -2
  363. package/src/permissions/secret-prompter.ts +4 -1
  364. package/src/plugins/defaults/injectors.ts +118 -26
  365. package/src/plugins/external-plugin-loader.ts +82 -10
  366. package/src/plugins/types.ts +16 -7
  367. package/src/prompts/__tests__/system-prompt.test.ts +44 -45
  368. package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
  369. package/src/prompts/normalize-onboarding.ts +40 -0
  370. package/src/prompts/sections.ts +32 -14
  371. package/src/prompts/system-prompt.ts +105 -76
  372. package/src/prompts/template-detection.ts +37 -0
  373. package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
  374. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  375. package/src/prompts/templates/VOICE.md +3 -0
  376. package/src/prompts/templates/system-sections.ts +51 -10
  377. package/src/providers/__tests__/inference.test.ts +2 -0
  378. package/src/providers/anthropic/client.ts +132 -5
  379. package/src/providers/call-site-routing.ts +24 -6
  380. package/src/providers/connection-resolution.ts +63 -13
  381. package/src/providers/fireworks/client.ts +20 -2
  382. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
  383. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
  384. package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
  385. package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
  386. package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
  387. package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
  388. package/src/providers/inference/adapter-factory.ts +24 -21
  389. package/src/providers/inference/auth.ts +15 -3
  390. package/src/providers/inference/backfill.ts +14 -1
  391. package/src/providers/inference/codex-token-refresh.ts +128 -0
  392. package/src/providers/inference/connections.ts +85 -5
  393. package/src/providers/inference/resolve-auth.ts +50 -5
  394. package/src/providers/model-catalog.ts +244 -242
  395. package/src/providers/model-intents.ts +3 -3
  396. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
  397. package/src/providers/openai/chat-completions-provider.ts +215 -25
  398. package/src/providers/openai/responses-provider.ts +9 -3
  399. package/src/providers/openrouter/client.ts +46 -4
  400. package/src/providers/platform-proxy/constants.ts +3 -4
  401. package/src/providers/provider-catalog-visibility.ts +3 -1
  402. package/src/providers/provider-send-message.ts +27 -12
  403. package/src/providers/registry.ts +30 -1
  404. package/src/providers/types.ts +25 -0
  405. package/src/runtime/__tests__/agent-wake.test.ts +214 -0
  406. package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
  407. package/src/runtime/agent-wake.ts +212 -57
  408. package/src/runtime/auth/route-policy.ts +20 -3
  409. package/src/runtime/background-job-runner.ts +26 -0
  410. package/src/runtime/channel-reply-delivery.ts +182 -47
  411. package/src/runtime/channel-retry-sweep.ts +141 -16
  412. package/src/runtime/http-server.ts +7 -16
  413. package/src/runtime/http-types.ts +7 -51
  414. package/src/runtime/pending-interactions.ts +51 -8
  415. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
  416. package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
  417. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +121 -5
  418. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
  419. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
  420. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
  421. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +271 -0
  422. package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
  423. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
  424. package/src/runtime/routes/approval-routes.ts +4 -1
  425. package/src/runtime/routes/channel-availability-routes.ts +5 -0
  426. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
  427. package/src/runtime/routes/consolidation-routes.ts +100 -0
  428. package/src/runtime/routes/content-source-routes.ts +78 -0
  429. package/src/runtime/routes/conversation-cli-routes.ts +146 -1
  430. package/src/runtime/routes/conversation-query-routes.ts +130 -12
  431. package/src/runtime/routes/conversation-routes.ts +288 -76
  432. package/src/runtime/routes/document-comments-routes.ts +287 -0
  433. package/src/runtime/routes/documents-routes.ts +33 -0
  434. package/src/runtime/routes/home-feed-routes.ts +6 -3
  435. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  436. package/src/runtime/routes/host-browser-routes.ts +8 -1
  437. package/src/runtime/routes/identity-routes.ts +21 -0
  438. package/src/runtime/routes/inbound-message-handler.ts +288 -58
  439. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
  440. package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
  441. package/src/runtime/routes/index.ts +14 -4
  442. package/src/runtime/routes/inference-provider-connection-routes.ts +192 -3
  443. package/src/runtime/routes/integrations/a2a.ts +294 -0
  444. package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
  445. package/src/runtime/routes/log-export-routes.ts +39 -0
  446. package/src/runtime/routes/memory-v2-routes.ts +217 -0
  447. package/src/runtime/routes/notification-routes.ts +19 -2
  448. package/src/runtime/routes/question-routes.ts +4 -1
  449. package/src/runtime/routes/sanity-routes.ts +159 -0
  450. package/src/runtime/routes/slack-channel-routes.ts +187 -0
  451. package/src/runtime/routes/subagents-routes.ts +41 -0
  452. package/src/runtime/services/conversation-serializer.ts +30 -4
  453. package/src/schedule/integration-status.ts +3 -1
  454. package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
  455. package/src/security/oauth2-device-code.ts +307 -0
  456. package/src/security/oauth2.ts +26 -9
  457. package/src/security/secure-keys.ts +5 -0
  458. package/src/skills/catalog-install.ts +6 -2
  459. package/src/subagent/manager.ts +2 -0
  460. package/src/tools/browser/__tests__/pinned-tabs.test.ts +80 -0
  461. package/src/tools/browser/browser-execution.ts +93 -0
  462. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
  463. package/src/tools/browser/cdp-client/__tests__/types.test.ts +1 -0
  464. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +10 -0
  465. package/src/tools/browser/cdp-client/extension-cdp-client.ts +15 -1
  466. package/src/tools/browser/cdp-client/factory.ts +87 -3
  467. package/src/tools/browser/cdp-client/local-cdp-client.ts +9 -0
  468. package/src/tools/browser/cdp-client/types.ts +36 -0
  469. package/src/tools/browser/pinned-tabs.ts +90 -0
  470. package/src/tools/document/document-comment-tool.test.ts +379 -0
  471. package/src/tools/document/document-comment-tool.ts +156 -0
  472. package/src/tools/document/document-tool.ts +128 -2
  473. package/src/tools/memory/register.ts +1 -9
  474. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
  475. package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
  476. package/src/tools/network/domain-normalize.ts +17 -0
  477. package/src/tools/network/web-fetch.ts +213 -64
  478. package/src/tools/network/web-search.ts +191 -66
  479. package/src/tools/registry.ts +2 -2
  480. package/src/tools/terminal/safe-env.ts +3 -2
  481. package/src/tools/tool-approval-handler.ts +19 -12
  482. package/src/tools/types.ts +41 -2
  483. package/src/tools/ui-surface/definitions.ts +3 -1
  484. package/src/types/onboarding-context.ts +4 -0
  485. package/src/util/__tests__/favicon.test.ts +84 -0
  486. package/src/util/favicon.ts +40 -0
  487. package/src/util/platform.ts +0 -5
  488. package/src/workspace/git-service.ts +75 -4
  489. package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
  490. package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
  491. package/src/workspace/migrations/registry.ts +4 -0
  492. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
  493. package/src/config/bundled-skills/document/SKILL.md +0 -54
  494. package/src/config/bundled-skills/document/TOOLS.json +0 -106
  495. package/src/daemon/seed-files.ts +0 -18
  496. package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
  497. package/src/runtime/guardian-action-conversation-turn.ts +0 -99
  498. package/src/runtime/routes/interface-routes.ts +0 -43
  499. /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
  500. /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
  501. /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
  502. /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
  503. /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Tests for the assistant_tool pass-through path in the notification
3
+ * decision engine. When a producer hands us a verbatim message via
4
+ * contextPayload.requestedMessage, the engine must skip the LLM call
5
+ * entirely and use the producer-supplied copy as-is.
6
+ */
7
+
8
+ import { describe, expect, mock, test } from "bun:test";
9
+
10
+ // ── Mocks (must precede imports from mocked modules) ──────────────────
11
+
12
+ mock.module("../../channels/config.js", () => ({
13
+ getDeliverableChannels: () => ["vellum", "telegram"],
14
+ }));
15
+
16
+ mock.module("../decisions-store.js", () => ({
17
+ createDecision: () => {},
18
+ }));
19
+
20
+ mock.module("../preference-summary.js", () => ({
21
+ getPreferenceSummary: () => undefined,
22
+ }));
23
+
24
+ mock.module("../conversation-candidates.js", () => ({
25
+ buildConversationCandidates: () => undefined,
26
+ serializeCandidatesForPrompt: () => undefined,
27
+ }));
28
+
29
+ mock.module("../../prompts/persona-resolver.js", () => ({
30
+ resolveGuardianPersona: () => null,
31
+ }));
32
+
33
+ mock.module("../../prompts/system-prompt.js", () => ({
34
+ buildCoreIdentityContext: () => null,
35
+ }));
36
+
37
+ mock.module("../../contacts/contact-store.js", () => ({
38
+ listGuardianChannels: () => null,
39
+ }));
40
+
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.
44
+ 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
+ },
50
+ createTimeout: () => ({
51
+ signal: new AbortController().signal,
52
+ cleanup: () => {},
53
+ }),
54
+ extractToolUse: () => null,
55
+ userMessage: (text: string) => ({ role: "user", content: text }),
56
+ }));
57
+
58
+ mock.module("../../util/logger.js", () => ({
59
+ getLogger: () =>
60
+ new Proxy({} as Record<string, unknown>, {
61
+ get: () => () => {},
62
+ }),
63
+ }));
64
+
65
+ // ── Imports (after all mocks) ─────────────────────────────────────────
66
+
67
+ import { enforceRoutingIntent, evaluateSignal } from "../decision-engine.js";
68
+ import type { NotificationSignal } from "../signal.js";
69
+ import type { NotificationChannel } from "../types.js";
70
+
71
+ // ── Helpers ───────────────────────────────────────────────────────────
72
+
73
+ function makeAssistantToolSignal(
74
+ overrides?: Partial<NotificationSignal>,
75
+ ): NotificationSignal {
76
+ return {
77
+ signalId: "sig-assistant-tool-test-1",
78
+ createdAt: Date.now(),
79
+ sourceChannel: "assistant_tool",
80
+ sourceContextId: "tool-call-1",
81
+ sourceEventName: "user.send_notification",
82
+ contextPayload: {
83
+ requestedMessage: "exact verbatim text here",
84
+ requestedTitle: "Custom Title",
85
+ },
86
+ attentionHints: {
87
+ requiresAction: false,
88
+ urgency: "low",
89
+ isAsyncBackground: false,
90
+ visibleInSourceNow: false,
91
+ },
92
+ ...overrides,
93
+ };
94
+ }
95
+
96
+ // ── Tests ─────────────────────────────────────────────────────────────
97
+
98
+ describe("assistant_tool pass-through in notification decision engine", () => {
99
+ test("uses producer-supplied title and body verbatim, no LLM call", async () => {
100
+ const signal = makeAssistantToolSignal();
101
+ const decision = await evaluateSignal(signal, [
102
+ "vellum",
103
+ "telegram",
104
+ ] as NotificationChannel[]);
105
+
106
+ expect(decision.shouldNotify).toBe(true);
107
+ expect(decision.selectedChannels).toContain("vellum");
108
+ expect(decision.renderedCopy.vellum?.body).toBe("exact verbatim text here");
109
+ expect(decision.renderedCopy.vellum?.title).toBe("Custom Title");
110
+ expect(decision.conversationActions?.vellum?.action).toBe("start_new");
111
+ expect(decision.reasoningSummary).toBe("assistant_tool pass-through");
112
+ expect(decision.fallbackUsed).toBe(false);
113
+ expect(decision.confidence).toBe(1.0);
114
+ expect(decision.dedupeKey).toBe(signal.signalId);
115
+ });
116
+
117
+ test("derives title from body when requestedTitle is not supplied", async () => {
118
+ const signal = makeAssistantToolSignal({
119
+ contextPayload: {
120
+ requestedMessage: "First sentence. Second sentence follows here.",
121
+ },
122
+ });
123
+ const decision = await evaluateSignal(signal, [
124
+ "vellum",
125
+ ] as NotificationChannel[]);
126
+
127
+ expect(decision.shouldNotify).toBe(true);
128
+ expect(decision.renderedCopy.vellum?.body).toBe(
129
+ "First sentence. Second sentence follows here.",
130
+ );
131
+ expect(decision.renderedCopy.vellum?.title).toBe("First sentence.");
132
+ expect(decision.reasoningSummary).toBe("assistant_tool pass-through");
133
+ });
134
+
135
+ test("critical urgency selects all available channels", async () => {
136
+ const signal = makeAssistantToolSignal({
137
+ attentionHints: {
138
+ requiresAction: true,
139
+ urgency: "critical",
140
+ isAsyncBackground: false,
141
+ visibleInSourceNow: false,
142
+ },
143
+ });
144
+ const availableChannels = ["vellum", "telegram"] as NotificationChannel[];
145
+ const decision = await evaluateSignal(signal, availableChannels);
146
+
147
+ expect(decision.shouldNotify).toBe(true);
148
+ expect(decision.selectedChannels).toEqual(
149
+ expect.arrayContaining(availableChannels),
150
+ );
151
+ expect(decision.selectedChannels.length).toBe(availableChannels.length);
152
+ expect(decision.renderedCopy.vellum?.body).toBe("exact verbatim text here");
153
+ expect(decision.renderedCopy.telegram?.body).toBe(
154
+ "exact verbatim text here",
155
+ );
156
+ expect(decision.conversationActions?.vellum?.action).toBe("start_new");
157
+ expect(decision.conversationActions?.telegram?.action).toBe("start_new");
158
+ });
159
+
160
+ test("threads contextPayload.deepLinkMetadata through to decision.deepLinkTarget", async () => {
161
+ const signal = makeAssistantToolSignal({
162
+ contextPayload: {
163
+ requestedMessage: "with deep link",
164
+ deepLinkMetadata: { route: "settings", anchor: "notifications" },
165
+ },
166
+ });
167
+ const decision = await evaluateSignal(signal, [
168
+ "vellum",
169
+ ] as NotificationChannel[]);
170
+
171
+ expect(decision.deepLinkTarget).toEqual({
172
+ route: "settings",
173
+ anchor: "notifications",
174
+ });
175
+ });
176
+
177
+ test("omits deepLinkTarget when deepLinkMetadata is not a plain object", async () => {
178
+ const signal = makeAssistantToolSignal({
179
+ contextPayload: {
180
+ requestedMessage: "no deep link",
181
+ deepLinkMetadata: ["not", "a", "plain", "object"],
182
+ },
183
+ });
184
+ const decision = await evaluateSignal(signal, [
185
+ "vellum",
186
+ ] as NotificationChannel[]);
187
+
188
+ expect(decision.deepLinkTarget).toBeUndefined();
189
+ });
190
+
191
+ test("preferredChannels adds to the default channel set (additive, not replacement)", async () => {
192
+ const signal = makeAssistantToolSignal({
193
+ contextPayload: {
194
+ requestedMessage: "also push to telegram",
195
+ preferredChannels: ["telegram"],
196
+ },
197
+ });
198
+ const decision = await evaluateSignal(signal, [
199
+ "vellum",
200
+ "telegram",
201
+ ] as NotificationChannel[]);
202
+
203
+ // Vellum (canonical inbox) stays in selectedChannels; telegram is
204
+ // added on top. `--preferred-channels` is additive push, never a
205
+ // replacement for the inbox.
206
+ expect(decision.selectedChannels).toContain("vellum");
207
+ expect(decision.selectedChannels).toContain("telegram");
208
+ expect(decision.selectedChannels.length).toBe(2);
209
+ expect(decision.renderedCopy.vellum?.body).toBe("also push to telegram");
210
+ expect(decision.renderedCopy.telegram?.body).toBe("also push to telegram");
211
+ });
212
+
213
+ test("urgent + preferredChannels keeps urgent's full broadcast intact", async () => {
214
+ const signal = makeAssistantToolSignal({
215
+ contextPayload: {
216
+ requestedMessage: "urgent broadcast",
217
+ requestedTitle: "Heads up",
218
+ preferredChannels: ["telegram"],
219
+ },
220
+ attentionHints: {
221
+ requiresAction: true,
222
+ urgency: "critical",
223
+ isAsyncBackground: false,
224
+ visibleInSourceNow: false,
225
+ },
226
+ });
227
+ const available = ["vellum", "telegram", "slack"] as NotificationChannel[];
228
+ const decision = await evaluateSignal(signal, available);
229
+
230
+ // Urgent broadcasts to every available channel; the additive union
231
+ // with preferredChannels is idempotent (telegram already included).
232
+ expect(decision.selectedChannels).toEqual(
233
+ expect.arrayContaining(available),
234
+ );
235
+ expect(decision.selectedChannels.length).toBe(available.length);
236
+ for (const ch of available) {
237
+ expect(decision.renderedCopy[ch]?.body).toBe("urgent broadcast");
238
+ expect(decision.renderedCopy[ch]?.title).toBe("Heads up");
239
+ }
240
+ });
241
+
242
+ test("routing-intent expansion to all_channels preserves verbatim copy on added channels", async () => {
243
+ const signal = makeAssistantToolSignal({
244
+ contextPayload: {
245
+ requestedMessage: "verbatim broadcast body",
246
+ requestedTitle: "verbatim broadcast title",
247
+ },
248
+ routingIntent: "all_channels",
249
+ });
250
+ const connected = ["vellum", "telegram"] as NotificationChannel[];
251
+ const decision = await evaluateSignal(signal, connected);
252
+ const enforced = enforceRoutingIntent(
253
+ decision,
254
+ "all_channels",
255
+ connected,
256
+ "assistant_tool",
257
+ );
258
+
259
+ expect(enforced.selectedChannels).toEqual(
260
+ expect.arrayContaining(["vellum", "telegram"]),
261
+ );
262
+ for (const ch of enforced.selectedChannels) {
263
+ expect(enforced.renderedCopy[ch]?.body).toBe("verbatim broadcast body");
264
+ expect(enforced.renderedCopy[ch]?.title).toBe("verbatim broadcast title");
265
+ }
266
+ });
267
+
268
+ test("preferredChannels falls back to default channel set when no overlap with availableChannels", async () => {
269
+ const signal = makeAssistantToolSignal({
270
+ contextPayload: {
271
+ requestedMessage: "fyi",
272
+ preferredChannels: ["disconnected_channel"],
273
+ },
274
+ });
275
+ const decision = await evaluateSignal(signal, [
276
+ "vellum",
277
+ "telegram",
278
+ ] as NotificationChannel[]);
279
+
280
+ expect(decision.selectedChannels).toEqual(["vellum"]);
281
+ expect(decision.renderedCopy.vellum?.body).toBe("fyi");
282
+ });
283
+ });
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Tests for the deterministic pre-send checks.
3
+ *
4
+ * Focus: the rendered-copy quality check that suppresses notifications
5
+ * with empty bodies or bodies that leak the raw source event name
6
+ * (the `buildGenericCopy` fallback path).
7
+ */
8
+
9
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
10
+
11
+ mock.module("../../util/logger.js", () => ({
12
+ getLogger: () =>
13
+ new Proxy({} as Record<string, unknown>, {
14
+ get: () => () => {},
15
+ }),
16
+ truncateForLog: (value: string) => value,
17
+ }));
18
+
19
+ import { getDb } from "../../memory/db-connection.js";
20
+ import { initializeDb } from "../../memory/db-init.js";
21
+ import { notificationEvents } from "../../memory/schema.js";
22
+ import {
23
+ type DeterministicCheckContext,
24
+ runDeterministicChecks,
25
+ } from "../deterministic-checks.js";
26
+ import type { NotificationSignal } from "../signal.js";
27
+ import type { NotificationDecision } from "../types.js";
28
+
29
+ initializeDb();
30
+
31
+ beforeEach(() => {
32
+ getDb().delete(notificationEvents).run();
33
+ });
34
+
35
+ function makeSignal(
36
+ overrides?: Partial<NotificationSignal>,
37
+ ): NotificationSignal {
38
+ return {
39
+ signalId: `sig-${crypto.randomUUID()}`,
40
+ createdAt: Date.now(),
41
+ sourceChannel: "scheduler",
42
+ sourceContextId: "ctx-1",
43
+ sourceEventName: "schedule.notify",
44
+ contextPayload: {},
45
+ attentionHints: {
46
+ requiresAction: false,
47
+ urgency: "low",
48
+ isAsyncBackground: false,
49
+ visibleInSourceNow: false,
50
+ },
51
+ ...overrides,
52
+ };
53
+ }
54
+
55
+ function makeDecision(
56
+ overrides?: Partial<NotificationDecision>,
57
+ ): NotificationDecision {
58
+ return {
59
+ shouldNotify: true,
60
+ selectedChannels: ["vellum"],
61
+ reasoningSummary: "test",
62
+ renderedCopy: {
63
+ vellum: { title: "Reminder", body: "Time to drink water" },
64
+ },
65
+ dedupeKey: `dk-${crypto.randomUUID()}`,
66
+ confidence: 0.9,
67
+ fallbackUsed: false,
68
+ ...overrides,
69
+ };
70
+ }
71
+
72
+ const context: DeterministicCheckContext = {
73
+ connectedChannels: ["vellum"],
74
+ };
75
+
76
+ describe("checkRenderedCopyQuality (via runDeterministicChecks)", () => {
77
+ test("passes when body is real non-empty text", async () => {
78
+ const result = await runDeterministicChecks(
79
+ makeSignal(),
80
+ makeDecision(),
81
+ context,
82
+ );
83
+ expect(result.passed).toBe(true);
84
+ });
85
+
86
+ test("fails when body is empty", async () => {
87
+ const decision = makeDecision({
88
+ renderedCopy: {
89
+ vellum: { title: "Reminder", body: "" },
90
+ },
91
+ });
92
+ const result = await runDeterministicChecks(
93
+ makeSignal(),
94
+ decision,
95
+ context,
96
+ );
97
+ expect(result.passed).toBe(false);
98
+ expect(result.reason).toContain("empty");
99
+ });
100
+
101
+ test("fails when body is whitespace only", async () => {
102
+ const decision = makeDecision({
103
+ renderedCopy: {
104
+ vellum: { title: "Reminder", body: " \n " },
105
+ },
106
+ });
107
+ const result = await runDeterministicChecks(
108
+ makeSignal(),
109
+ decision,
110
+ context,
111
+ );
112
+ expect(result.passed).toBe(false);
113
+ expect(result.reason).toContain("empty");
114
+ });
115
+
116
+ test("fails when body is the raw source event name", async () => {
117
+ const signal = makeSignal({ sourceEventName: "user.send_notification" });
118
+ const decision = makeDecision({
119
+ renderedCopy: {
120
+ vellum: { title: "Reminder", body: "user.send_notification" },
121
+ },
122
+ });
123
+ const result = await runDeterministicChecks(signal, decision, context);
124
+ expect(result.passed).toBe(false);
125
+ expect(result.reason).toContain("fallback leak");
126
+ });
127
+
128
+ test("fails when body matches the normalized source event name", async () => {
129
+ const signal = makeSignal({ sourceEventName: "user.send_notification" });
130
+ const decision = makeDecision({
131
+ renderedCopy: {
132
+ vellum: { title: "Reminder", body: "user send notification" },
133
+ },
134
+ });
135
+ const result = await runDeterministicChecks(signal, decision, context);
136
+ expect(result.passed).toBe(false);
137
+ expect(result.reason).toContain("fallback leak");
138
+ });
139
+
140
+ test("passes when channel was appended post-decision (urgency-forced vellum prepend)", async () => {
141
+ // Regression: emit-signal.ts prepends `vellum` to selectedChannels for
142
+ // high/critical urgency without populating renderedCopy.vellum. The
143
+ // broadcaster's composeFallbackCopy rescue handles those channels at
144
+ // delivery time, so the deterministic check must not fail-closed here.
145
+ const signal = makeSignal({
146
+ attentionHints: {
147
+ requiresAction: false,
148
+ urgency: "high",
149
+ isAsyncBackground: false,
150
+ visibleInSourceNow: false,
151
+ },
152
+ });
153
+ const decision = makeDecision({
154
+ selectedChannels: ["vellum", "telegram"],
155
+ renderedCopy: {
156
+ telegram: { title: "Reminder", body: "Time to drink water" },
157
+ },
158
+ });
159
+ const result = await runDeterministicChecks(signal, decision, {
160
+ connectedChannels: ["vellum", "telegram"],
161
+ });
162
+ expect(result.passed).toBe(true);
163
+ });
164
+
165
+ test("passes when enforceRoutingIntent expanded channels post-decision", async () => {
166
+ // Regression: enforceRoutingIntent can expand selectedChannels to
167
+ // all_channels / multi_channel without populating renderedCopy for the
168
+ // added channels. Broadcaster fallback covers them — check must allow.
169
+ const decision = makeDecision({
170
+ selectedChannels: ["vellum", "telegram", "slack"],
171
+ renderedCopy: {
172
+ vellum: { title: "Reminder", body: "Time to drink water" },
173
+ },
174
+ });
175
+ const result = await runDeterministicChecks(makeSignal(), decision, {
176
+ connectedChannels: ["vellum", "telegram", "slack"],
177
+ });
178
+ expect(result.passed).toBe(true);
179
+ });
180
+
181
+ test("still validates body quality for channels with rendered copy", async () => {
182
+ // Even when some channels lack copy (broadcaster fallback territory),
183
+ // channels that DO have copy must still pass the empty/event-name checks.
184
+ const signal = makeSignal({ sourceEventName: "user.send_notification" });
185
+ const decision = makeDecision({
186
+ selectedChannels: ["vellum", "telegram"],
187
+ renderedCopy: {
188
+ telegram: { title: "Reminder", body: "user.send_notification" },
189
+ },
190
+ });
191
+ const result = await runDeterministicChecks(signal, decision, {
192
+ connectedChannels: ["vellum", "telegram"],
193
+ });
194
+ expect(result.passed).toBe(false);
195
+ expect(result.reason).toContain("fallback leak");
196
+ });
197
+
198
+ test("fails when no selected channel has copy and fallback body is empty", async () => {
199
+ // Silent-no-delivery guard: if every selected channel is missing from
200
+ // renderedCopy AND the broadcaster's composeFallbackCopy can't produce
201
+ // a usable body (no template for sourceEventName → buildGenericCopy
202
+ // returns body=""), the gate must fail-closed rather than letting
203
+ // dispatchDecision report 0/N sent.
204
+ const signal = makeSignal({ sourceEventName: "user.send_notification" });
205
+ const decision = makeDecision({
206
+ selectedChannels: ["vellum"],
207
+ renderedCopy: {},
208
+ });
209
+ const result = await runDeterministicChecks(signal, decision, context);
210
+ expect(result.passed).toBe(false);
211
+ expect(result.reason).toContain("fallback");
212
+ });
213
+
214
+ test("passes when no selected channel has copy but fallback yields a usable body", async () => {
215
+ // schedule.notify has a copy-composer template that produces a usable
216
+ // body even with empty payload — the broadcaster's fallback path will
217
+ // deliver, so the deterministic gate must allow it through.
218
+ const signal = makeSignal({ sourceEventName: "schedule.notify" });
219
+ const decision = makeDecision({
220
+ selectedChannels: ["vellum"],
221
+ renderedCopy: {},
222
+ });
223
+ const result = await runDeterministicChecks(signal, decision, context);
224
+ expect(result.passed).toBe(true);
225
+ });
226
+
227
+ test("passes when shouldNotify is false regardless of copy contents", async () => {
228
+ const signal = makeSignal({ sourceEventName: "user.send_notification" });
229
+ const decision = makeDecision({
230
+ shouldNotify: false,
231
+ // Empty body + event-name body would both fail the copy check if
232
+ // shouldNotify were true. Short-circuit must skip the check.
233
+ renderedCopy: {
234
+ vellum: { title: "", body: "" },
235
+ },
236
+ });
237
+ const result = await runDeterministicChecks(signal, decision, context);
238
+ expect(result.passed).toBe(true);
239
+ });
240
+
241
+ test("passes assistant_tool pass-through even when body matches normalized event name", async () => {
242
+ // The pass-through path produces verbatim user-supplied body text.
243
+ // A coincidental match with the source event name is the user's
244
+ // intent, not a fallback leak — the check must not suppress it.
245
+ const signal = makeSignal({
246
+ sourceChannel: "assistant_tool",
247
+ sourceEventName: "assistant.share",
248
+ });
249
+ const decision = makeDecision({
250
+ reasoningSummary: "assistant_tool pass-through",
251
+ renderedCopy: {
252
+ vellum: { title: "Assistant share", body: "assistant share" },
253
+ },
254
+ });
255
+ const result = await runDeterministicChecks(signal, decision, context);
256
+ expect(result.passed).toBe(true);
257
+ });
258
+
259
+ test("fails assistant_tool pass-through with empty body (empty-body branch still fires)", async () => {
260
+ const signal = makeSignal({ sourceChannel: "assistant_tool" });
261
+ const decision = makeDecision({
262
+ reasoningSummary: "assistant_tool pass-through",
263
+ renderedCopy: {
264
+ vellum: { title: "Reminder", body: "" },
265
+ },
266
+ });
267
+ const result = await runDeterministicChecks(signal, decision, context);
268
+ expect(result.passed).toBe(false);
269
+ expect(result.reason).toContain("empty");
270
+ });
271
+
272
+ test("still fails non-pass-through decision when body matches event name", async () => {
273
+ // Regression guard: the pass-through short-circuit must not weaken
274
+ // the check for LLM/fallback paths.
275
+ const signal = makeSignal({ sourceEventName: "user.send_notification" });
276
+ const decision = makeDecision({
277
+ reasoningSummary: "llm classification",
278
+ renderedCopy: {
279
+ vellum: { title: "Reminder", body: "user.send_notification" },
280
+ },
281
+ });
282
+ const result = await runDeterministicChecks(signal, decision, context);
283
+ expect(result.passed).toBe(false);
284
+ expect(result.reason).toContain("fallback leak");
285
+ });
286
+ });
@@ -144,6 +144,7 @@ describe("emitNotificationSignal home-feed wire-up", () => {
144
144
  sourceEventName: "schedule.notify",
145
145
  sourceChannel: "scheduler",
146
146
  sourceContextId: "conv-source-1",
147
+ contextPayload: { title: "Background job done" },
147
148
  attentionHints: {
148
149
  requiresAction: false,
149
150
  urgency: "medium",
@@ -158,7 +159,10 @@ describe("emitNotificationSignal home-feed wire-up", () => {
158
159
  expect(appended.id).toBe(`notif:${result.signalId}`);
159
160
  expect(appended.title).toBe("Background job done");
160
161
  expect(appended.summary).toBe("Summary of what happened.");
161
- expect(appended.conversationId).toBe("conv-vellum-1");
162
+ // The feed item's conversationId points to the source conversation
163
+ // that emitted the signal, not the conversation the notification
164
+ // pipeline spawned to handle it.
165
+ expect(appended.conversationId).toBe("conv-source-1");
162
166
  });
163
167
 
164
168
  test("interactive standard conversation does NOT trigger appendFeedItem", async () => {