@vellumai/assistant 0.8.1 → 0.8.3

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 (630) hide show
  1. package/ARCHITECTURE.md +13 -19
  2. package/Dockerfile +75 -1
  3. package/bun.lock +11 -1
  4. package/docker-entrypoint.sh +17 -0
  5. package/docker-init-apt-root.sh +167 -0
  6. package/docker-kata-apt-env.sh +39 -0
  7. package/docs/plugins.md +88 -47
  8. package/docs/skills.md +9 -7
  9. package/examples/plugins/echo/README.md +27 -27
  10. package/examples/plugins/echo/package.json +3 -0
  11. package/examples/plugins/echo/register.ts +31 -31
  12. package/node_modules/@vellumai/slack-text/src/index.test.ts +114 -14
  13. package/node_modules/@vellumai/slack-text/src/index.ts +82 -18
  14. package/openapi.yaml +642 -5
  15. package/package.json +3 -1
  16. package/scripts/generate-openapi.ts +83 -10
  17. package/scripts/sync-llm-catalog.ts +2 -2
  18. package/scripts/sync-web-search-catalog.ts +47 -25
  19. package/src/__tests__/agent-image-optimize.test.ts +11 -3
  20. package/src/__tests__/agent-loop-exit-reason.test.ts +272 -0
  21. package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
  22. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +131 -0
  23. package/src/__tests__/anthropic-provider.test.ts +45 -0
  24. package/src/__tests__/app-builder-tool-scripts.test.ts +9 -3
  25. package/src/__tests__/app-executors.test.ts +220 -4
  26. package/src/__tests__/auto-analysis-end-to-end.test.ts +35 -0
  27. package/src/__tests__/bundled-asset.test.ts +6 -6
  28. package/src/__tests__/channel-availability-routes.test.ts +206 -0
  29. package/src/__tests__/channel-delivery-store.test.ts +289 -1
  30. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -1
  31. package/src/__tests__/clawhub.test.ts +75 -16
  32. package/src/__tests__/compactor-tail-resolution.test.ts +147 -0
  33. package/src/__tests__/config-get-vision-flag.test.ts +136 -0
  34. package/src/__tests__/config-loader-backfill.test.ts +115 -18
  35. package/src/__tests__/config-schema.test.ts +21 -0
  36. package/src/__tests__/config-set-route.test.ts +80 -0
  37. package/src/__tests__/config-sounds-sync.test.ts +97 -0
  38. package/src/__tests__/config-watcher-skill-reseed.test.ts +453 -0
  39. package/src/__tests__/context-search-conversations-source.test.ts +117 -2
  40. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  41. package/src/__tests__/context-search-workspace-source.test.ts +7 -0
  42. package/src/__tests__/context-token-estimator.test.ts +31 -65
  43. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  44. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  45. package/src/__tests__/conversation-agent-loop-overflow.test.ts +92 -92
  46. package/src/__tests__/conversation-agent-loop.test.ts +59 -1
  47. package/src/__tests__/conversation-error.test.ts +42 -3
  48. package/src/__tests__/conversation-fork-crud.test.ts +82 -0
  49. package/src/__tests__/conversation-inference-profile-route.test.ts +40 -4
  50. package/src/__tests__/conversation-lifecycle.test.ts +173 -0
  51. package/src/__tests__/conversation-media-retry.test.ts +19 -8
  52. package/src/__tests__/conversation-message-sync-tags.test.ts +97 -0
  53. package/src/__tests__/conversation-pairing.test.ts +54 -0
  54. package/src/__tests__/conversation-process-callsite.test.ts +4 -1
  55. package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -1
  56. package/src/__tests__/conversation-queue.test.ts +4 -1
  57. package/src/__tests__/conversation-runtime-assembly.test.ts +102 -13
  58. package/src/__tests__/conversation-slash-queue.test.ts +59 -1
  59. package/src/__tests__/conversation-slash-unknown.test.ts +4 -1
  60. package/src/__tests__/conversation-surfaces-table-action.test.ts +360 -0
  61. package/src/__tests__/conversation-sync-tags.test.ts +235 -0
  62. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  63. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  64. package/src/__tests__/credential-security-invariants.test.ts +3 -2
  65. package/src/__tests__/date-context.test.ts +45 -0
  66. package/src/__tests__/db-slack-external-content-normalization.test.ts +301 -0
  67. package/src/__tests__/delete-managed-skill-tool.test.ts +55 -13
  68. package/src/__tests__/disk-pressure-tools.test.ts +1 -0
  69. package/src/__tests__/dm-backfill.test.ts +121 -10
  70. package/src/__tests__/document-tool-security.test.ts +258 -0
  71. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  72. package/src/__tests__/edit-propagation.test.ts +33 -0
  73. package/src/__tests__/empty-response-pipeline.test.ts +0 -4
  74. package/src/__tests__/external-plugin-loader.test.ts +151 -55
  75. package/src/__tests__/filing-service.test.ts +140 -0
  76. package/src/__tests__/get-skill-detail-audit.test.ts +0 -4
  77. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
  78. package/src/__tests__/guardian-dispatch.test.ts +1 -0
  79. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +43 -62
  80. package/src/__tests__/heartbeat-service.test.ts +24 -164
  81. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  82. package/src/__tests__/helpers/tar-fixtures.ts +39 -0
  83. package/src/__tests__/helpers/wait-for.ts +21 -0
  84. package/src/__tests__/history-repair-pipeline.test.ts +0 -3
  85. package/src/__tests__/history-repair.test.ts +73 -0
  86. package/src/__tests__/host-app-control-proxy.test.ts +507 -10
  87. package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
  88. package/src/__tests__/image-credentials.test.ts +1 -1
  89. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  90. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +1 -1
  91. package/src/__tests__/inference-profile-reaper.test.ts +4 -2
  92. package/src/__tests__/inference-profile-session-handler.test.ts +18 -6
  93. package/src/__tests__/inference-profile-session-ipc.test.ts +17 -5
  94. package/src/__tests__/injector-background-turn.test.ts +153 -0
  95. package/src/__tests__/injector-chain.test.ts +15 -8
  96. package/src/__tests__/install-skill-routing.test.ts +155 -37
  97. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +99 -3
  98. package/src/__tests__/list-messages-page-latest.test.ts +55 -0
  99. package/src/__tests__/llm-call-pipeline.test.ts +0 -3
  100. package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
  101. package/src/__tests__/llm-catalog-parity.test.ts +58 -13
  102. package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
  103. package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
  104. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +36 -0
  105. package/src/__tests__/llm-request-log-source-factory.test.ts +29 -53
  106. package/src/__tests__/llm-resolver.test.ts +255 -2
  107. package/src/__tests__/llm-usage-store.test.ts +114 -0
  108. package/src/__tests__/managed-profile-guard.test.ts +41 -29
  109. package/src/__tests__/managed-skill-lifecycle.test.ts +109 -18
  110. package/src/__tests__/managed-store.test.ts +84 -192
  111. package/src/__tests__/media-generate-image.test.ts +1 -1
  112. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -2
  113. package/src/__tests__/messages-after-tiebreaker.test.ts +122 -0
  114. package/src/__tests__/notification-decision-fallback.test.ts +0 -91
  115. package/src/__tests__/notification-decision-strategy.test.ts +14 -31
  116. package/src/__tests__/notification-deep-link.test.ts +15 -0
  117. package/src/__tests__/notification-guardian-path.test.ts +1 -2
  118. package/src/__tests__/notification-platform-adapter.test.ts +5 -4
  119. package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
  120. package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
  121. package/src/__tests__/oauth-commands-routes.test.ts +168 -16
  122. package/src/__tests__/oauth-provider-profiles.test.ts +9 -0
  123. package/src/__tests__/openai-provider.test.ts +242 -3
  124. package/src/__tests__/openai-responses-cutover-guard.test.ts +17 -9
  125. package/src/__tests__/openrouter-provider-only.test.ts +51 -3
  126. package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
  127. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -2
  128. package/src/__tests__/persistence-pipeline.test.ts +0 -2
  129. package/src/__tests__/{managed-proxy-context.test.ts → platform-proxy-context.test.ts} +7 -2
  130. package/src/__tests__/platform.test.ts +2 -0
  131. package/src/__tests__/plugin-api-shim.test.ts +125 -0
  132. package/src/__tests__/plugin-bootstrap.test.ts +10 -36
  133. package/src/__tests__/plugin-external-api.test.ts +68 -0
  134. package/src/__tests__/plugin-registry.test.ts +0 -77
  135. package/src/__tests__/plugin-route-contribution.test.ts +0 -1
  136. package/src/__tests__/plugin-skill-contribution.test.ts +0 -2
  137. package/src/__tests__/plugin-tool-contribution.test.ts +16 -15
  138. package/src/__tests__/plugin-types.test.ts +3 -13
  139. package/src/__tests__/process-message-background-slack.test.ts +8 -1
  140. package/src/__tests__/process-message-display-content.test.ts +421 -0
  141. package/src/__tests__/provider-catalog-visibility.test.ts +158 -0
  142. package/src/__tests__/provider-error-scenarios.test.ts +111 -0
  143. package/src/__tests__/{provider-managed-proxy-integration.test.ts → provider-platform-proxy-integration.test.ts} +33 -31
  144. package/src/__tests__/scaffold-managed-skill-tool.test.ts +65 -13
  145. package/src/__tests__/schedule-routes.test.ts +50 -3
  146. package/src/__tests__/schedule-store.test.ts +94 -0
  147. package/src/__tests__/scheduler-reuse-conversation.test.ts +54 -7
  148. package/src/__tests__/schema-transforms.test.ts +20 -0
  149. package/src/__tests__/search-skills-unified.test.ts +0 -5
  150. package/src/__tests__/{secret-routes-managed-proxy.test.ts → secret-routes-platform-proxy.test.ts} +1 -1
  151. package/src/__tests__/server-history-render.test.ts +43 -0
  152. package/src/__tests__/skill-load-feature-flag.test.ts +0 -12
  153. package/src/__tests__/skill-load-tool.test.ts +27 -89
  154. package/src/__tests__/skill-memory.test.ts +23 -3
  155. package/src/__tests__/skills-file-content-endpoint.test.ts +9 -38
  156. package/src/__tests__/skills-files-catalog-fallback.test.ts +0 -3
  157. package/src/__tests__/skills-install-extract.test.ts +49 -38
  158. package/src/__tests__/skills-install-staging.test.ts +159 -0
  159. package/src/__tests__/skills-uninstall.test.ts +9 -41
  160. package/src/__tests__/skills.test.ts +51 -58
  161. package/src/__tests__/slack-channel-config.test.ts +9 -0
  162. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  163. package/src/__tests__/system-prompt.test.ts +670 -63
  164. package/src/__tests__/terminal-tools.test.ts +28 -1
  165. package/src/__tests__/thread-backfill.test.ts +557 -27
  166. package/src/__tests__/title-generate-pipeline.test.ts +0 -13
  167. package/src/__tests__/token-estimate-pipeline.test.ts +0 -3
  168. package/src/__tests__/tool-error-pipeline.test.ts +0 -3
  169. package/src/__tests__/tool-execute-pipeline.test.ts +0 -5
  170. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
  171. package/src/__tests__/tool-executor.test.ts +16 -4
  172. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -12
  173. package/src/__tests__/turn-events-store.test.ts +256 -0
  174. package/src/__tests__/twilio-routes.test.ts +4 -0
  175. package/src/__tests__/user-plugin-loader.test.ts +0 -7
  176. package/src/__tests__/voice-session-bridge.test.ts +198 -0
  177. package/src/__tests__/web-search-catalog-parity.test.ts +32 -10
  178. package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +115 -3
  179. package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +50 -0
  180. package/src/__tests__/workspace-migration-073-repair-recall-callsite-empty-profile.test.ts +153 -0
  181. package/src/__tests__/workspace-migration-085-memory-v2-bm25-b-reembed-disabled-v2-pages.test.ts +220 -0
  182. package/src/__tests__/workspace-migration-086-revert-stale-gemini-mis-rewrites.test.ts +269 -0
  183. package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
  184. package/src/__tests__/workspace-migration-remove-legacy-skills-index.test.ts +309 -0
  185. package/src/__tests__/workspace-migrations-runner.test.ts +111 -3
  186. package/src/a2a/__tests__/agent-card.test.ts +98 -0
  187. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
  188. package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
  189. package/src/a2a/__tests__/task-store.test.ts +246 -0
  190. package/src/a2a/agent-card.ts +58 -0
  191. package/src/a2a/feature-gate.ts +8 -0
  192. package/src/a2a/protocol-constants.ts +21 -0
  193. package/src/a2a/protocol-errors.ts +50 -0
  194. package/src/a2a/protocol-types.ts +162 -0
  195. package/src/a2a/task-store.ts +168 -0
  196. package/src/acp/resolve-agent.ts +1 -1
  197. package/src/agent/image-optimize.ts +13 -5
  198. package/src/agent/loop.ts +167 -18
  199. package/src/calls/voice-session-bridge.ts +61 -42
  200. package/src/channels/config.ts +9 -0
  201. package/src/channels/types.ts +122 -0
  202. package/src/cli/__tests__/unknown-command.test.ts +24 -0
  203. package/src/cli/commands/__tests__/changelog.test.ts +304 -319
  204. package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
  205. package/src/cli/commands/__tests__/schedules.test.ts +960 -0
  206. package/src/cli/commands/changelog.ts +106 -42
  207. package/src/cli/commands/conversations.ts +102 -17
  208. package/src/cli/commands/default-action.ts +10 -53
  209. package/src/cli/commands/notifications.ts +388 -346
  210. package/src/cli/commands/plugins.ts +252 -0
  211. package/src/cli/commands/schedules.ts +683 -0
  212. package/src/cli/commands/telemetry.ts +40 -0
  213. package/src/cli/lib/__tests__/cli-colors.test.ts +48 -0
  214. package/src/cli/lib/__tests__/confirm-prompt.test.ts +159 -0
  215. package/src/cli/lib/__tests__/install-from-github.test.ts +355 -0
  216. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +154 -0
  217. package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
  218. package/src/cli/lib/__tests__/uninstall-plugin.test.ts +124 -0
  219. package/src/cli/lib/__tests__/unknown-command.test.ts +106 -0
  220. package/src/cli/lib/cli-colors.ts +12 -0
  221. package/src/cli/lib/confirm-prompt.ts +79 -0
  222. package/src/cli/lib/install-from-github.ts +303 -0
  223. package/src/cli/lib/list-installed-plugins.ts +137 -0
  224. package/src/cli/lib/search-plugins.ts +163 -0
  225. package/src/cli/lib/uninstall-plugin.ts +82 -0
  226. package/src/cli/lib/unknown-command.ts +111 -0
  227. package/src/cli/program.ts +52 -2
  228. package/src/config/assistant-feature-flags.ts +24 -54
  229. package/src/config/bundled-skills/app-builder/SKILL.md +140 -22
  230. package/src/config/bundled-skills/app-builder/TOOLS.json +7 -0
  231. package/src/config/bundled-skills/computer-use/TOOLS.json +15 -52
  232. package/src/config/bundled-skills/document/SKILL.md +23 -3
  233. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  234. package/src/config/bundled-skills/document/tools/document-delete.ts +12 -0
  235. package/src/config/bundled-skills/document/tools/document-list.ts +12 -0
  236. package/src/config/bundled-skills/document/tools/document-read.ts +12 -0
  237. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  238. package/src/config/bundled-skills/skill-management/SKILL.md +2 -2
  239. package/src/config/bundled-skills/skill-management/TOOLS.json +7 -7
  240. package/src/config/bundled-tool-registry.ts +6 -0
  241. package/src/config/call-site-defaults.ts +105 -0
  242. package/src/config/feature-flag-registry.json +41 -9
  243. package/src/config/llm-resolver.ts +52 -1
  244. package/src/config/loader.ts +64 -38
  245. package/src/config/schema.ts +9 -10
  246. package/src/config/schemas/__tests__/llm-request-logs.test.ts +36 -0
  247. package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
  248. package/src/config/schemas/channels.ts +17 -0
  249. package/src/config/schemas/compaction.ts +28 -0
  250. package/src/config/schemas/conversations.ts +10 -0
  251. package/src/config/schemas/heartbeat.ts +23 -0
  252. package/src/config/schemas/llm-request-logs.ts +31 -7
  253. package/src/config/schemas/llm.ts +1 -0
  254. package/src/config/schemas/memory-retrieval.ts +18 -0
  255. package/src/config/schemas/memory-retrospective.ts +1 -1
  256. package/src/config/schemas/memory-v2.ts +4 -4
  257. package/src/config/schemas/memory.ts +3 -1
  258. package/src/config/schemas/tools.ts +14 -0
  259. package/src/config/seed-inference-profiles.ts +99 -29
  260. package/src/config/skills.ts +3 -96
  261. package/src/context/compactor.ts +1107 -0
  262. package/src/context/token-estimator.ts +34 -36
  263. package/src/context/window-manager.ts +197 -1520
  264. package/src/credential-execution/managed-catalog.ts +37 -0
  265. package/src/credential-health/credential-health-service.ts +280 -19
  266. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +33 -18
  267. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +138 -0
  268. package/src/daemon/__tests__/conversation-tool-setup.test.ts +74 -0
  269. package/src/daemon/approval-generators.ts +8 -6
  270. package/src/daemon/config-watcher.ts +94 -31
  271. package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
  272. package/src/daemon/conversation-agent-loop.ts +198 -11
  273. package/src/daemon/conversation-error.ts +171 -37
  274. package/src/daemon/conversation-lifecycle.ts +53 -40
  275. package/src/daemon/conversation-messaging.ts +25 -6
  276. package/src/daemon/conversation-process.ts +49 -12
  277. package/src/daemon/conversation-runtime-assembly.ts +25 -1
  278. package/src/daemon/conversation-slash.ts +12 -5
  279. package/src/daemon/conversation-store.ts +11 -4
  280. package/src/daemon/conversation-tool-setup.ts +39 -7
  281. package/src/daemon/conversation.ts +33 -8
  282. package/src/daemon/date-context.ts +40 -0
  283. package/src/daemon/external-plugins-bootstrap.ts +217 -181
  284. package/src/daemon/first-greeting.ts +22 -2
  285. package/src/daemon/guardian-action-generators.ts +1 -125
  286. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
  287. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
  288. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
  289. package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
  290. package/src/daemon/handlers/config-a2a.ts +289 -0
  291. package/src/daemon/handlers/config-model.ts +6 -5
  292. package/src/daemon/handlers/config-slack-channel.ts +15 -3
  293. package/src/daemon/handlers/conversations.ts +1 -0
  294. package/src/daemon/handlers/shared.ts +14 -5
  295. package/src/daemon/handlers/skills.ts +111 -108
  296. package/src/daemon/history-repair.ts +28 -1
  297. package/src/daemon/host-app-control-proxy.ts +153 -27
  298. package/src/daemon/host-proxy-preactivation.ts +85 -18
  299. package/src/daemon/lifecycle.ts +89 -91
  300. package/src/daemon/meet-host-supervisor.ts +5 -4
  301. package/src/daemon/memory-v2-startup.ts +85 -0
  302. package/src/daemon/message-protocol.ts +1 -0
  303. package/src/daemon/message-types/conversations.ts +25 -0
  304. package/src/daemon/message-types/messages.ts +61 -0
  305. package/src/daemon/message-types/notifications.ts +21 -0
  306. package/src/daemon/message-types/subagents.ts +1 -0
  307. package/src/daemon/message-types/sync.ts +1 -0
  308. package/src/daemon/pkb-reminder-builder.test.ts +11 -54
  309. package/src/daemon/pkb-reminder-builder.ts +5 -20
  310. package/src/daemon/plugin-source-watcher.ts +146 -0
  311. package/src/daemon/process-message.ts +24 -3
  312. package/src/daemon/server.ts +11 -2
  313. package/src/daemon/skill-memory-refresh.ts +33 -0
  314. package/src/daemon/wake-target-adapter.ts +2 -0
  315. package/src/documents/document-store.ts +221 -3
  316. package/src/embedded/plugin-api.ts +40 -0
  317. package/src/export/__tests__/transcript-formatter.test.ts +121 -0
  318. package/src/export/transcript-formatter.ts +54 -20
  319. package/src/filing/filing-service.ts +39 -0
  320. package/src/heartbeat/__tests__/heartbeat-service.test.ts +135 -6
  321. package/src/heartbeat/heartbeat-run-store.ts +2 -1
  322. package/src/heartbeat/heartbeat-service.ts +73 -189
  323. package/src/home/__tests__/feed-types.test.ts +80 -0
  324. package/src/home/feed-types.ts +36 -2
  325. package/src/home/post-connect-feed.ts +1 -0
  326. package/src/index.ts +18 -1
  327. package/src/ipc/cli-client.ts +147 -45
  328. package/src/live-voice/__tests__/live-voice-stt.test.ts +57 -0
  329. package/src/mcp/client.ts +20 -4
  330. package/src/media/image-credentials.ts +3 -3
  331. package/src/memory/__tests__/bookmark-crud.test.ts +33 -27
  332. package/src/memory/__tests__/conversation-queries.test.ts +483 -0
  333. package/src/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +113 -0
  334. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
  335. package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
  336. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +119 -14
  337. package/src/memory/__tests__/message-content.test.ts +35 -0
  338. package/src/memory/bookmark-crud.ts +42 -10
  339. package/src/memory/context-search/sources/conversations.ts +62 -2
  340. package/src/memory/context-search/sources/workspace.ts +4 -0
  341. package/src/memory/conversation-crud.ts +63 -19
  342. package/src/memory/conversation-queries.ts +197 -11
  343. package/src/memory/conversation-title-service.ts +26 -4
  344. package/src/memory/db-init.ts +12 -0
  345. package/src/memory/delivery-crud.ts +152 -5
  346. package/src/memory/embedding-backend.ts +4 -4
  347. package/src/memory/external-conversation-store.ts +66 -5
  348. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +150 -12
  349. package/src/memory/graph/conversation-graph-memory.ts +49 -21
  350. package/src/memory/graph/tools.ts +9 -40
  351. package/src/memory/indexer.ts +34 -29
  352. package/src/memory/invite-store.ts +53 -0
  353. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +73 -0
  354. package/src/memory/jobs/embed-concept-page.ts +20 -11
  355. package/src/memory/jobs-worker.ts +6 -1
  356. package/src/memory/llm-request-log-source-clickhouse.ts +24 -12
  357. package/src/memory/llm-request-log-source.ts +19 -52
  358. package/src/memory/llm-request-log-store.ts +92 -1
  359. package/src/memory/llm-usage-store.ts +125 -5
  360. package/src/memory/memory-retrospective-enqueue.ts +1 -20
  361. package/src/memory/memory-retrospective-job.ts +33 -6
  362. package/src/memory/memory-retrospective-startup-cleanup.ts +72 -5
  363. package/src/memory/message-content.ts +1 -1
  364. package/src/memory/migrations/109-external-conversation-bindings.ts +15 -4
  365. package/src/memory/migrations/229-delete-private-conversations.test.ts +38 -1
  366. package/src/memory/migrations/229-delete-private-conversations.ts +7 -0
  367. package/src/memory/migrations/247-external-conversation-binding-thread-id.ts +78 -0
  368. package/src/memory/migrations/248-create-onboarding-events.ts +21 -0
  369. package/src/memory/migrations/249-normalize-slack-external-content.ts +240 -0
  370. package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
  371. package/src/memory/migrations/251-a2a-tasks.ts +49 -0
  372. package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
  373. package/src/memory/migrations/index.ts +9 -0
  374. package/src/memory/migrations/registry.ts +16 -0
  375. package/src/memory/onboarding-events-store.ts +106 -0
  376. package/src/memory/schema/a2a.ts +15 -0
  377. package/src/memory/schema/bookmarks.ts +0 -2
  378. package/src/memory/schema/calls.ts +1 -0
  379. package/src/memory/schema/index.ts +1 -0
  380. package/src/memory/schema/inference.ts +3 -3
  381. package/src/memory/schema/infrastructure.ts +13 -0
  382. package/src/memory/turn-events-store.ts +127 -2
  383. package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
  384. package/src/memory/v2/__tests__/activation.test.ts +0 -8
  385. package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
  386. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
  387. package/src/memory/v2/__tests__/injection.test.ts +288 -11
  388. package/src/memory/v2/__tests__/migration.test.ts +87 -0
  389. package/src/memory/v2/__tests__/page-index.test.ts +83 -0
  390. package/src/memory/v2/__tests__/prompts-router.test.ts +58 -6
  391. package/src/memory/v2/__tests__/qdrant.test.ts +66 -3
  392. package/src/memory/v2/__tests__/router.test.ts +15 -0
  393. package/src/memory/v2/__tests__/skill-store.test.ts +387 -8
  394. package/src/memory/v2/__tests__/static-context.test.ts +12 -1
  395. package/src/memory/v2/activation-store.ts +14 -16
  396. package/src/memory/v2/cli-command-content.ts +19 -0
  397. package/src/memory/v2/cli-command-store.ts +304 -0
  398. package/src/memory/v2/frontmatter-sweep.ts +7 -1
  399. package/src/memory/v2/injection.ts +81 -26
  400. package/src/memory/v2/migration.ts +49 -19
  401. package/src/memory/v2/page-index.ts +63 -8
  402. package/src/memory/v2/prompts/router.ts +11 -8
  403. package/src/memory/v2/prompts/sweep.ts +2 -2
  404. package/src/memory/v2/qdrant.ts +135 -7
  405. package/src/memory/v2/router.ts +9 -8
  406. package/src/memory/v2/skill-store.ts +120 -35
  407. package/src/memory/v2/static-context.ts +4 -4
  408. package/src/memory/v2/types.ts +23 -0
  409. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
  410. package/src/messaging/providers/a2a/deliver.ts +156 -0
  411. package/src/messaging/providers/gmail/client.ts +9 -2
  412. package/src/messaging/providers/index.ts +11 -2
  413. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +45 -5
  414. package/src/messaging/providers/slack/__tests__/download.test.ts +231 -0
  415. package/src/messaging/providers/slack/adapter.ts +43 -5
  416. package/src/messaging/providers/slack/client.ts +27 -0
  417. package/src/messaging/providers/slack/deep-link.ts +65 -0
  418. package/src/messaging/providers/slack/download.ts +104 -0
  419. package/src/messaging/providers/slack/message-metadata.test.ts +32 -0
  420. package/src/messaging/providers/slack/message-metadata.ts +27 -0
  421. package/src/messaging/providers/slack/render-transcript.test.ts +134 -0
  422. package/src/messaging/providers/slack/render-transcript.ts +69 -5
  423. package/src/messaging/providers/slack/types.ts +20 -1
  424. package/src/notifications/__tests__/broadcaster.test.ts +203 -0
  425. package/src/notifications/__tests__/decision-engine.test.ts +283 -0
  426. package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
  427. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
  428. package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
  429. package/src/notifications/adapters/macos.ts +12 -2
  430. package/src/notifications/broadcaster.ts +29 -4
  431. package/src/notifications/conversation-pairing.ts +2 -1
  432. package/src/notifications/copy-composer.ts +17 -64
  433. package/src/notifications/decision-engine.ts +113 -45
  434. package/src/notifications/deterministic-checks.ts +96 -0
  435. package/src/notifications/emit-signal.ts +21 -1
  436. package/src/notifications/home-feed-side-effect.ts +138 -5
  437. package/src/notifications/signal.ts +3 -5
  438. package/src/notifications/types.ts +8 -0
  439. package/src/oauth/connection-resolver.ts +8 -4
  440. package/src/oauth/platform-connection.test.ts +43 -3
  441. package/src/oauth/platform-connection.ts +19 -6
  442. package/src/oauth/seed-providers.ts +10 -1
  443. package/src/permissions/checker.ts +2 -0
  444. package/src/permissions/ipc-risk-types.ts +1 -0
  445. package/src/permissions/question-prompter.test.ts +416 -0
  446. package/src/permissions/question-prompter.ts +294 -0
  447. package/src/platform/client.test.ts +1 -1
  448. package/src/platform/client.ts +1 -1
  449. package/src/plugin-api/constants.ts +26 -0
  450. package/src/plugin-api/index.ts +34 -1
  451. package/src/plugin-api/types.ts +104 -22
  452. package/src/plugins/defaults/circuit-breaker.ts +0 -5
  453. package/src/plugins/defaults/compaction.ts +0 -4
  454. package/src/plugins/defaults/empty-response.ts +0 -2
  455. package/src/plugins/defaults/history-repair.ts +0 -2
  456. package/src/plugins/defaults/injectors.ts +74 -22
  457. package/src/plugins/defaults/llm-call.ts +0 -2
  458. package/src/plugins/defaults/memory-retrieval.ts +0 -1
  459. package/src/plugins/defaults/overflow-reduce.ts +0 -1
  460. package/src/plugins/defaults/persistence.ts +0 -2
  461. package/src/plugins/defaults/title-generate.ts +0 -5
  462. package/src/plugins/defaults/token-estimate.ts +0 -2
  463. package/src/plugins/defaults/tool-error.ts +0 -7
  464. package/src/plugins/defaults/tool-execute.ts +0 -2
  465. package/src/plugins/defaults/tool-result-truncate.ts +0 -4
  466. package/src/plugins/ensure-plugin-api-shim.ts +96 -0
  467. package/src/plugins/external-api.ts +104 -0
  468. package/src/plugins/external-plugin-loader.ts +187 -42
  469. package/src/plugins/feature-gate.ts +22 -0
  470. package/src/plugins/pipeline.ts +37 -0
  471. package/src/plugins/registry.ts +48 -80
  472. package/src/plugins/types.ts +40 -26
  473. package/src/plugins/user-loader.ts +21 -2
  474. package/src/proactive-artifact/aux-message-injector.ts +11 -0
  475. package/src/proactive-artifact/job.test.ts +37 -5
  476. package/src/prompts/__tests__/system-prompt.test.ts +10 -43
  477. package/src/prompts/__tests__/task-progress-hint-section.test.ts +95 -0
  478. package/src/prompts/normalize-onboarding.ts +27 -0
  479. package/src/prompts/sections.ts +302 -0
  480. package/src/prompts/system-prompt.ts +63 -174
  481. package/src/prompts/templates/BOOTSTRAP.md +17 -1
  482. package/src/prompts/templates/system-sections.ts +164 -0
  483. package/src/providers/__tests__/inference.test.ts +24 -7
  484. package/src/providers/anthropic/client.ts +28 -28
  485. package/src/providers/call-site-routing.ts +24 -6
  486. package/src/providers/connection-resolution.ts +68 -11
  487. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
  488. package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
  489. package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
  490. package/src/providers/inference/adapter-factory.ts +32 -6
  491. package/src/providers/inference/auth.ts +12 -0
  492. package/src/providers/inference/backfill.ts +14 -1
  493. package/src/providers/inference/connections.ts +159 -34
  494. package/src/providers/inference/resolve-auth.ts +14 -4
  495. package/src/providers/model-catalog.ts +249 -12
  496. package/src/providers/model-intents.ts +3 -3
  497. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
  498. package/src/providers/openai/chat-completions-provider.ts +169 -8
  499. package/src/providers/openrouter/client.ts +49 -4
  500. package/src/providers/{managed-proxy → platform-proxy}/constants.ts +4 -2
  501. package/src/providers/{managed-proxy → platform-proxy}/context.ts +3 -3
  502. package/src/providers/provider-availability.ts +17 -2
  503. package/src/providers/provider-catalog-visibility.ts +38 -0
  504. package/src/providers/provider-send-message.ts +27 -12
  505. package/src/providers/registry.ts +52 -15
  506. package/src/providers/retry.ts +47 -1
  507. package/src/runtime/__tests__/agent-wake.test.ts +152 -0
  508. package/src/runtime/agent-wake.ts +103 -15
  509. package/src/runtime/auth/route-policy.ts +21 -1
  510. package/src/runtime/btw-sidechain.ts +2 -0
  511. package/src/runtime/http-server.ts +7 -16
  512. package/src/runtime/http-types.ts +19 -47
  513. package/src/runtime/migrations/origin-mode.ts +1 -1
  514. package/src/runtime/pending-interactions.ts +1 -0
  515. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +17 -0
  516. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
  517. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +5 -1
  518. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +172 -23
  519. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
  520. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
  521. package/src/runtime/routes/__tests__/question-routes.test.ts +395 -0
  522. package/src/runtime/routes/__tests__/tts-routes.test.ts +64 -1
  523. package/src/runtime/routes/acp-routes-list.test.ts +143 -0
  524. package/src/runtime/routes/acp-routes.ts +5 -3
  525. package/src/runtime/routes/auth-routes.ts +1 -1
  526. package/src/runtime/routes/bookmark-routes.ts +5 -3
  527. package/src/runtime/routes/btw-routes.ts +5 -1
  528. package/src/runtime/routes/channel-availability-routes.ts +126 -0
  529. package/src/runtime/routes/consolidation-routes.ts +100 -0
  530. package/src/runtime/routes/conversation-cli-routes.ts +44 -3
  531. package/src/runtime/routes/conversation-list-routes.ts +3 -20
  532. package/src/runtime/routes/conversation-management-routes.ts +17 -42
  533. package/src/runtime/routes/conversation-query-routes.ts +99 -35
  534. package/src/runtime/routes/conversation-routes.ts +97 -11
  535. package/src/runtime/routes/documents-routes.ts +25 -86
  536. package/src/runtime/routes/group-routes.ts +5 -0
  537. package/src/runtime/routes/inbound-conversation.ts +28 -8
  538. package/src/runtime/routes/inbound-message-handler.ts +236 -41
  539. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +111 -0
  540. package/src/runtime/routes/inbound-stages/background-dispatch.ts +32 -1
  541. package/src/runtime/routes/inbound-stages/edit-intercept.ts +17 -4
  542. package/src/runtime/routes/index.ts +8 -0
  543. package/src/runtime/routes/inference-profile-session-handler.ts +17 -44
  544. package/src/runtime/routes/inference-profile-session-reaper.ts +7 -21
  545. package/src/runtime/routes/inference-provider-connection-routes.ts +199 -22
  546. package/src/runtime/routes/integrations/a2a.ts +235 -0
  547. package/src/runtime/routes/integrations/slack/share.ts +4 -52
  548. package/src/runtime/routes/integrations/slack/token.ts +43 -0
  549. package/src/runtime/routes/integrations/twilio.ts +6 -13
  550. package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
  551. package/src/runtime/routes/notification-routes.ts +1 -1
  552. package/src/runtime/routes/oauth-commands-routes.ts +105 -15
  553. package/src/runtime/routes/oauth-lifecycle-routes.ts +43 -0
  554. package/src/runtime/routes/question-routes.ts +259 -0
  555. package/src/runtime/routes/rename-conversation-routes.ts +2 -33
  556. package/src/runtime/routes/schedule-routes.ts +4 -7
  557. package/src/runtime/routes/subagents-routes.ts +98 -18
  558. package/src/runtime/routes/telemetry-routes.ts +27 -0
  559. package/src/runtime/routes/tts-routes.ts +27 -2
  560. package/src/runtime/routes/workspace-routes.test.ts +43 -0
  561. package/src/runtime/routes/workspace-routes.ts +28 -0
  562. package/src/runtime/services/conversation-serializer.ts +39 -7
  563. package/src/runtime/sync/resource-sync-events.ts +93 -1
  564. package/src/schedule/schedule-store.ts +27 -2
  565. package/src/schedule/scheduler.ts +9 -1
  566. package/src/security/__tests__/untrusted-content.test.ts +86 -0
  567. package/src/security/untrusted-content.ts +93 -8
  568. package/src/skills/catalog-files.ts +1 -1
  569. package/src/skills/catalog-install.ts +233 -116
  570. package/src/skills/clawhub.ts +70 -13
  571. package/src/skills/managed-store.ts +4 -119
  572. package/src/skills/skillssh-registry.ts +27 -48
  573. package/src/subagent/manager.ts +17 -7
  574. package/src/telemetry/types.ts +113 -1
  575. package/src/telemetry/usage-telemetry-reporter.test.ts +312 -5
  576. package/src/telemetry/usage-telemetry-reporter.ts +113 -7
  577. package/src/tools/apps/executors.ts +58 -7
  578. package/src/tools/ask-question/ask-question-tool.test.ts +509 -0
  579. package/src/tools/ask-question/ask-question-tool.ts +304 -0
  580. package/src/tools/browser/browser-execution.ts +15 -11
  581. package/src/tools/computer-use/definitions.ts +3 -3
  582. package/src/tools/credentials/vault.ts +1 -1
  583. package/src/tools/document/document-tool.ts +124 -1
  584. package/src/tools/filesystem/edit.ts +1 -1
  585. package/src/tools/filesystem/list.ts +1 -1
  586. package/src/tools/filesystem/read.ts +1 -1
  587. package/src/tools/filesystem/write.ts +5 -2
  588. package/src/tools/host-filesystem/transfer.ts +1 -1
  589. package/src/tools/host-terminal/host-shell.ts +1 -1
  590. package/src/tools/memory/register.ts +1 -9
  591. package/src/tools/permission-checker.ts +1 -1
  592. package/src/tools/registry.ts +17 -7
  593. package/src/tools/schedule/create.ts +2 -2
  594. package/src/tools/schema-transforms.ts +7 -2
  595. package/src/tools/side-effects.ts +1 -0
  596. package/src/tools/skills/delete-managed.ts +4 -4
  597. package/src/tools/skills/execute.ts +1 -1
  598. package/src/tools/skills/scaffold-managed.ts +3 -2
  599. package/src/tools/subagent/notify-parent.ts +1 -1
  600. package/src/tools/system/request-permission.ts +2 -2
  601. package/src/tools/terminal/safe-env.ts +60 -1
  602. package/src/tools/tool-manifest.ts +2 -0
  603. package/src/tools/types.ts +107 -21
  604. package/src/tools/ui-surface/definitions.ts +6 -5
  605. package/src/tts/__tests__/provider-adapters.test.ts +76 -2
  606. package/src/tts/providers/elevenlabs-provider.ts +75 -1
  607. package/src/types/onboarding-context.ts +2 -0
  608. package/src/util/errors.ts +17 -0
  609. package/src/util/platform.ts +10 -0
  610. package/src/watcher/__tests__/engine.test.ts +22 -0
  611. package/src/watcher/engine.ts +6 -2
  612. package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +80 -15
  613. package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +35 -22
  614. package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +3 -1
  615. package/src/workspace/migrations/083-system-prompt-prefix-to-file.ts +191 -0
  616. package/src/workspace/migrations/084-remove-legacy-skills-index.ts +276 -0
  617. package/src/workspace/migrations/085-memory-v2-bm25-b-reembed-disabled-v2-pages.ts +137 -0
  618. package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +198 -0
  619. package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
  620. package/src/workspace/migrations/registry.ts +10 -0
  621. package/src/workspace/migrations/runner.ts +39 -9
  622. package/src/workspace/migrations/types.ts +4 -0
  623. package/examples/plugins/echo/bun.lock +0 -25
  624. package/src/__tests__/context-window-manager.test.ts +0 -2481
  625. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
  626. package/src/context/__tests__/compact-prompt.test.ts +0 -63
  627. package/src/context/prompts/compact.md +0 -26
  628. package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
  629. package/src/prompts/__tests__/build-cli-reference-section.test.ts +0 -37
  630. package/src/runtime/guardian-action-conversation-turn.ts +0 -99
