@vellumai/assistant 0.11.1 → 0.11.2-staging.2

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 (318) hide show
  1. package/Dockerfile +1 -3
  2. package/README.md +1 -1
  3. package/eslint.config.mjs +28 -8
  4. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  10. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  12. package/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  13. package/node_modules/@vellumai/slack-text/src/index.test.ts +48 -0
  14. package/node_modules/@vellumai/slack-text/src/index.ts +25 -6
  15. package/openapi.yaml +292 -5
  16. package/package.json +1 -1
  17. package/src/__tests__/agent-loop-resume-interrupted.test.ts +223 -0
  18. package/src/__tests__/byok-default-profile-ensure.test.ts +43 -0
  19. package/src/__tests__/cli-logger-boundary-guard.test.ts +87 -0
  20. package/src/__tests__/compaction-events.test.ts +139 -2
  21. package/src/__tests__/config-loader-backfill.test.ts +35 -3
  22. package/src/__tests__/config-schema.test.ts +1 -0
  23. package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +30 -0
  24. package/src/__tests__/conversation-agent-loop.test.ts +180 -7
  25. package/src/__tests__/conversation-error.test.ts +211 -4
  26. package/src/__tests__/conversation-options-turn-scoped-transport.test.ts +64 -0
  27. package/src/__tests__/conversation-queue.test.ts +126 -0
  28. package/src/__tests__/conversation-retry-route.test.ts +46 -7
  29. package/src/__tests__/conversation-slash.test.ts +4 -2
  30. package/src/__tests__/conversation-summarize-route.test.ts +7 -1
  31. package/src/__tests__/conversation-summarize-up-to.test.ts +71 -5
  32. package/src/__tests__/document-create-dedupe.test.ts +2 -1
  33. package/src/__tests__/document-find-replace.test.ts +2 -1
  34. package/src/__tests__/document-tool-security.test.ts +2 -1
  35. package/src/__tests__/document-update-default-surface.test.ts +2 -1
  36. package/src/__tests__/document-workspace-file.test.ts +467 -0
  37. package/src/__tests__/emit-signal-routing-intent.test.ts +290 -49
  38. package/src/__tests__/guardian-card-withdrawal.test.ts +91 -3
  39. package/src/__tests__/http-user-message-parity.test.ts +66 -0
  40. package/src/__tests__/list-messages-attachments.test.ts +87 -0
  41. package/src/__tests__/list-messages-provider-error.test.ts +143 -0
  42. package/src/__tests__/list-messages-system-card.test.ts +100 -0
  43. package/src/__tests__/llm-resolver.test.ts +19 -9
  44. package/src/__tests__/managed-profile-guard.test.ts +23 -0
  45. package/src/__tests__/notification-platform-adapter.test.ts +130 -2
  46. package/src/__tests__/notification-telegram-adapter.test.ts +6 -0
  47. package/src/__tests__/notification-vellum-adapter.test.ts +45 -0
  48. package/src/__tests__/plugin-api-model-profiles.test.ts +10 -1
  49. package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
  50. package/src/__tests__/provider-error-scenarios.test.ts +140 -0
  51. package/src/__tests__/provider-send-message-override-profile.test.ts +95 -0
  52. package/src/__tests__/run-conversation-turn-persistence.test.ts +66 -3
  53. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +209 -0
  54. package/src/__tests__/skills.test.ts +27 -0
  55. package/src/__tests__/slack-channels-routes.test.ts +0 -2
  56. package/src/__tests__/slack-share-routes.test.ts +0 -3
  57. package/src/__tests__/slack-users-routes.test.ts +0 -2
  58. package/src/__tests__/subagent-tools.test.ts +11 -0
  59. package/src/__tests__/tool-preview-lifecycle.test.ts +58 -0
  60. package/src/__tests__/tool-result-spool.test.ts +5 -4
  61. package/src/__tests__/turn-boundary-resolution.test.ts +53 -0
  62. package/src/__tests__/turn-events-store.test.ts +26 -0
  63. package/src/__tests__/ui-visual-surface.test.ts +695 -0
  64. package/src/__tests__/unified-turn-context-visible-app.test.ts +99 -0
  65. package/src/__tests__/visible-app-context.test.ts +189 -0
  66. package/src/__tests__/workspace-git-service.test.ts +173 -33
  67. package/src/__tests__/workspace-migration-137-repair-retired-fireworks-minimax-model-id.test.ts +157 -0
  68. package/src/__tests__/workspace-migration-138-backfill-home-feed-titles.test.ts +373 -0
  69. package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +137 -0
  70. package/src/agent/loop.ts +36 -7
  71. package/src/api/events/context-window-usage.ts +31 -0
  72. package/src/api/events/notification-intent.ts +8 -0
  73. package/src/api/events/ui-surface-pending.ts +35 -0
  74. package/src/api/index.ts +14 -0
  75. package/src/api/responses/conversation-message.ts +25 -4
  76. package/src/api/surfaces.ts +90 -1
  77. package/src/approvals/guardian-card-withdrawal.ts +66 -31
  78. package/src/approvals/guardian-decision-primitive.ts +4 -0
  79. package/src/calls/__tests__/call-setup-router.test.ts +156 -26
  80. package/src/calls/__tests__/voice-session-bridge.test.ts +201 -12
  81. package/src/calls/call-setup-router.ts +80 -36
  82. package/src/calls/voice-session-bridge.ts +173 -26
  83. package/src/cli/commands/inference.help.ts +3 -3
  84. package/src/cli/commands/notifications.help.ts +3 -2
  85. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +42 -128
  86. package/src/cli/commands/platform/__tests__/credits.test.ts +9 -78
  87. package/src/cli/commands/platform/__tests__/helpers.ts +90 -0
  88. package/src/cli/commands/platform/__tests__/invoices.test.ts +238 -0
  89. package/src/cli/commands/platform/__tests__/plans.test.ts +9 -88
  90. package/src/cli/commands/platform/__tests__/status.test.ts +12 -87
  91. package/src/cli/commands/platform/__tests__/subscription.test.ts +9 -86
  92. package/src/cli/commands/platform/index.help.ts +92 -0
  93. package/src/cli/commands/platform/index.ts +7 -0
  94. package/src/cli/commands/platform/invoices.ts +132 -0
  95. package/src/cli/commands/usage.help.ts +1 -1
  96. package/src/cli/lib/list-installed-plugins.ts +2 -1
  97. package/src/config/__tests__/default-profile-catalog.test.ts +7 -10
  98. package/src/config/__tests__/deployment-context-defaults.test.ts +27 -5
  99. package/src/config/assistant-feature-flags.ts +7 -2
  100. package/src/config/bundled-skills/app-builder/SKILL.md +3 -2
  101. package/src/config/bundled-skills/subagent/SKILL.md +3 -1
  102. package/src/config/bundled-skills/subagent/TOOLS.json +3 -3
  103. package/src/config/bundled-skills/visualize/SKILL.md +163 -0
  104. package/src/config/call-site-defaults.ts +3 -2
  105. package/src/config/default-profile-catalog.ts +75 -70
  106. package/src/config/default-profile-names.ts +13 -28
  107. package/src/config/env-registry.ts +1 -0
  108. package/src/config/feature-flag-registry.json +16 -0
  109. package/src/config/llm-resolver.ts +21 -5
  110. package/src/config/loader.ts +18 -11
  111. package/src/config/schemas/llm.ts +1 -9
  112. package/src/config/schemas/memory-retrospective.ts +9 -0
  113. package/src/config/schemas/monitoring.ts +28 -2
  114. package/src/config/schemas/workspace-git.ts +15 -0
  115. package/src/config/seed-inference-profiles.ts +19 -5
  116. package/src/context/post-turn-tool-result-truncation.ts +2 -2
  117. package/src/conversations/__tests__/message-consolidation.test.ts +54 -0
  118. package/src/conversations/message-consolidation.ts +17 -15
  119. package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +182 -0
  120. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +181 -0
  121. package/src/daemon/conversation-agent-loop-handlers.ts +90 -7
  122. package/src/daemon/conversation-agent-loop.ts +107 -22
  123. package/src/daemon/conversation-error.ts +169 -48
  124. package/src/daemon/conversation-messaging.ts +87 -4
  125. package/src/daemon/conversation-process.ts +57 -45
  126. package/src/daemon/conversation-runtime-assembly.ts +57 -0
  127. package/src/daemon/conversation-store.ts +36 -1
  128. package/src/daemon/conversation-surfaces.ts +68 -6
  129. package/src/daemon/conversation-turn-finalize.ts +76 -18
  130. package/src/daemon/conversation.ts +122 -27
  131. package/src/daemon/lifecycle.ts +9 -0
  132. package/src/daemon/message-types/conversations.ts +8 -0
  133. package/src/daemon/message-types/surfaces.ts +3 -0
  134. package/src/documents/document-store.ts +247 -10
  135. package/src/home/__tests__/feed-types.test.ts +43 -8
  136. package/src/home/__tests__/feed-writer.test.ts +59 -0
  137. package/src/home/feed-types.ts +34 -7
  138. package/src/home/feed-writer.ts +15 -6
  139. package/src/live-voice/__tests__/activity-label.test.ts +95 -0
  140. package/src/live-voice/__tests__/live-activity-reporter.test.ts +86 -5
  141. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +310 -25
  142. package/src/live-voice/__tests__/live-voice-events.test.ts +4 -4
  143. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +8 -4
  144. package/src/live-voice/__tests__/live-voice-vad.test.ts +9 -1
  145. package/src/live-voice/activity-label.ts +169 -0
  146. package/src/live-voice/live-activity-reporter.ts +35 -6
  147. package/src/live-voice/live-voice-session.ts +282 -41
  148. package/src/live-voice/protocol.ts +35 -0
  149. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +84 -2
  150. package/src/messaging/providers/slack/adapter.ts +86 -19
  151. package/src/messaging/providers/slack/api.test.ts +85 -1
  152. package/src/messaging/providers/slack/api.ts +121 -288
  153. package/src/messaging/providers/slack/client.ts +20 -251
  154. package/src/messaging/providers/slack/send.test.ts +4 -9
  155. package/src/messaging/providers/slack/send.ts +1 -1
  156. package/src/messaging/providers/slack/types.ts +5 -0
  157. package/src/messaging/providers/slack/web-api-transport.test.ts +181 -0
  158. package/src/messaging/providers/slack/web-api-transport.ts +367 -0
  159. package/src/messaging/providers/slack/withdraw.ts +6 -14
  160. package/src/messaging/providers/telegram-bot/send.test.ts +31 -1
  161. package/src/messaging/providers/telegram-bot/send.ts +23 -2
  162. package/src/messaging/providers/telegram-bot/withdraw.test.ts +152 -0
  163. package/src/messaging/providers/telegram-bot/withdraw.ts +166 -0
  164. package/src/monitoring/__tests__/db-integrity-sample.test.ts +18 -4
  165. package/src/monitoring/__tests__/file-descriptors.test.ts +144 -0
  166. package/src/monitoring/file-descriptors.ts +262 -0
  167. package/src/monitoring/process-memory.ts +4 -11
  168. package/src/monitoring/resource-sampler.ts +5 -0
  169. package/src/monitoring/worker.ts +12 -0
  170. package/src/notifications/__tests__/assistant-reply-producer.test.ts +740 -0
  171. package/src/notifications/__tests__/broadcaster.test.ts +347 -7
  172. package/src/notifications/__tests__/copy-composer.test.ts +53 -3
  173. package/src/notifications/__tests__/decision-engine.test.ts +349 -29
  174. package/src/notifications/__tests__/deterministic-checks.test.ts +21 -0
  175. package/src/notifications/__tests__/edit-notification.test.ts +330 -0
  176. package/src/notifications/__tests__/guardian-delivery-recorder.test.ts +71 -0
  177. package/src/notifications/__tests__/home-feed-side-effect.test.ts +170 -12
  178. package/src/notifications/adapters/macos.ts +3 -0
  179. package/src/notifications/adapters/platform.ts +90 -14
  180. package/src/notifications/adapters/telegram.ts +6 -4
  181. package/src/notifications/assistant-reply-producer.ts +219 -0
  182. package/src/notifications/broadcaster.ts +523 -317
  183. package/src/notifications/copy-composer.ts +27 -6
  184. package/src/notifications/decision-engine.ts +154 -77
  185. package/src/notifications/deterministic-checks.ts +6 -7
  186. package/src/notifications/edit-notification.ts +8 -4
  187. package/src/notifications/emit-signal.ts +49 -16
  188. package/src/notifications/guardian-delivery-recorder.ts +8 -2
  189. package/src/notifications/home-feed-side-effect.ts +43 -6
  190. package/src/notifications/notification-utils.ts +39 -4
  191. package/src/notifications/signal.ts +10 -0
  192. package/src/notifications/types.ts +17 -0
  193. package/src/persistence/bookmark-crud.ts +18 -9
  194. package/src/persistence/conversation-attention-store.ts +26 -0
  195. package/src/persistence/conversation-crud.ts +79 -32
  196. package/src/persistence/conversation-queries.ts +30 -15
  197. package/src/persistence/conversation-title-service.ts +5 -170
  198. package/src/persistence/conversation-types.ts +192 -1
  199. package/src/persistence/db-init.ts +14 -1
  200. package/src/persistence/db-maintenance.ts +5 -4
  201. package/src/persistence/embeddings/__tests__/plugin-index-qdrant-init.test.ts +219 -0
  202. package/src/persistence/embeddings/__tests__/plugin-index.test.ts +22 -0
  203. package/src/persistence/embeddings/__tests__/worker-script-version.test.ts +76 -0
  204. package/src/persistence/embeddings/embedding-local.ts +2 -1
  205. package/src/persistence/embeddings/embedding-runtime-manager.ts +29 -5
  206. package/src/persistence/embeddings/plugin-index.ts +84 -12
  207. package/src/persistence/migrations/360-add-document-workspace-path.test.ts +110 -0
  208. package/src/persistence/migrations/360-add-document-workspace-path.ts +39 -0
  209. package/src/persistence/planner-statistics.ts +14 -0
  210. package/src/persistence/schema/documents.ts +26 -11
  211. package/src/persistence/steps.ts +2 -0
  212. package/src/platform/client.test.ts +172 -2
  213. package/src/platform/client.ts +178 -55
  214. package/src/plugin-api/conversation-turn.ts +13 -0
  215. package/src/plugin-api/model-profiles.test.ts +6 -2
  216. package/src/plugins/defaults/memory/__tests__/bookmark-crud.test.ts +34 -0
  217. package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +22 -0
  218. package/src/plugins/defaults/memory/__tests__/jobs-store-enqueue-gate.test.ts +11 -0
  219. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +199 -0
  220. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +98 -1
  221. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +105 -1
  222. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +24 -2
  223. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +524 -0
  224. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +1 -1
  225. package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +9 -4
  226. package/src/plugins/defaults/memory/host-utils.ts +5 -0
  227. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +105 -1
  228. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +64 -5
  229. package/src/plugins/defaults/memory/memory-retrospective-job.ts +36 -1
  230. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +12 -3
  231. package/src/plugins/defaults/memory/src/__tests__/memory-v2-simulate-route.test.ts +1 -0
  232. package/src/plugins/defaults/memory/substrate/__tests__/skill-store.test.ts +195 -1
  233. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +1 -1
  234. package/src/plugins/defaults/memory/substrate/skill-store.ts +167 -29
  235. package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +131 -0
  236. package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +1 -0
  237. package/src/plugins/defaults/memory/v2/activation-log-store.ts +4 -0
  238. package/src/plugins/defaults/memory/v2/injection.ts +48 -3
  239. package/src/plugins/defaults/memory/v2/rerank-local.ts +6 -2
  240. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -1
  241. package/src/plugins/defaults/platform-hosted/routes/reengage.ts +1 -1
  242. package/src/plugins/defaults/turn-context/injectors.ts +1 -0
  243. package/src/plugins/defaults/turn-context/unified-turn-context.ts +26 -0
  244. package/src/plugins/types.ts +17 -0
  245. package/src/prompts/templates/system-sections.ts +2 -2
  246. package/src/providers/__tests__/dispatch-connection-routing.test.ts +43 -2
  247. package/src/providers/__tests__/vellum-mismatch-routing.test.ts +8 -0
  248. package/src/providers/call-site-routing.ts +77 -14
  249. package/src/providers/connection-resolution.ts +42 -2
  250. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +127 -1
  251. package/src/providers/inference/adapter-factory.ts +70 -12
  252. package/src/providers/model-catalog.ts +0 -11
  253. package/src/providers/model-intents.ts +11 -0
  254. package/src/providers/registry.ts +8 -0
  255. package/src/providers/retry.ts +85 -17
  256. package/src/providers/types.ts +8 -1
  257. package/src/runtime/__tests__/agent-wake.test.ts +181 -0
  258. package/src/runtime/agent-wake.ts +132 -9
  259. package/src/runtime/channel-approval-types.ts +25 -0
  260. package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +2 -2
  261. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +11 -0
  262. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +121 -9
  263. package/src/runtime/routes/__tests__/platform-invoice-routes.test.ts +392 -0
  264. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +7 -0
  265. package/src/runtime/routes/__tests__/workspace-commit-routes.test.ts +63 -0
  266. package/src/runtime/routes/consolidation-routes.ts +1 -1
  267. package/src/runtime/routes/conversation-management-routes.ts +18 -6
  268. package/src/runtime/routes/conversation-query-routes.ts +14 -23
  269. package/src/runtime/routes/conversation-routes.ts +113 -8
  270. package/src/runtime/routes/credential-routes.ts +1 -5
  271. package/src/runtime/routes/documents-routes.ts +207 -12
  272. package/src/runtime/routes/identity-routes.ts +3 -93
  273. package/src/runtime/routes/inbound-stages/admission-policy.test.ts +31 -1
  274. package/src/runtime/routes/inbound-stages/admission-policy.ts +18 -0
  275. package/src/runtime/routes/inference-provider-connection-routes.ts +52 -3
  276. package/src/runtime/routes/notification-routes.ts +4 -1
  277. package/src/runtime/routes/platform-routes.ts +238 -3
  278. package/src/runtime/routes/playground/guard.ts +1 -2
  279. package/src/runtime/routes/slack-channel-routes.ts +2 -4
  280. package/src/runtime/routes/workspace-commit-routes.ts +49 -2
  281. package/src/runtime/routes/workspace-routes.ts +11 -12
  282. package/src/runtime/routes/workspace-utils.ts +49 -2
  283. package/src/runtime/services/conversation-serializer.ts +2 -4
  284. package/src/subagent/__tests__/consult-context-gating.test.ts +98 -0
  285. package/src/subagent/__tests__/consult-context-skills.test.ts +82 -0
  286. package/src/subagent/__tests__/consult-context.test.ts +84 -0
  287. package/src/subagent/__tests__/consult-prompt.test.ts +36 -0
  288. package/src/subagent/consult-context.ts +410 -0
  289. package/src/subagent/consult-prompt.ts +38 -5
  290. package/src/subagent/manager.ts +9 -4
  291. package/src/subagent/types.ts +8 -0
  292. package/src/telemetry/telemetry-event-sources.ts +7 -0
  293. package/src/telemetry/telemetry-wire-source.json +1 -1
  294. package/src/telemetry/telemetry-wire.generated.ts +1 -0
  295. package/src/telemetry/turn-events-store.ts +30 -1
  296. package/src/telemetry/types.ts +24 -0
  297. package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
  298. package/src/tools/browser/pinned-tabs.ts +3 -1
  299. package/src/tools/subagent/spawn.ts +23 -1
  300. package/src/tools/terminal/safe-env.ts +1 -0
  301. package/src/tools/ui-surface/definitions.ts +39 -1
  302. package/src/tools/ui-surface/surface-shape-docs.ts +38 -4
  303. package/src/tools/ui-surface/visual-validation.ts +787 -0
  304. package/src/tools/workflows/run-workflow.test.ts +1 -0
  305. package/src/util/__tests__/short-title.test.ts +229 -0
  306. package/src/util/__tests__/worker-compute.test.ts +65 -0
  307. package/src/util/cgroup-cpu.ts +93 -0
  308. package/src/util/errors.ts +27 -0
  309. package/src/util/process-tree.ts +19 -0
  310. package/src/util/short-title.ts +189 -0
  311. package/src/util/worker-compute.ts +83 -0
  312. package/src/workspace/byok-default-profile-ensure.ts +25 -15
  313. package/src/workspace/git-service.ts +79 -30
  314. package/src/workspace/migrations/137-repair-retired-fireworks-minimax-model-id.ts +131 -0
  315. package/src/workspace/migrations/138-backfill-home-feed-titles.ts +179 -0
  316. package/src/workspace/migrations/139-clear-renamed-cost-profile-label.ts +92 -0
  317. package/src/workspace/migrations/registry.ts +6 -0
  318. package/src/plugins/defaults/memory/substrate/constants.ts +0 -8
