@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
@@ -2,10 +2,7 @@ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
3
  import type { FeedItem } from "../../home/feed-types.js";
4
4
  import type { NotificationSignal } from "../signal.js";
5
- import type {
6
- NotificationDecision,
7
- NotificationDeliveryResult,
8
- } from "../types.js";
5
+ import type { NotificationDecision } from "../types.js";
9
6
 
10
7
  // ── Module mocks ───────────────────────────────────────────────────────
11
8
  //
@@ -83,9 +80,11 @@ beforeEach(() => {
83
80
  });
84
81
 
85
82
  describe("writeHomeFeedItemForSignal", () => {
86
- test("background conversation signal writes a feed item with rendered copy", async () => {
83
+ test("background conversation signal writes a feed item with payload title + rendered body", async () => {
87
84
  conversationRow = { conversationType: "background" };
88
- const signal = makeSignal();
85
+ const signal = makeSignal({
86
+ contextPayload: { title: "Background job done" },
87
+ });
89
88
  const decision = makeDecision({
90
89
  renderedCopy: {
91
90
  vellum: {
@@ -95,7 +94,7 @@ describe("writeHomeFeedItemForSignal", () => {
95
94
  },
96
95
  });
97
96
 
98
- const item = await writeHomeFeedItemForSignal(signal, decision, []);
97
+ const item = await writeHomeFeedItemForSignal(signal, decision);
99
98
 
100
99
  expect(conversationLookups).toEqual(["conv-source-1"]);
101
100
  expect(item).not.toBeNull();
@@ -112,6 +111,10 @@ describe("writeHomeFeedItemForSignal", () => {
112
111
  expect(appended.title).toBe("Background job done");
113
112
  expect(appended.summary).toBe("Summary of what happened.");
114
113
  expect(appended.urgency).toBe("medium");
114
+ // The button in the home detail panel navigates to the source
115
+ // conversation that emitted the notification, not the conversation the
116
+ // notification pipeline spawned to handle it.
117
+ expect(appended.conversationId).toBe("conv-source-1");
115
118
  expect(typeof appended.timestamp).toBe("string");
116
119
  expect(appended.createdAt).toBe(appended.timestamp);
117
120
  });
@@ -127,15 +130,16 @@ describe("writeHomeFeedItemForSignal", () => {
127
130
  },
128
131
  });
129
132
 
130
- const item = await writeHomeFeedItemForSignal(signal, makeDecision(), []);
133
+ const item = await writeHomeFeedItemForSignal(signal, makeDecision());
131
134
 
132
135
  expect(item).toBeNull();
133
136
  expect(appendCalls).toHaveLength(0);
134
137
  });
135
138
 
136
139
  test("isAsyncBackground hint writes even when sourceContextId does not resolve", async () => {
137
- // No conversation row matches; the conversation lookup is bypassed
138
- // entirely because the hint short-circuits the filter.
140
+ // Source lookup throws treated as non-navigable, so the item lands
141
+ // without a `conversationId` and the "Go to Thread" button hides on the
142
+ // client. The async-background hint still forces the mirror.
139
143
  conversationLookupShouldThrow = true;
140
144
  const signal = makeSignal({
141
145
  sourceContextId: "not-a-conversation-id",
@@ -146,54 +150,535 @@ describe("writeHomeFeedItemForSignal", () => {
146
150
  visibleInSourceNow: false,
147
151
  },
148
152
  });
153
+ const decision = makeDecision({
154
+ renderedCopy: {
155
+ vellum: { title: "Async title", body: "Async body" },
156
+ },
157
+ });
149
158
 
150
- const item = await writeHomeFeedItemForSignal(signal, makeDecision(), []);
159
+ const item = await writeHomeFeedItemForSignal(signal, decision);
151
160
 
152
161
  expect(item).not.toBeNull();
153
162
  expect(appendCalls).toHaveLength(1);
154
163
  expect(appendCalls[0]!.urgency).toBe("high");
155
- // The async-background short-circuit must not consult the conversation store.
156
- expect(conversationLookups).toHaveLength(0);
164
+ expect(appendCalls[0]!.conversationId).toBeUndefined();
165
+ expect(conversationLookups).toEqual(["not-a-conversation-id"]);
157
166
  });
158
167
 
159
- test("vellum delivery result conversationId propagates onto the feed item", async () => {
160
- conversationRow = { conversationType: "background" };
161
- const signal = makeSignal();
162
- const deliveryResults: NotificationDeliveryResult[] = [
163
- {
164
- channel: "telegram",
165
- destination: "chat-1",
166
- status: "sent",
167
- conversationId: "conv-telegram-1",
168
+ test("assistant_tool source mirrors to the home feed even without a background conversation or async hint", async () => {
169
+ // Regression: the `notifications send` CLI/skill emits with
170
+ // `sourceChannel: "assistant_tool"`, a synthetic `cli-<ts>` source
171
+ // context id that does not resolve to a conversation, and
172
+ // `isAsyncBackground: false`. The assistant_tool channel forces the
173
+ // mirror; the source-id lookup misses so the item lands without a
174
+ // `conversationId` and the "Go to Thread" button hides on the client.
175
+ conversationRow = null;
176
+ const signal = makeSignal({
177
+ sourceChannel: "assistant_tool",
178
+ sourceEventName: "assistant.share",
179
+ sourceContextId: "cli-12345",
180
+ contextPayload: { title: "Shared from CLI" },
181
+ attentionHints: {
182
+ requiresAction: false,
183
+ urgency: "low",
184
+ isAsyncBackground: false,
185
+ visibleInSourceNow: false,
186
+ },
187
+ });
188
+ const decision = makeDecision({
189
+ renderedCopy: {
190
+ vellum: { title: "Shared from CLI", body: "Body from CLI share" },
168
191
  },
169
- {
170
- channel: "vellum",
171
- destination: "vellum-client",
172
- status: "sent",
173
- conversationId: "conv-vellum-1",
192
+ });
193
+
194
+ const item = await writeHomeFeedItemForSignal(signal, decision);
195
+
196
+ expect(item).not.toBeNull();
197
+ expect(appendCalls).toHaveLength(1);
198
+ expect(appendCalls[0]!.title).toBe("Shared from CLI");
199
+ expect(appendCalls[0]!.noteworthy).toBe(true);
200
+ expect(appendCalls[0]!.conversationId).toBeUndefined();
201
+ expect(conversationLookups).toEqual(["cli-12345"]);
202
+ });
203
+
204
+ test("source conversation id does not propagate when the lookup misses", async () => {
205
+ // When `sourceContextId` does not resolve to a real conversation row
206
+ // (e.g. scheduler job id, watcher event id), the item is still mirrored
207
+ // via the `isAsyncBackground` hint but `conversationId` stays undefined
208
+ // so the client hides the "Go to Thread" affordance.
209
+ conversationRow = null;
210
+ const signal = makeSignal({
211
+ sourceContextId: "scheduler-job-42",
212
+ attentionHints: {
213
+ requiresAction: false,
214
+ urgency: "medium",
215
+ isAsyncBackground: true,
216
+ visibleInSourceNow: false,
217
+ },
218
+ });
219
+ const decision = makeDecision({
220
+ renderedCopy: {
221
+ vellum: { title: "Routed title", body: "Routed body" },
174
222
  },
175
- ];
223
+ });
224
+
225
+ const item = await writeHomeFeedItemForSignal(signal, decision);
226
+
227
+ expect(item?.conversationId).toBeUndefined();
228
+ expect(appendCalls[0]!.conversationId).toBeUndefined();
229
+ expect(conversationLookups).toEqual(["scheduler-job-42"]);
230
+ });
231
+
232
+ test("falls back to the paired delivery conversation when sourceContextId does not resolve", async () => {
233
+ // Regression: producers whose `sourceContextId` is a sentinel string
234
+ // (heartbeat startup `"heartbeat"`, credential health `connectionId`,
235
+ // watcher `watcher-<ts>`, scheduler retries-exhausted `jobId`, sweep
236
+ // job id) never resolve via `getConversation`. The notification
237
+ // broadcaster pairs each vellum delivery with a real conversation
238
+ // before the home-feed write runs, so the caller threads that paired
239
+ // id through as the fallback — the "Go to Convo" button now points at
240
+ // the conversation the notification was actually delivered into.
241
+ conversationRow = null;
242
+ const signal = makeSignal({
243
+ sourceChannel: "assistant_tool",
244
+ sourceEventName: "assistant.share",
245
+ sourceContextId: "watcher-1700000000",
246
+ contextPayload: { title: "Watcher alert" },
247
+ attentionHints: {
248
+ requiresAction: false,
249
+ urgency: "medium",
250
+ isAsyncBackground: true,
251
+ visibleInSourceNow: false,
252
+ },
253
+ });
254
+ const decision = makeDecision({
255
+ renderedCopy: {
256
+ vellum: { title: "Watcher alert", body: "Watcher body" },
257
+ },
258
+ });
176
259
 
177
260
  const item = await writeHomeFeedItemForSignal(
178
261
  signal,
179
- makeDecision(),
180
- deliveryResults,
262
+ decision,
263
+ "paired-delivery-conv-id",
181
264
  );
182
265
 
183
- expect(item?.conversationId).toBe("conv-vellum-1");
184
- expect(appendCalls[0]!.conversationId).toBe("conv-vellum-1");
266
+ expect(item).not.toBeNull();
267
+ expect(appendCalls).toHaveLength(1);
268
+ expect(appendCalls[0]!.conversationId).toBe("paired-delivery-conv-id");
185
269
  });
186
270
 
187
- test("falls back to sourceEventName when no rendered copy or payload title is present", async () => {
271
+ test("source conversation id wins over the paired delivery fallback when both are available", async () => {
272
+ // When the producer's `sourceContextId` already points at a real
273
+ // conversation (the canonical "where the work happened"), prefer it
274
+ // over the paired delivery — the fallback is only meant to fill the
275
+ // gap for sentinel-id producers.
276
+ conversationRow = { conversationType: "background" };
277
+ const signal = makeSignal({
278
+ contextPayload: { title: "Background job done" },
279
+ });
280
+ const decision = makeDecision({
281
+ renderedCopy: {
282
+ vellum: { title: "Background job done", body: "Summary." },
283
+ },
284
+ });
285
+
286
+ const item = await writeHomeFeedItemForSignal(
287
+ signal,
288
+ decision,
289
+ "paired-delivery-conv-id",
290
+ );
291
+
292
+ expect(item).not.toBeNull();
293
+ expect(appendCalls[0]!.conversationId).toBe("conv-source-1");
294
+ });
295
+
296
+ test("returns null and does not write when no rendered copy or payload title/body is present", async () => {
188
297
  conversationRow = { conversationType: "scheduled" };
189
298
  const signal = makeSignal({
190
299
  sourceEventName: "watcher.notification",
191
300
  contextPayload: {},
192
301
  });
193
302
 
194
- const item = await writeHomeFeedItemForSignal(signal, makeDecision(), []);
303
+ const item = await writeHomeFeedItemForSignal(signal, makeDecision());
304
+
305
+ expect(item).toBeNull();
306
+ expect(appendCalls).toHaveLength(0);
307
+ });
308
+
309
+ test("returns null when only the title is available but the summary would fall back to event name", async () => {
310
+ conversationRow = { conversationType: "background" };
311
+ const signal = makeSignal({
312
+ sourceEventName: "example.event",
313
+ contextPayload: { title: "Real title" },
314
+ });
315
+
316
+ const item = await writeHomeFeedItemForSignal(signal, makeDecision());
317
+
318
+ expect(item).toBeNull();
319
+ expect(appendCalls).toHaveLength(0);
320
+ });
321
+
322
+ test("writes a feed item with undefined title when only the body is available", async () => {
323
+ // Regression: when `notifications send` is called without `--title`, the
324
+ // notification pipeline must not manufacture a title (the LLM's rendered
325
+ // copy echoes the body into `renderedCopy.title`). Leave `title`
326
+ // undefined so renderers fall back to `summary` instead of stuttering.
327
+ conversationRow = { conversationType: "background" };
328
+ const signal = makeSignal({
329
+ sourceEventName: "example.event",
330
+ contextPayload: { body: "Real body" },
331
+ });
332
+
333
+ const item = await writeHomeFeedItemForSignal(signal, makeDecision());
334
+
335
+ expect(item).not.toBeNull();
336
+ expect(appendCalls).toHaveLength(1);
337
+ expect(appendCalls[0]!.title).toBeUndefined();
338
+ expect(appendCalls[0]!.summary).toBe("Real body");
339
+ });
340
+
341
+ test("ignores LLM-rendered title when no payload title was supplied", async () => {
342
+ // The LLM often echoes the body verbatim into `renderedCopy.title` when
343
+ // the source didn't pass one. The home-feed writer must NOT promote that
344
+ // echo into the feed item — only an explicit source title is honored.
345
+ conversationRow = { conversationType: "background" };
346
+ const signal = makeSignal({
347
+ sourceEventName: "example.event",
348
+ contextPayload: { body: "Real body" },
349
+ });
350
+ const decision = makeDecision({
351
+ renderedCopy: {
352
+ vellum: {
353
+ title: "Real body",
354
+ body: "Real body",
355
+ },
356
+ },
357
+ });
358
+
359
+ const item = await writeHomeFeedItemForSignal(signal, decision);
360
+
361
+ expect(item).not.toBeNull();
362
+ expect(appendCalls).toHaveLength(1);
363
+ expect(appendCalls[0]!.title).toBeUndefined();
364
+ expect(appendCalls[0]!.summary).toBe("Real body");
365
+ });
366
+
367
+ test("treats whitespace-only rendered copy and payload values as missing and returns null", async () => {
368
+ conversationRow = { conversationType: "background" };
369
+ const signal = makeSignal({
370
+ sourceEventName: "example.event",
371
+ contextPayload: { title: " ", body: "\t\n" },
372
+ });
373
+ const decision = makeDecision({
374
+ renderedCopy: {
375
+ vellum: { title: " ", body: " " },
376
+ },
377
+ });
378
+
379
+ const item = await writeHomeFeedItemForSignal(signal, decision);
380
+
381
+ expect(item).toBeNull();
382
+ expect(appendCalls).toHaveLength(0);
383
+ });
384
+
385
+ test("falls back to a non-vellum channel's rendered copy when vellum copy is absent", async () => {
386
+ // Regression: when `preferredChannels` narrows an assistant_tool signal
387
+ // to a non-vellum channel (e.g. telegram), the broadcaster ships real
388
+ // copy on that channel but `renderedCopy.vellum` is undefined. The
389
+ // guard must still write to the home feed using the first available
390
+ // rendered copy entry rather than skipping silently.
391
+ conversationRow = { conversationType: "background" };
392
+ const signal = makeSignal({
393
+ sourceChannel: "assistant_tool",
394
+ sourceEventName: "assistant.share",
395
+ sourceContextId: "cli-12345",
396
+ contextPayload: { title: "Telegram title" },
397
+ });
398
+ const decision = makeDecision({
399
+ selectedChannels: ["telegram"],
400
+ renderedCopy: {
401
+ telegram: { title: "Telegram title", body: "Telegram body" },
402
+ },
403
+ });
404
+
405
+ const item = await writeHomeFeedItemForSignal(signal, decision);
406
+
407
+ expect(item).not.toBeNull();
408
+ expect(appendCalls).toHaveLength(1);
409
+ expect(appendCalls[0]!.title).toBe("Telegram title");
410
+ expect(appendCalls[0]!.summary).toBe("Telegram body");
411
+ });
412
+
413
+ test("ignores rendered copy for channels not in selectedChannels", async () => {
414
+ // Regression: routing-intent enforcement can prune selectedChannels
415
+ // without pruning renderedCopy, leaving copy entries for channels that
416
+ // were never delivered. The fallback must only consider channels that
417
+ // actually shipped — otherwise an unselected channel's copy can land in
418
+ // Home in place of the selected channel's copy.
419
+ conversationRow = { conversationType: "background" };
420
+ const signal = makeSignal({
421
+ sourceChannel: "assistant_tool",
422
+ sourceEventName: "assistant.share",
423
+ sourceContextId: "cli-12345",
424
+ contextPayload: { title: "Telegram title" },
425
+ });
426
+ const decision = makeDecision({
427
+ selectedChannels: ["telegram"],
428
+ renderedCopy: {
429
+ slack: {
430
+ title: "Slack title (unselected)",
431
+ body: "Slack body (unselected)",
432
+ },
433
+ telegram: { title: "Telegram title", body: "Telegram body" },
434
+ },
435
+ });
436
+
437
+ const item = await writeHomeFeedItemForSignal(signal, decision);
438
+
439
+ expect(item).not.toBeNull();
440
+ expect(appendCalls).toHaveLength(1);
441
+ expect(appendCalls[0]!.title).toBe("Telegram title");
442
+ expect(appendCalls[0]!.summary).toBe("Telegram body");
443
+ });
444
+
445
+ test("skips fallback when only unselected channels have rendered copy", async () => {
446
+ // Regression: if every renderedCopy entry is for a channel that was
447
+ // pruned from selectedChannels, treat it as no copy at all rather than
448
+ // surfacing the stale entry.
449
+ conversationRow = { conversationType: "background" };
450
+ const signal = makeSignal({
451
+ sourceChannel: "assistant_tool",
452
+ sourceEventName: "assistant.share",
453
+ sourceContextId: "cli-12345",
454
+ });
455
+ const decision = makeDecision({
456
+ selectedChannels: ["telegram"],
457
+ renderedCopy: {
458
+ slack: {
459
+ title: "Slack title (unselected)",
460
+ body: "Slack body (unselected)",
461
+ },
462
+ },
463
+ });
464
+
465
+ const item = await writeHomeFeedItemForSignal(signal, decision);
466
+
467
+ expect(item).toBeNull();
468
+ expect(appendCalls).toHaveLength(0);
469
+ });
470
+
471
+ test("falls back to requestedTitle/requestedMessage payload keys", async () => {
472
+ // Regression: the `notifications send` CLI surface stores the
473
+ // user-supplied copy on the signal payload under `requestedTitle` and
474
+ // `requestedMessage`. If the decision strips renderedCopy.vellum (e.g.
475
+ // routed only to a non-vellum channel that also lacks renderedCopy),
476
+ // the home-feed guard must still recover the copy from the payload.
477
+ conversationRow = { conversationType: "background" };
478
+ const signal = makeSignal({
479
+ sourceChannel: "assistant_tool",
480
+ sourceEventName: "assistant.share",
481
+ sourceContextId: "cli-12345",
482
+ contextPayload: {
483
+ requestedTitle: "Requested title",
484
+ requestedMessage: "Requested message body",
485
+ },
486
+ });
487
+
488
+ const item = await writeHomeFeedItemForSignal(signal, makeDecision());
489
+
490
+ expect(item).not.toBeNull();
491
+ expect(appendCalls).toHaveLength(1);
492
+ expect(appendCalls[0]!.title).toBe("Requested title");
493
+ expect(appendCalls[0]!.summary).toBe("Requested message body");
494
+ });
495
+
496
+ test("uses payload title/body when rendered copy is absent", async () => {
497
+ conversationRow = { conversationType: "background" };
498
+ const signal = makeSignal({
499
+ sourceEventName: "watcher.notification",
500
+ contextPayload: { title: "Payload title", body: "Payload body" },
501
+ });
502
+
503
+ const item = await writeHomeFeedItemForSignal(signal, makeDecision());
504
+
505
+ expect(item).not.toBeNull();
506
+ expect(item?.title).toBe("Payload title");
507
+ expect(item?.summary).toBe("Payload body");
508
+ expect(appendCalls).toHaveLength(1);
509
+ });
510
+
511
+ // ── noteworthy derivation ────────────────────────────────────────────
512
+
513
+ test("assistant_tool source marks the feed item noteworthy", async () => {
514
+ conversationRow = { conversationType: "background" };
515
+ const signal = makeSignal({
516
+ sourceChannel: "assistant_tool",
517
+ sourceEventName: "user.send_notification",
518
+ contextPayload: { title: "Tool share", body: "Body" },
519
+ });
520
+
521
+ const item = await writeHomeFeedItemForSignal(signal, makeDecision());
522
+
523
+ expect(item?.noteworthy).toBe(true);
524
+ expect(appendCalls[0]!.noteworthy).toBe(true);
525
+ });
526
+
527
+ test("assistant_tool source sets fromAssistant=true", async () => {
528
+ conversationRow = { conversationType: "background" };
529
+ const signal = makeSignal({
530
+ sourceChannel: "assistant_tool",
531
+ sourceEventName: "user.send_notification",
532
+ contextPayload: { title: "Tool share", body: "Body" },
533
+ });
534
+
535
+ const item = await writeHomeFeedItemForSignal(signal, makeDecision());
536
+
537
+ expect(item?.fromAssistant).toBe(true);
538
+ expect(appendCalls[0]!.fromAssistant).toBe(true);
539
+ });
540
+
541
+ test("non-assistant_tool source sets fromAssistant=false", async () => {
542
+ conversationRow = { conversationType: "background" };
543
+ const signal = makeSignal({
544
+ sourceChannel: "scheduler",
545
+ sourceEventName: "schedule.notify",
546
+ contextPayload: { title: "Reminder", body: "Time to do thing" },
547
+ });
548
+
549
+ const item = await writeHomeFeedItemForSignal(signal, makeDecision());
550
+
551
+ expect(item?.fromAssistant).toBe(false);
552
+ expect(appendCalls[0]!.fromAssistant).toBe(false);
553
+ });
554
+
555
+ test("scheduler source with schedule.notify is not noteworthy", async () => {
556
+ conversationRow = { conversationType: "background" };
557
+ const signal = makeSignal({
558
+ sourceChannel: "scheduler",
559
+ sourceEventName: "schedule.notify",
560
+ contextPayload: { title: "Reminder", body: "Time to do thing" },
561
+ });
562
+
563
+ const item = await writeHomeFeedItemForSignal(signal, makeDecision());
564
+
565
+ expect(item?.noteworthy).toBe(false);
566
+ expect(appendCalls[0]!.noteworthy).toBe(false);
567
+ });
568
+
569
+ test("assistant_tool source with guardian.question event still wins (noteworthy true)", async () => {
570
+ conversationRow = { conversationType: "background" };
571
+ const signal = makeSignal({
572
+ sourceChannel: "assistant_tool",
573
+ sourceEventName: "guardian.question",
574
+ contextPayload: { title: "Question", body: "Approve?" },
575
+ });
576
+
577
+ const item = await writeHomeFeedItemForSignal(signal, makeDecision());
578
+
579
+ expect(item?.noteworthy).toBe(true);
580
+ expect(appendCalls[0]!.noteworthy).toBe(true);
581
+ });
582
+
583
+ test("activity.failed with critical urgency is noteworthy (scheduler source)", async () => {
584
+ conversationRow = { conversationType: "background" };
585
+ const signal = makeSignal({
586
+ sourceChannel: "scheduler",
587
+ sourceEventName: "activity.failed",
588
+ contextPayload: { title: "Job failed", body: "Critical failure" },
589
+ attentionHints: {
590
+ requiresAction: false,
591
+ urgency: "critical",
592
+ isAsyncBackground: false,
593
+ visibleInSourceNow: false,
594
+ },
595
+ });
596
+
597
+ const item = await writeHomeFeedItemForSignal(signal, makeDecision());
598
+
599
+ expect(item?.noteworthy).toBe(true);
600
+ expect(appendCalls[0]!.noteworthy).toBe(true);
601
+ });
602
+
603
+ test("activity.failed with low urgency is not noteworthy (scheduler source)", async () => {
604
+ conversationRow = { conversationType: "background" };
605
+ const signal = makeSignal({
606
+ sourceChannel: "scheduler",
607
+ sourceEventName: "activity.failed",
608
+ contextPayload: { title: "Job failed", body: "Routine failure" },
609
+ attentionHints: {
610
+ requiresAction: false,
611
+ urgency: "low",
612
+ isAsyncBackground: false,
613
+ visibleInSourceNow: false,
614
+ },
615
+ });
616
+
617
+ const item = await writeHomeFeedItemForSignal(signal, makeDecision());
618
+
619
+ expect(item?.noteworthy).toBe(false);
620
+ expect(appendCalls[0]!.noteworthy).toBe(false);
621
+ });
622
+
623
+ test("activity.failed from background-job-runner shape (assistant_tool + medium) is NOT noteworthy", async () => {
624
+ // Regression: `runtime/background-job-runner.ts` emits activity.failed
625
+ // with `sourceChannel: "assistant_tool"` and `urgency: "medium"`. Before
626
+ // the fix, the assistant_tool short-circuit short-circuited noteworthy
627
+ // to true, so every routine watcher/heartbeat failure landed in the
628
+ // Inbox. The activity.failed rule must run first and require critical
629
+ // urgency.
630
+ conversationRow = { conversationType: "background" };
631
+ const signal = makeSignal({
632
+ sourceChannel: "assistant_tool",
633
+ sourceEventName: "activity.failed",
634
+ contextPayload: { title: "Job failed", body: "Routine failure" },
635
+ attentionHints: {
636
+ requiresAction: false,
637
+ urgency: "medium",
638
+ isAsyncBackground: true,
639
+ visibleInSourceNow: false,
640
+ },
641
+ });
642
+
643
+ const item = await writeHomeFeedItemForSignal(signal, makeDecision());
644
+
645
+ expect(item?.noteworthy).toBe(false);
646
+ expect(appendCalls[0]!.noteworthy).toBe(false);
647
+ });
648
+
649
+ test("activity.failed from assistant_tool with critical urgency IS noteworthy", async () => {
650
+ // Companion to the regression test above: a background-job-runner
651
+ // shape that opts up to critical urgency should still reach the Inbox.
652
+ conversationRow = { conversationType: "background" };
653
+ const signal = makeSignal({
654
+ sourceChannel: "assistant_tool",
655
+ sourceEventName: "activity.failed",
656
+ contextPayload: { title: "Job failed", body: "Critical failure" },
657
+ attentionHints: {
658
+ requiresAction: false,
659
+ urgency: "critical",
660
+ isAsyncBackground: true,
661
+ visibleInSourceNow: false,
662
+ },
663
+ });
664
+
665
+ const item = await writeHomeFeedItemForSignal(signal, makeDecision());
666
+
667
+ expect(item?.noteworthy).toBe(true);
668
+ expect(appendCalls[0]!.noteworthy).toBe(true);
669
+ });
670
+
671
+ test("credential.health_alert is noteworthy regardless of source channel", async () => {
672
+ conversationRow = { conversationType: "background" };
673
+ const signal = makeSignal({
674
+ sourceChannel: "watcher",
675
+ sourceEventName: "credential.health_alert",
676
+ contextPayload: { title: "Credential expired", body: "Reconnect" },
677
+ });
678
+
679
+ const item = await writeHomeFeedItemForSignal(signal, makeDecision());
195
680
 
196
- expect(item?.title).toBe("watcher.notification");
197
- expect(item?.summary).toBe("watcher.notification");
681
+ expect(item?.noteworthy).toBe(true);
682
+ expect(appendCalls[0]!.noteworthy).toBe(true);
198
683
  });
199
684
  });
@@ -3,8 +3,13 @@
3
3
  * and mobile clients via the daemon's event broadcast mechanism.
4
4
  *
5
5
  * The adapter broadcasts a `notification_intent` message that the Vellum
6
- * client can use to display a native notification (e.g. NSUserNotification
7
- * or UNUserNotificationCenter).
6
+ * client uses for two distinct purposes: paired-conversation bookkeeping
7
+ * (mark-unseen + history catch-up, fallback dedup) and posting an OS
8
+ * banner via `UNUserNotificationCenter`. The banner posting is gated by
9
+ * the `silent` flag — set to true for non-urgent (`low`/`medium`) signals
10
+ * so the notification center inbox still receives the entry but the OS
11
+ * does not surface a push banner. Urgent signals (`high`/`critical`)
12
+ * broadcast with `silent: false` and fire the banner.
8
13
  *
9
14
  * Guardian-sensitive notifications (approval requests, escalation alerts)
10
15
  * are annotated with `targetGuardianPrincipalId` so that only clients
@@ -75,6 +80,9 @@ export class VellumAdapter implements ChannelAdapter {
75
80
  ? guardianPrincipalId
76
81
  : undefined;
77
82
 
83
+ const silent =
84
+ payload.urgency !== "high" && payload.urgency !== "critical";
85
+
78
86
  this.broadcast({
79
87
  type: "notification_intent",
80
88
  deliveryId: payload.deliveryId,
@@ -83,6 +91,7 @@ export class VellumAdapter implements ChannelAdapter {
83
91
  body: payload.copy.body,
84
92
  deepLinkMetadata: payload.deepLinkTarget,
85
93
  targetGuardianPrincipalId,
94
+ silent,
86
95
  } as ServerMessage);
87
96
 
88
97
  log.info(
@@ -90,6 +99,7 @@ export class VellumAdapter implements ChannelAdapter {
90
99
  sourceEventName: payload.sourceEventName,
91
100
  title: payload.copy.title,
92
101
  guardianScoped: targetGuardianPrincipalId != null,
102
+ silent,
93
103
  },
94
104
  "Vellum notification intent broadcast",
95
105
  );