@@ -83,9 +83,11 @@ beforeEach(() => {
83
83
  });
84
84
 
85
85
  describe("writeHomeFeedItemForSignal", () => {
86
- test("background conversation signal writes a feed item with rendered copy", async () => {
86
+ test("background conversation signal writes a feed item with payload title + rendered body", async () => {
87
87
  conversationRow = { conversationType: "background" };
88
- const signal = makeSignal();
88
+ const signal = makeSignal({
89
+ contextPayload: { title: "Background job done" },
90
+ });
89
91
  const decision = makeDecision({
90
92
  renderedCopy: {
91
93
  vellum: {
@@ -146,8 +148,13 @@ describe("writeHomeFeedItemForSignal", () => {
146
148
  visibleInSourceNow: false,
147
149
  },
148
150
  });
151
+ const decision = makeDecision({
152
+ renderedCopy: {
153
+ vellum: { title: "Async title", body: "Async body" },
154
+ },
155
+ });
149
156
 
150
- const item = await writeHomeFeedItemForSignal(signal, makeDecision(), []);
157
+ const item = await writeHomeFeedItemForSignal(signal, decision, []);
151
158
 
152
159
  expect(item).not.toBeNull();
153
160
  expect(appendCalls).toHaveLength(1);
@@ -156,9 +163,49 @@ describe("writeHomeFeedItemForSignal", () => {
156
163
  expect(conversationLookups).toHaveLength(0);
157
164
  });
158
165
 
166
+ test("assistant_tool source mirrors to the home feed even without a background conversation or async hint", async () => {
167
+ // Regression: the `notifications send` CLI/skill emits with
168
+ // `sourceChannel: "assistant_tool"`, a synthetic `cli-<ts>` source
169
+ // context id that does not resolve to a conversation, and
170
+ // `isAsyncBackground: false`. Before the fix, `shouldMirrorToHomeFeed`
171
+ // returned `false` for this shape and the Inbox stayed empty.
172
+ conversationRow = null;
173
+ const signal = makeSignal({
174
+ sourceChannel: "assistant_tool",
175
+ sourceEventName: "assistant.share",
176
+ sourceContextId: "cli-12345",
177
+ contextPayload: { title: "Shared from CLI" },
178
+ attentionHints: {
179
+ requiresAction: false,
180
+ urgency: "low",
181
+ isAsyncBackground: false,
182
+ visibleInSourceNow: false,
183
+ },
184
+ });
185
+ const decision = makeDecision({
186
+ renderedCopy: {
187
+ vellum: { title: "Shared from CLI", body: "Body from CLI share" },
188
+ },
189
+ });
190
+
191
+ const item = await writeHomeFeedItemForSignal(signal, decision, []);
192
+
193
+ expect(item).not.toBeNull();
194
+ expect(appendCalls).toHaveLength(1);
195
+ expect(appendCalls[0]!.title).toBe("Shared from CLI");
196
+ expect(appendCalls[0]!.noteworthy).toBe(true);
197
+ // The assistant_tool short-circuit must not consult the conversation store.
198
+ expect(conversationLookups).toHaveLength(0);
199
+ });
200
+
159
201
  test("vellum delivery result conversationId propagates onto the feed item", async () => {
160
202
  conversationRow = { conversationType: "background" };
161
203
  const signal = makeSignal();
204
+ const decision = makeDecision({
205
+ renderedCopy: {
206
+ vellum: { title: "Routed title", body: "Routed body" },
207
+ },
208
+ });
162
209
  const deliveryResults: NotificationDeliveryResult[] = [
163
210
  {
164
211
  channel: "telegram",
@@ -176,7 +223,7 @@ describe("writeHomeFeedItemForSignal", () => {
176
223
 
177
224
  const item = await writeHomeFeedItemForSignal(
178
225
  signal,
179
- makeDecision(),
226
+ decision,
180
227
  deliveryResults,
181
228
  );
182
229
 
@@ -184,7 +231,7 @@ describe("writeHomeFeedItemForSignal", () => {
184
231
  expect(appendCalls[0]!.conversationId).toBe("conv-vellum-1");
185
232
  });
186
233
 
187
- test("falls back to sourceEventName when no rendered copy or payload title is present", async () => {
234
+ test("returns null and does not write when no rendered copy or payload title/body is present", async () => {
188
235
  conversationRow = { conversationType: "scheduled" };
189
236
  const signal = makeSignal({
190
237
  sourceEventName: "watcher.notification",
@@ -193,7 +240,383 @@ describe("writeHomeFeedItemForSignal", () => {
193
240
 
194
241
  const item = await writeHomeFeedItemForSignal(signal, makeDecision(), []);
195
242
 
196
- expect(item?.title).toBe("watcher.notification");
197
- expect(item?.summary).toBe("watcher.notification");
243
+ expect(item).toBeNull();
244
+ expect(appendCalls).toHaveLength(0);
245
+ });
246
+
247
+ test("returns null when only the title is available but the summary would fall back to event name", async () => {
248
+ conversationRow = { conversationType: "background" };
249
+ const signal = makeSignal({
250
+ sourceEventName: "example.event",
251
+ contextPayload: { title: "Real title" },
252
+ });
253
+
254
+ const item = await writeHomeFeedItemForSignal(signal, makeDecision(), []);
255
+
256
+ expect(item).toBeNull();
257
+ expect(appendCalls).toHaveLength(0);
258
+ });
259
+
260
+ test("writes a feed item with undefined title when only the body is available", async () => {
261
+ // Regression: when `notifications send` is called without `--title`, the
262
+ // notification pipeline must not manufacture a title (the LLM's rendered
263
+ // copy echoes the body into `renderedCopy.title`). Leave `title`
264
+ // undefined so renderers fall back to `summary` instead of stuttering.
265
+ conversationRow = { conversationType: "background" };
266
+ const signal = makeSignal({
267
+ sourceEventName: "example.event",
268
+ contextPayload: { body: "Real body" },
269
+ });
270
+
271
+ const item = await writeHomeFeedItemForSignal(signal, makeDecision(), []);
272
+
273
+ expect(item).not.toBeNull();
274
+ expect(appendCalls).toHaveLength(1);
275
+ expect(appendCalls[0]!.title).toBeUndefined();
276
+ expect(appendCalls[0]!.summary).toBe("Real body");
277
+ });
278
+
279
+ test("ignores LLM-rendered title when no payload title was supplied", async () => {
280
+ // The LLM often echoes the body verbatim into `renderedCopy.title` when
281
+ // the source didn't pass one. The home-feed writer must NOT promote that
282
+ // echo into the feed item — only an explicit source title is honored.
283
+ conversationRow = { conversationType: "background" };
284
+ const signal = makeSignal({
285
+ sourceEventName: "example.event",
286
+ contextPayload: { body: "Real body" },
287
+ });
288
+ const decision = makeDecision({
289
+ renderedCopy: {
290
+ vellum: {
291
+ title: "Real body",
292
+ body: "Real body",
293
+ },
294
+ },
295
+ });
296
+
297
+ const item = await writeHomeFeedItemForSignal(signal, decision, []);
298
+
299
+ expect(item).not.toBeNull();
300
+ expect(appendCalls).toHaveLength(1);
301
+ expect(appendCalls[0]!.title).toBeUndefined();
302
+ expect(appendCalls[0]!.summary).toBe("Real body");
303
+ });
304
+
305
+ test("treats whitespace-only rendered copy and payload values as missing and returns null", async () => {
306
+ conversationRow = { conversationType: "background" };
307
+ const signal = makeSignal({
308
+ sourceEventName: "example.event",
309
+ contextPayload: { title: " ", body: "\t\n" },
310
+ });
311
+ const decision = makeDecision({
312
+ renderedCopy: {
313
+ vellum: { title: " ", body: " " },
314
+ },
315
+ });
316
+
317
+ const item = await writeHomeFeedItemForSignal(signal, decision, []);
318
+
319
+ expect(item).toBeNull();
320
+ expect(appendCalls).toHaveLength(0);
321
+ });
322
+
323
+ test("falls back to a non-vellum channel's rendered copy when vellum copy is absent", async () => {
324
+ // Regression: when `preferredChannels` narrows an assistant_tool signal
325
+ // to a non-vellum channel (e.g. telegram), the broadcaster ships real
326
+ // copy on that channel but `renderedCopy.vellum` is undefined. The
327
+ // guard must still write to the home feed using the first available
328
+ // rendered copy entry rather than skipping silently.
329
+ conversationRow = { conversationType: "background" };
330
+ const signal = makeSignal({
331
+ sourceChannel: "assistant_tool",
332
+ sourceEventName: "assistant.share",
333
+ sourceContextId: "cli-12345",
334
+ contextPayload: { title: "Telegram title" },
335
+ });
336
+ const decision = makeDecision({
337
+ selectedChannels: ["telegram"],
338
+ renderedCopy: {
339
+ telegram: { title: "Telegram title", body: "Telegram body" },
340
+ },
341
+ });
342
+
343
+ const item = await writeHomeFeedItemForSignal(signal, decision, []);
344
+
345
+ expect(item).not.toBeNull();
346
+ expect(appendCalls).toHaveLength(1);
347
+ expect(appendCalls[0]!.title).toBe("Telegram title");
348
+ expect(appendCalls[0]!.summary).toBe("Telegram body");
349
+ });
350
+
351
+ test("ignores rendered copy for channels not in selectedChannels", async () => {
352
+ // Regression: routing-intent enforcement can prune selectedChannels
353
+ // without pruning renderedCopy, leaving copy entries for channels that
354
+ // were never delivered. The fallback must only consider channels that
355
+ // actually shipped — otherwise an unselected channel's copy can land in
356
+ // Home in place of the selected channel's copy.
357
+ conversationRow = { conversationType: "background" };
358
+ const signal = makeSignal({
359
+ sourceChannel: "assistant_tool",
360
+ sourceEventName: "assistant.share",
361
+ sourceContextId: "cli-12345",
362
+ contextPayload: { title: "Telegram title" },
363
+ });
364
+ const decision = makeDecision({
365
+ selectedChannels: ["telegram"],
366
+ renderedCopy: {
367
+ slack: {
368
+ title: "Slack title (unselected)",
369
+ body: "Slack body (unselected)",
370
+ },
371
+ telegram: { title: "Telegram title", body: "Telegram body" },
372
+ },
373
+ });
374
+
375
+ const item = await writeHomeFeedItemForSignal(signal, decision, []);
376
+
377
+ expect(item).not.toBeNull();
378
+ expect(appendCalls).toHaveLength(1);
379
+ expect(appendCalls[0]!.title).toBe("Telegram title");
380
+ expect(appendCalls[0]!.summary).toBe("Telegram body");
381
+ });
382
+
383
+ test("skips fallback when only unselected channels have rendered copy", async () => {
384
+ // Regression: if every renderedCopy entry is for a channel that was
385
+ // pruned from selectedChannels, treat it as no copy at all rather than
386
+ // surfacing the stale entry.
387
+ conversationRow = { conversationType: "background" };
388
+ const signal = makeSignal({
389
+ sourceChannel: "assistant_tool",
390
+ sourceEventName: "assistant.share",
391
+ sourceContextId: "cli-12345",
392
+ });
393
+ const decision = makeDecision({
394
+ selectedChannels: ["telegram"],
395
+ renderedCopy: {
396
+ slack: {
397
+ title: "Slack title (unselected)",
398
+ body: "Slack body (unselected)",
399
+ },
400
+ },
401
+ });
402
+
403
+ const item = await writeHomeFeedItemForSignal(signal, decision, []);
404
+
405
+ expect(item).toBeNull();
406
+ expect(appendCalls).toHaveLength(0);
407
+ });
408
+
409
+ test("falls back to requestedTitle/requestedMessage payload keys", async () => {
410
+ // Regression: the `notifications send` CLI surface stores the
411
+ // user-supplied copy on the signal payload under `requestedTitle` and
412
+ // `requestedMessage`. If the decision strips renderedCopy.vellum (e.g.
413
+ // routed only to a non-vellum channel that also lacks renderedCopy),
414
+ // the home-feed guard must still recover the copy from the payload.
415
+ conversationRow = { conversationType: "background" };
416
+ const signal = makeSignal({
417
+ sourceChannel: "assistant_tool",
418
+ sourceEventName: "assistant.share",
419
+ sourceContextId: "cli-12345",
420
+ contextPayload: {
421
+ requestedTitle: "Requested title",
422
+ requestedMessage: "Requested message body",
423
+ },
424
+ });
425
+
426
+ const item = await writeHomeFeedItemForSignal(signal, makeDecision(), []);
427
+
428
+ expect(item).not.toBeNull();
429
+ expect(appendCalls).toHaveLength(1);
430
+ expect(appendCalls[0]!.title).toBe("Requested title");
431
+ expect(appendCalls[0]!.summary).toBe("Requested message body");
432
+ });
433
+
434
+ test("uses payload title/body when rendered copy is absent", async () => {
435
+ conversationRow = { conversationType: "background" };
436
+ const signal = makeSignal({
437
+ sourceEventName: "watcher.notification",
438
+ contextPayload: { title: "Payload title", body: "Payload body" },
439
+ });
440
+
441
+ const item = await writeHomeFeedItemForSignal(signal, makeDecision(), []);
442
+
443
+ expect(item).not.toBeNull();
444
+ expect(item?.title).toBe("Payload title");
445
+ expect(item?.summary).toBe("Payload body");
446
+ expect(appendCalls).toHaveLength(1);
447
+ });
448
+
449
+ // ── noteworthy derivation ────────────────────────────────────────────
450
+
451
+ test("assistant_tool source marks the feed item noteworthy", async () => {
452
+ conversationRow = { conversationType: "background" };
453
+ const signal = makeSignal({
454
+ sourceChannel: "assistant_tool",
455
+ sourceEventName: "user.send_notification",
456
+ contextPayload: { title: "Tool share", body: "Body" },
457
+ });
458
+
459
+ const item = await writeHomeFeedItemForSignal(signal, makeDecision(), []);
460
+
461
+ expect(item?.noteworthy).toBe(true);
462
+ expect(appendCalls[0]!.noteworthy).toBe(true);
463
+ });
464
+
465
+ test("assistant_tool source sets fromAssistant=true", async () => {
466
+ conversationRow = { conversationType: "background" };
467
+ const signal = makeSignal({
468
+ sourceChannel: "assistant_tool",
469
+ sourceEventName: "user.send_notification",
470
+ contextPayload: { title: "Tool share", body: "Body" },
471
+ });
472
+
473
+ const item = await writeHomeFeedItemForSignal(signal, makeDecision(), []);
474
+
475
+ expect(item?.fromAssistant).toBe(true);
476
+ expect(appendCalls[0]!.fromAssistant).toBe(true);
477
+ });
478
+
479
+ test("non-assistant_tool source sets fromAssistant=false", async () => {
480
+ conversationRow = { conversationType: "background" };
481
+ const signal = makeSignal({
482
+ sourceChannel: "scheduler",
483
+ sourceEventName: "schedule.notify",
484
+ contextPayload: { title: "Reminder", body: "Time to do thing" },
485
+ });
486
+
487
+ const item = await writeHomeFeedItemForSignal(signal, makeDecision(), []);
488
+
489
+ expect(item?.fromAssistant).toBe(false);
490
+ expect(appendCalls[0]!.fromAssistant).toBe(false);
491
+ });
492
+
493
+ test("scheduler source with schedule.notify is not noteworthy", async () => {
494
+ conversationRow = { conversationType: "background" };
495
+ const signal = makeSignal({
496
+ sourceChannel: "scheduler",
497
+ sourceEventName: "schedule.notify",
498
+ contextPayload: { title: "Reminder", body: "Time to do thing" },
499
+ });
500
+
501
+ const item = await writeHomeFeedItemForSignal(signal, makeDecision(), []);
502
+
503
+ expect(item?.noteworthy).toBe(false);
504
+ expect(appendCalls[0]!.noteworthy).toBe(false);
505
+ });
506
+
507
+ test("assistant_tool source with guardian.question event still wins (noteworthy true)", async () => {
508
+ conversationRow = { conversationType: "background" };
509
+ const signal = makeSignal({
510
+ sourceChannel: "assistant_tool",
511
+ sourceEventName: "guardian.question",
512
+ contextPayload: { title: "Question", body: "Approve?" },
513
+ });
514
+
515
+ const item = await writeHomeFeedItemForSignal(signal, makeDecision(), []);
516
+
517
+ expect(item?.noteworthy).toBe(true);
518
+ expect(appendCalls[0]!.noteworthy).toBe(true);
519
+ });
520
+
521
+ test("activity.failed with critical urgency is noteworthy (scheduler source)", async () => {
522
+ conversationRow = { conversationType: "background" };
523
+ const signal = makeSignal({
524
+ sourceChannel: "scheduler",
525
+ sourceEventName: "activity.failed",
526
+ contextPayload: { title: "Job failed", body: "Critical failure" },
527
+ attentionHints: {
528
+ requiresAction: false,
529
+ urgency: "critical",
530
+ isAsyncBackground: false,
531
+ visibleInSourceNow: false,
532
+ },
533
+ });
534
+
535
+ const item = await writeHomeFeedItemForSignal(signal, makeDecision(), []);
536
+
537
+ expect(item?.noteworthy).toBe(true);
538
+ expect(appendCalls[0]!.noteworthy).toBe(true);
539
+ });
540
+
541
+ test("activity.failed with low urgency is not noteworthy (scheduler source)", async () => {
542
+ conversationRow = { conversationType: "background" };
543
+ const signal = makeSignal({
544
+ sourceChannel: "scheduler",
545
+ sourceEventName: "activity.failed",
546
+ contextPayload: { title: "Job failed", body: "Routine failure" },
547
+ attentionHints: {
548
+ requiresAction: false,
549
+ urgency: "low",
550
+ isAsyncBackground: false,
551
+ visibleInSourceNow: false,
552
+ },
553
+ });
554
+
555
+ const item = await writeHomeFeedItemForSignal(signal, makeDecision(), []);
556
+
557
+ expect(item?.noteworthy).toBe(false);
558
+ expect(appendCalls[0]!.noteworthy).toBe(false);
559
+ });
560
+
561
+ test("activity.failed from background-job-runner shape (assistant_tool + medium) is NOT noteworthy", async () => {
562
+ // Regression: `runtime/background-job-runner.ts` emits activity.failed
563
+ // with `sourceChannel: "assistant_tool"` and `urgency: "medium"`. Before
564
+ // the fix, the assistant_tool short-circuit short-circuited noteworthy
565
+ // to true, so every routine watcher/heartbeat failure landed in the
566
+ // Inbox. The activity.failed rule must run first and require critical
567
+ // urgency.
568
+ conversationRow = { conversationType: "background" };
569
+ const signal = makeSignal({
570
+ sourceChannel: "assistant_tool",
571
+ sourceEventName: "activity.failed",
572
+ contextPayload: { title: "Job failed", body: "Routine failure" },
573
+ attentionHints: {
574
+ requiresAction: false,
575
+ urgency: "medium",
576
+ isAsyncBackground: true,
577
+ visibleInSourceNow: false,
578
+ },
579
+ });
580
+
581
+ const item = await writeHomeFeedItemForSignal(signal, makeDecision(), []);
582
+
583
+ expect(item?.noteworthy).toBe(false);
584
+ expect(appendCalls[0]!.noteworthy).toBe(false);
585
+ });
586
+
587
+ test("activity.failed from assistant_tool with critical urgency IS noteworthy", async () => {
588
+ // Companion to the regression test above: a background-job-runner
589
+ // shape that opts up to critical urgency should still reach the Inbox.
590
+ conversationRow = { conversationType: "background" };
591
+ const signal = makeSignal({
592
+ sourceChannel: "assistant_tool",
593
+ sourceEventName: "activity.failed",
594
+ contextPayload: { title: "Job failed", body: "Critical failure" },
595
+ attentionHints: {
596
+ requiresAction: false,
597
+ urgency: "critical",
598
+ isAsyncBackground: true,
599
+ visibleInSourceNow: false,
600
+ },
601
+ });
602
+
603
+ const item = await writeHomeFeedItemForSignal(signal, makeDecision(), []);
604
+
605
+ expect(item?.noteworthy).toBe(true);
606
+ expect(appendCalls[0]!.noteworthy).toBe(true);
607
+ });
608
+
609
+ test("credential.health_alert is noteworthy regardless of source channel", async () => {
610
+ conversationRow = { conversationType: "background" };
611
+ const signal = makeSignal({
612
+ sourceChannel: "watcher",
613
+ sourceEventName: "credential.health_alert",
614
+ contextPayload: { title: "Credential expired", body: "Reconnect" },
615
+ });
616
+
617
+ const item = await writeHomeFeedItemForSignal(signal, makeDecision(), []);
618
+
619
+ expect(item?.noteworthy).toBe(true);
620
+ expect(appendCalls[0]!.noteworthy).toBe(true);
198
621
  });
199
622
  });
@@ -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
  );
@@ -45,6 +45,11 @@ export interface ConversationCreatedInfo {
45
45
  groupId?: string;
46
46
  /** Semantic source from the signal producer (e.g. "schedule", "reminder"). */
47
47
  source?: string;
48
+ /**
49
+ * Mirrors the vellum adapter's `silent` flag. When true the client
50
+ * must skip the fallback OS banner — the sidebar entry still appears.
51
+ */
52
+ silent: boolean;
48
53
  }
49
54
  export type OnConversationCreatedFn = (info: ConversationCreatedInfo) => void;
50
55
  export interface BroadcastDecisionOptions {
@@ -145,10 +150,25 @@ export class NotificationBroadcaster {
145
150
  if (!fallbackCopy) {
146
151
  fallbackCopy = composeFallbackCopy(signal, decision.selectedChannels);
147
152
  }
148
- copy = fallbackCopy[channel] ?? {
149
- title: "Notification",
150
- body: signal.sourceEventName,
151
- };
153
+ copy = fallbackCopy[channel];
154
+ }
155
+
156
+ // Fail closed: if neither the decision nor the fallback composer produced
157
+ // a usable body, skip the channel rather than leaking the raw event name
158
+ // as placeholder text. The pre-send `checkRenderedCopyQuality` only sees
159
+ // `decision.renderedCopy`, so this is the last guard before delivery.
160
+ if (!copy || !copy.body?.trim()) {
161
+ log.warn(
162
+ { channel, signalId: signal.signalId },
163
+ "No usable rendered copy available -- skipping channel to avoid leaking event name",
164
+ );
165
+ results.push({
166
+ channel,
167
+ destination: destination.endpoint ?? channel,
168
+ status: "skipped",
169
+ errorMessage: `No usable rendered copy for channel: ${channel}`,
170
+ });
171
+ continue;
152
172
  }
153
173
 
154
174
  // For tool_grant_request signals, prefer the deterministic template seed
@@ -237,6 +257,9 @@ export class NotificationBroadcaster {
237
257
 
238
258
  const conversationTitle =
239
259
  copy.conversationTitle ?? copy.title ?? signal.sourceEventName;
260
+ const conversationSilent =
261
+ signal.attentionHints.urgency !== "high" &&
262
+ signal.attentionHints.urgency !== "critical";
240
263
  const info: ConversationCreatedInfo = {
241
264
  conversationId: pairing.conversationId,
242
265
  title: conversationTitle,
@@ -244,6 +267,7 @@ export class NotificationBroadcaster {
244
267
  targetGuardianPrincipalId,
245
268
  groupId: signal.conversationMetadata?.groupId,
246
269
  source: signal.conversationMetadata?.source,
270
+ silent: conversationSilent,
247
271
  };
248
272
 
249
273
  // The per-dispatch onConversationCreated callback fires whenever a vellum
@@ -291,6 +315,7 @@ export class NotificationBroadcaster {
291
315
  copy,
292
316
  deepLinkTarget,
293
317
  contextPayload: signal.contextPayload,
318
+ urgency: signal.attentionHints.urgency,
294
319
  };
295
320
 
296
321
  // Compute conversation decision audit fields for the delivery record
@@ -120,7 +120,8 @@ export async function pairDeliveryWithConversation(
120
120
  // Channels with continue_existing_conversation reuse bound external conversations
121
121
  // and mark them as background so they don't clutter the sidebar UI.
122
122
  const conversationType =
123
- strategy === "start_new_conversation" ? "standard" : "background";
123
+ signal.conversationMetadata?.conversationType ??
124
+ (strategy === "start_new_conversation" ? "standard" : "background");
124
125
 
125
126
  // Prefer model-provided conversationSeedMessage when present and sane;
126
127
  // fall back to the runtime composer which adapts verbosity to the