@@ -7,9 +7,11 @@
7
7
 
8
8
  import { beforeEach, describe, expect, mock, test } from "bun:test";
9
9
 
10
+ import type { PairingResult } from "../conversation-pairing.js";
10
11
  import type { NotificationSignal } from "../signal.js";
11
12
  import type {
12
13
  ChannelAdapter,
14
+ ChannelDeliveryObserver,
13
15
  ChannelDeliveryPayload,
14
16
  ChannelDestination,
15
17
  DeliveryResult,
@@ -36,14 +38,27 @@ mock.module("../../contacts/guardian-delivery-reader.js", () => ({
36
38
  getGuardianDelivery: async () => null,
37
39
  }));
38
40
 
39
- mock.module("../conversation-pairing.js", () => ({
40
- pairDeliveryWithConversation: async () => ({
41
- conversationId: undefined,
42
- messageId: undefined,
41
+ function defaultPairing(): PairingResult {
42
+ return {
43
+ conversationId: null,
44
+ messageId: null,
43
45
  strategy: "start_new_conversation",
44
46
  createdNewConversation: false,
45
47
  conversationFallbackUsed: false,
46
- }),
48
+ };
49
+ }
50
+
51
+ let pairingByChannel: Record<string, PairingResult> = {};
52
+ let pairingErrorByChannel: Record<string, Error> = {};
53
+
54
+ mock.module("../conversation-pairing.js", () => ({
55
+ pairDeliveryWithConversation: async (_signal: unknown, channel: string) => {
56
+ const error = pairingErrorByChannel[channel];
57
+ if (error) {
58
+ throw error;
59
+ }
60
+ return pairingByChannel[channel] ?? defaultPairing();
61
+ },
47
62
  }));
48
63
 
49
64
  mock.module("../deliveries-store.js", () => ({
@@ -121,7 +136,10 @@ interface CapturedSend {
121
136
  destination: ChannelDestination;
122
137
  }
123
138
 
124
- function makeCapturingAdapter(channel: "vellum" | "platform"): {
139
+ function makeCapturingAdapter(
140
+ channel: "vellum" | "platform",
141
+ result: DeliveryResult = { success: true },
142
+ ): {
125
143
  adapter: ChannelAdapter;
126
144
  sends: CapturedSend[];
127
145
  } {
@@ -133,7 +151,7 @@ function makeCapturingAdapter(channel: "vellum" | "platform"): {
133
151
  destination: ChannelDestination,
134
152
  ): Promise<DeliveryResult> {
135
153
  sends.push({ payload, destination });
136
- return { success: true };
154
+ return result;
137
155
  },
138
156
  };
139
157
  return { adapter, sends };
@@ -142,6 +160,8 @@ function makeCapturingAdapter(channel: "vellum" | "platform"): {
142
160
  beforeEach(() => {
143
161
  composeFallbackReturn = {};
144
162
  knownConversations = new Set();
163
+ pairingByChannel = {};
164
+ pairingErrorByChannel = {};
145
165
  });
146
166
 
147
167
  // ── Tests ───────────────────────────────────────────────────────────────
@@ -293,6 +313,326 @@ describe("NotificationBroadcaster platform deep-link from contextPayload", () =>
293
313
  });
294
314
  });
295
315
 
316
+ describe("NotificationBroadcaster remotePushDispatched flag", () => {
317
+ const bothChannelsCopy = () => {
318
+ composeFallbackReturn = {
319
+ vellum: { title: "Reminder", body: "Hello" },
320
+ platform: { title: "Reminder", body: "Hello" },
321
+ };
322
+ };
323
+
324
+ const bothChannelsDecision = () =>
325
+ makeDecision({
326
+ selectedChannels: ["vellum", "platform"],
327
+ renderedCopy: {},
328
+ });
329
+
330
+ test("vellum payload carries true when the platform adapter reports an accepted push; platform payload omits it", async () => {
331
+ bothChannelsCopy();
332
+
333
+ const vellum = makeCapturingAdapter("vellum");
334
+ const platform = makeCapturingAdapter("platform", {
335
+ success: true,
336
+ remotePushAccepted: true,
337
+ });
338
+ const broadcaster = new NotificationBroadcaster([
339
+ vellum.adapter,
340
+ platform.adapter,
341
+ ]);
342
+
343
+ await broadcaster.broadcastDecision(makeSignal(), bothChannelsDecision());
344
+
345
+ expect(vellum.sends.length).toBe(1);
346
+ expect(vellum.sends[0]?.payload.remotePushDispatched).toBe(true);
347
+ expect(platform.sends.length).toBe(1);
348
+ expect(platform.sends[0]?.payload.correlationId).toBe(
349
+ vellum.sends[0]?.payload.correlationId,
350
+ );
351
+ expect(platform.sends[0]?.payload.remotePushDispatched).toBeUndefined();
352
+ });
353
+
354
+ test("vellum payload carries false when the platform dispatch fails", async () => {
355
+ bothChannelsCopy();
356
+
357
+ const vellum = makeCapturingAdapter("vellum");
358
+ const platform = makeCapturingAdapter("platform", {
359
+ success: false,
360
+ error: "HTTP 503",
361
+ });
362
+ const broadcaster = new NotificationBroadcaster([
363
+ vellum.adapter,
364
+ platform.adapter,
365
+ ]);
366
+
367
+ await broadcaster.broadcastDecision(makeSignal(), bothChannelsDecision());
368
+
369
+ expect(vellum.sends.length).toBe(1);
370
+ expect(vellum.sends[0]?.payload.remotePushDispatched).toBe(false);
371
+ });
372
+
373
+ test("vellum payload carries platforms accepted before a partial failure", async () => {
374
+ bothChannelsCopy();
375
+
376
+ const vellum = makeCapturingAdapter("vellum");
377
+ const platform = makeCapturingAdapter("platform", {
378
+ success: false,
379
+ error: "FCM failed",
380
+ remotePushPlatforms: ["ios"],
381
+ });
382
+ const broadcaster = new NotificationBroadcaster([
383
+ vellum.adapter,
384
+ platform.adapter,
385
+ ]);
386
+
387
+ await broadcaster.broadcastDecision(makeSignal(), bothChannelsDecision());
388
+
389
+ expect(vellum.sends[0]?.payload.remotePushPlatforms).toEqual(["ios"]);
390
+ });
391
+
392
+ test("vellum payload carries false when the platform reports no accepted push (skipped or zero tokens)", async () => {
393
+ bothChannelsCopy();
394
+
395
+ const vellum = makeCapturingAdapter("vellum");
396
+ const platform = makeCapturingAdapter("platform", {
397
+ success: true,
398
+ remotePushAccepted: false,
399
+ });
400
+ const broadcaster = new NotificationBroadcaster([
401
+ vellum.adapter,
402
+ platform.adapter,
403
+ ]);
404
+
405
+ await broadcaster.broadcastDecision(makeSignal(), bothChannelsDecision());
406
+
407
+ expect(vellum.sends.length).toBe(1);
408
+ expect(vellum.sends[0]?.payload.remotePushDispatched).toBe(false);
409
+ });
410
+
411
+ test("vellum payload carries false when the platform result omits remotePushAccepted", async () => {
412
+ bothChannelsCopy();
413
+
414
+ const vellum = makeCapturingAdapter("vellum");
415
+ const platform = makeCapturingAdapter("platform", { success: true });
416
+ const broadcaster = new NotificationBroadcaster([
417
+ vellum.adapter,
418
+ platform.adapter,
419
+ ]);
420
+
421
+ await broadcaster.broadcastDecision(makeSignal(), bothChannelsDecision());
422
+
423
+ expect(vellum.sends.length).toBe(1);
424
+ expect(vellum.sends[0]?.payload.remotePushDispatched).toBe(false);
425
+ });
426
+
427
+ test("vellum payload carries false when the platform adapter throws", async () => {
428
+ bothChannelsCopy();
429
+
430
+ const vellum = makeCapturingAdapter("vellum");
431
+ const platform: ChannelAdapter = {
432
+ channel: "platform",
433
+ async send(): Promise<DeliveryResult> {
434
+ throw new Error("boom");
435
+ },
436
+ };
437
+ const broadcaster = new NotificationBroadcaster([vellum.adapter, platform]);
438
+
439
+ const results = await broadcaster.broadcastDecision(
440
+ makeSignal(),
441
+ bothChannelsDecision(),
442
+ );
443
+
444
+ expect(vellum.sends.length).toBe(1);
445
+ expect(vellum.sends[0]?.payload.remotePushDispatched).toBe(false);
446
+ expect(results.find((r) => r.channel === "platform")?.status).toBe(
447
+ "failed",
448
+ );
449
+ expect(results.find((r) => r.channel === "vellum")?.status).toBe("sent");
450
+ });
451
+
452
+ test("vellum intent still flushes with false when the platform channel's prep throws", async () => {
453
+ bothChannelsCopy();
454
+ pairingErrorByChannel = { platform: new Error("pairing exploded") };
455
+
456
+ const vellum = makeCapturingAdapter("vellum");
457
+ const platform = makeCapturingAdapter("platform", {
458
+ success: true,
459
+ remotePushAccepted: true,
460
+ });
461
+ const broadcaster = new NotificationBroadcaster([
462
+ vellum.adapter,
463
+ platform.adapter,
464
+ ]);
465
+
466
+ await expect(
467
+ broadcaster.broadcastDecision(makeSignal(), bothChannelsDecision()),
468
+ ).rejects.toThrow("pairing exploded");
469
+
470
+ // The platform adapter never ran, but the deferred vellum send must not
471
+ // be lost -- its pending delivery row would block retries forever.
472
+ expect(platform.sends.length).toBe(0);
473
+ expect(vellum.sends.length).toBe(1);
474
+ expect(vellum.sends[0]?.payload.remotePushDispatched).toBe(false);
475
+ });
476
+
477
+ test("deadline expiry flushes vellum with false while the platform dispatch finishes in the background", async () => {
478
+ bothChannelsCopy();
479
+
480
+ let resolvePlatform: ((result: DeliveryResult) => void) | undefined;
481
+ const platformSends: CapturedSend[] = [];
482
+ const platform: ChannelAdapter = {
483
+ channel: "platform",
484
+ send(
485
+ payload: ChannelDeliveryPayload,
486
+ destination: ChannelDestination,
487
+ observer?: ChannelDeliveryObserver,
488
+ ): Promise<DeliveryResult> {
489
+ platformSends.push({ payload, destination });
490
+ observer?.onRemotePushPlatforms(["ios"]);
491
+ return new Promise<DeliveryResult>((resolve) => {
492
+ resolvePlatform = resolve;
493
+ });
494
+ },
495
+ };
496
+ const vellum = makeCapturingAdapter("vellum");
497
+ const broadcaster = new NotificationBroadcaster([vellum.adapter, platform]);
498
+
499
+ const results = await broadcaster.broadcastDecision(
500
+ makeSignal(),
501
+ bothChannelsDecision(),
502
+ { platformOutcomeDeadlineMs: 20 },
503
+ );
504
+
505
+ expect(platformSends.length).toBe(1);
506
+ expect(vellum.sends.length).toBe(1);
507
+ expect(vellum.sends[0]?.payload.remotePushDispatched).toBe(false);
508
+ expect(vellum.sends[0]?.payload.remotePushPlatforms).toEqual(["ios"]);
509
+ expect(results.find((r) => r.channel === "platform")?.status).toBe(
510
+ "pending",
511
+ );
512
+
513
+ // The dispatch settling after the deadline must not re-send vellum.
514
+ resolvePlatform?.({ success: true, remotePushAccepted: true });
515
+ await new Promise((resolve) => setTimeout(resolve, 0));
516
+ expect(vellum.sends.length).toBe(1);
517
+ expect(vellum.sends[0]?.payload.remotePushDispatched).toBe(false);
518
+ });
519
+
520
+ test("platform completing before the deadline sets the real outcome on the vellum payload", async () => {
521
+ bothChannelsCopy();
522
+
523
+ const vellum = makeCapturingAdapter("vellum");
524
+ const platform: ChannelAdapter = {
525
+ channel: "platform",
526
+ async send(): Promise<DeliveryResult> {
527
+ await new Promise((resolve) => setTimeout(resolve, 5));
528
+ return { success: true, remotePushAccepted: true };
529
+ },
530
+ };
531
+ const broadcaster = new NotificationBroadcaster([vellum.adapter, platform]);
532
+
533
+ const results = await broadcaster.broadcastDecision(
534
+ makeSignal(),
535
+ bothChannelsDecision(),
536
+ { platformOutcomeDeadlineMs: 1_000 },
537
+ );
538
+
539
+ expect(vellum.sends.length).toBe(1);
540
+ expect(vellum.sends[0]?.payload.remotePushDispatched).toBe(true);
541
+ expect(results.find((r) => r.channel === "platform")?.status).toBe("sent");
542
+ });
543
+
544
+ test("dispatches the platform adapter before the vellum intent when both are selected", async () => {
545
+ bothChannelsCopy();
546
+
547
+ const callOrder: string[] = [];
548
+ const vellum: ChannelAdapter = {
549
+ channel: "vellum",
550
+ async send(): Promise<DeliveryResult> {
551
+ callOrder.push("vellum");
552
+ return { success: true };
553
+ },
554
+ };
555
+ const platform: ChannelAdapter = {
556
+ channel: "platform",
557
+ async send(): Promise<DeliveryResult> {
558
+ callOrder.push("platform");
559
+ return { success: true, remotePushAccepted: true };
560
+ },
561
+ };
562
+ const broadcaster = new NotificationBroadcaster([vellum, platform]);
563
+
564
+ await broadcaster.broadcastDecision(makeSignal(), bothChannelsDecision());
565
+
566
+ expect(callOrder).toEqual(["platform", "vellum"]);
567
+ });
568
+
569
+ test("platform deep link still carries the vellum pairing when the vellum send is deferred", async () => {
570
+ bothChannelsCopy();
571
+
572
+ pairingByChannel = {
573
+ vellum: {
574
+ conversationId: "conv-vellum-1",
575
+ messageId: "msg-1",
576
+ strategy: "start_new_conversation",
577
+ createdNewConversation: false,
578
+ conversationFallbackUsed: false,
579
+ },
580
+ };
581
+
582
+ const vellum = makeCapturingAdapter("vellum");
583
+ const platform = makeCapturingAdapter("platform", {
584
+ success: true,
585
+ remotePushAccepted: true,
586
+ });
587
+ const broadcaster = new NotificationBroadcaster([
588
+ vellum.adapter,
589
+ platform.adapter,
590
+ ]);
591
+
592
+ await broadcaster.broadcastDecision(makeSignal(), bothChannelsDecision());
593
+
594
+ expect(platform.sends.length).toBe(1);
595
+ expect(platform.sends[0]?.payload.deepLinkTarget).toEqual({
596
+ conversationId: "conv-vellum-1",
597
+ messageId: "msg-1",
598
+ });
599
+ });
600
+ });
601
+
602
+ describe("NotificationBroadcaster forced-platform copy reuse", () => {
603
+ test("platform payload reuses the vellum rendered copy instead of template fallback", async () => {
604
+ // A urgency-forced platform channel has no rendered copy of its own; the
605
+ // template fallback must lose to the vellum channel's rendered copy.
606
+ composeFallbackReturn = {
607
+ platform: { title: "Template title", body: "Template body" },
608
+ };
609
+
610
+ const vellum = makeCapturingAdapter("vellum");
611
+ const platform = makeCapturingAdapter("platform", {
612
+ success: true,
613
+ remotePushAccepted: true,
614
+ });
615
+ const broadcaster = new NotificationBroadcaster([
616
+ vellum.adapter,
617
+ platform.adapter,
618
+ ]);
619
+
620
+ await broadcaster.broadcastDecision(
621
+ makeSignal(),
622
+ makeDecision({
623
+ selectedChannels: ["vellum", "platform"],
624
+ renderedCopy: {
625
+ vellum: { title: "Rendered title", body: "Rendered body" },
626
+ },
627
+ }),
628
+ );
629
+
630
+ expect(platform.sends.length).toBe(1);
631
+ expect(platform.sends[0]?.payload.copy.title).toBe("Rendered title");
632
+ expect(platform.sends[0]?.payload.copy.body).toBe("Rendered body");
633
+ });
634
+ });
635
+
296
636
  // The card context is built once per broadcast (adapters render only); an
297
637
  // answer-mode pending_question with structured options renders them as
298
638
  // tappable card actions in the answer-token scheme the reply router
@@ -1,7 +1,8 @@
1
1
  /**
2
- * Tests for the notification copy-composer specifically the fallback
3
- * path that composeFallbackCopy uses when the LLM is unavailable, and
4
- * the shared deriveTitle utility.
2
+ * Tests for the notification copy-composer: the fallback path that
3
+ * composeFallbackCopy uses when the LLM is unavailable, the normalization
4
+ * it applies to producer-supplied titles, and the shared deriveTitle
5
+ * utility.
5
6
  */
6
7
 
7
8
  import { describe, expect, test } from "bun:test";
@@ -190,6 +191,55 @@ describe("composeFallbackCopy honors requestedMessage / requestedTitle", () => {
190
191
  expect(copy.vellum?.title).toBe("User Title");
191
192
  });
192
193
 
194
+ test("strips markdown and quotes from a producer-supplied title", () => {
195
+ const signal = makeSignal({
196
+ contextPayload: {
197
+ requestedMessage: "The deploy finished cleanly.",
198
+ requestedTitle: '"**Deploy** finished"',
199
+ },
200
+ });
201
+ const copy = composeFallbackCopy(signal, CHANNELS);
202
+
203
+ expect(copy.vellum?.title).toBe("Deploy finished");
204
+ });
205
+
206
+ test("derives the title when requestedTitle reads as leaked prose", () => {
207
+ const signal = makeSignal({
208
+ contextPayload: {
209
+ requestedMessage: "Backup finished. Nothing needs your attention.",
210
+ requestedTitle: "I need to generate a title for this notification",
211
+ },
212
+ });
213
+ const copy = composeFallbackCopy(signal, CHANNELS);
214
+
215
+ expect(copy.vellum?.title).toBe("Backup finished.");
216
+ });
217
+
218
+ test("derives the title when requestedTitle spans multiple lines", () => {
219
+ const signal = makeSignal({
220
+ contextPayload: {
221
+ requestedMessage: "The nightly job is done.",
222
+ requestedTitle: "Nightly job\nsecond line",
223
+ },
224
+ });
225
+ const copy = composeFallbackCopy(signal, CHANNELS);
226
+
227
+ expect(copy.vellum?.title).toBe("The nightly job is done.");
228
+ });
229
+
230
+ test("truncates an over-long requestedTitle to the shared title budget", () => {
231
+ const signal = makeSignal({
232
+ contextPayload: {
233
+ requestedMessage: "Body text",
234
+ requestedTitle:
235
+ "Alpha bravo charlie delta echo foxtrot golf hotel india juliett",
236
+ },
237
+ });
238
+ const copy = composeFallbackCopy(signal, CHANNELS);
239
+
240
+ expect(copy.vellum?.title).toBe("Alpha bravo charlie delta echo");
241
+ });
242
+
193
243
  test("works with non-assistant_tool source channels", () => {
194
244
  for (const sourceChannel of ["scheduler", "watcher", "slack"] as const) {
195
245
  const signal = makeSignal({