@vellumai/assistant 0.12.5 → 0.12.6-dev.202609292116.41f7caa

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 (295) hide show
  1. package/.env.example +2 -0
  2. package/ARCHITECTURE.md +26 -3
  3. package/docs/automatic-done.md +32 -0
  4. package/docs/desktop-browser-cli.md +3 -2
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
  9. package/node_modules/@vellumai/environments/src/shell.test.ts +13 -1
  10. package/node_modules/@vellumai/environments/src/shell.ts +22 -1
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
  15. package/node_modules/@vellumai/gateway-client/src/__tests__/status-contracts.test.ts +39 -0
  16. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +25 -0
  17. package/node_modules/@vellumai/service-contracts/package.json +2 -1
  18. package/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
  19. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  20. package/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
  21. package/openapi.yaml +270 -10
  22. package/package.json +1 -1
  23. package/src/__tests__/agent-loop.test.ts +76 -0
  24. package/src/__tests__/anthropic-provider.test.ts +2 -0
  25. package/src/__tests__/byok-default-profile-ensure.test.ts +7 -0
  26. package/src/__tests__/config-get-vision-flag.test.ts +1 -1
  27. package/src/__tests__/config-loader-validation-reset-notice.test.ts +2 -2
  28. package/src/__tests__/conversation-agent-loop-overflow.test.ts +160 -154
  29. package/src/__tests__/conversation-evictor.test.ts +54 -0
  30. package/src/__tests__/conversation-pairing.test.ts +112 -0
  31. package/src/__tests__/document-append-idempotency.test.ts +13 -0
  32. package/src/__tests__/document-create-dedupe.test.ts +13 -0
  33. package/src/__tests__/document-find-replace.test.ts +13 -0
  34. package/src/__tests__/document-revision-history.test.ts +390 -0
  35. package/src/__tests__/document-revision.test.ts +425 -0
  36. package/src/__tests__/document-tool-security.test.ts +13 -0
  37. package/src/__tests__/document-update-default-surface.test.ts +13 -0
  38. package/src/__tests__/emit-signal-routing-intent.test.ts +270 -0
  39. package/src/__tests__/gateway-flag-listener.test.ts +59 -6
  40. package/src/__tests__/headless-browser-navigate.test.ts +20 -30
  41. package/src/__tests__/host-cu-proxy.test.ts +79 -0
  42. package/src/__tests__/host-cu-routes-targeted.test.ts +14 -5
  43. package/src/__tests__/host-shell-tool.test.ts +6 -2
  44. package/src/__tests__/identity-routes.test.ts +4 -3
  45. package/src/__tests__/list-messages-client-message-id.test.ts +43 -0
  46. package/src/__tests__/llm-catalog-parity.test.ts +4 -4
  47. package/src/__tests__/llm-schema.test.ts +1 -1
  48. package/src/__tests__/notification-platform-adapter.test.ts +83 -1
  49. package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
  50. package/src/__tests__/openai-provider.test.ts +38 -11
  51. package/src/__tests__/pricing.test.ts +11 -0
  52. package/src/__tests__/public-ingress-urls.test.ts +26 -0
  53. package/src/__tests__/retry-thinking-tool-choice.test.ts +47 -0
  54. package/src/__tests__/skill-execute-input.test.ts +11 -0
  55. package/src/__tests__/tool-approval-handler.test.ts +4 -0
  56. package/src/__tests__/tool-preview-lifecycle.test.ts +26 -0
  57. package/src/__tests__/usage-attribution.test.ts +31 -0
  58. package/src/__tests__/workspace-migration-133-collapse-provider-connections.test.ts +3 -3
  59. package/src/__tests__/workspace-migration-160-repair-retired-fireworks-model-ids.test.ts +262 -0
  60. package/src/__tests__/workspace-migration-161-repair-retired-fireworks-glm-5p2-model-id.test.ts +233 -0
  61. package/src/acp/agent-process.ts +6 -1
  62. package/src/acp/client-handler.ts +6 -1
  63. package/src/agent/loop-unparseable-tool-retry.test.ts +333 -0
  64. package/src/agent/loop.ts +161 -17
  65. package/src/api/events/document-editor-show.ts +5 -0
  66. package/src/api/events/document-editor-update.ts +10 -0
  67. package/src/api/events/tool-result.ts +1 -0
  68. package/src/calls/__tests__/call-controller.test.ts +273 -1
  69. package/src/calls/__tests__/media-stream-server-integration.test.ts +40 -0
  70. package/src/calls/__tests__/media-stream-stt-session.test.ts +33 -0
  71. package/src/calls/__tests__/voice-session-bridge.test.ts +54 -0
  72. package/src/calls/call-constants.ts +10 -0
  73. package/src/calls/call-controller.ts +359 -25
  74. package/src/calls/channel-admission-reader.ts +15 -9
  75. package/src/calls/media-stream-server.ts +18 -0
  76. package/src/calls/media-stream-stt-session.ts +30 -9
  77. package/src/calls/twilio-provider.ts +14 -31
  78. package/src/calls/voice-session-bridge.ts +18 -1
  79. package/src/cli/commands/browser.ts +1 -2
  80. package/src/cli/lib/__tests__/plugin-artifact.test.ts +23 -0
  81. package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +2 -0
  82. package/src/cli/lib/__tests__/plugin-details.test.ts +16 -0
  83. package/src/cli/lib/bundled-marketplace.json +54 -2
  84. package/src/cli/lib/inspect-plugin.ts +15 -15
  85. package/src/cli/lib/list-installed-plugins.ts +29 -28
  86. package/src/cli/lib/plugin-artifact.ts +74 -63
  87. package/src/cli/lib/plugin-catalog-visibility.ts +1 -0
  88. package/src/cli/lib/plugin-details.ts +27 -42
  89. package/src/cli/lib/plugin-marketplace.ts +6 -8
  90. package/src/cli/lib/plugin-surfaces.ts +8 -7
  91. package/src/cli.ts +1 -3
  92. package/src/config/__tests__/chat-settings.test.ts +121 -0
  93. package/src/config/assistant-feature-flags.ts +28 -23
  94. package/src/config/bundled-skills/computer-use/SKILL.md +20 -0
  95. package/src/config/bundled-skills/computer-use/TOOLS.json +11 -11
  96. package/src/config/chat-settings.ts +100 -0
  97. package/src/config/default-profile-catalog.ts +1 -1
  98. package/src/config/feature-flag-cache.ts +7 -25
  99. package/src/config/feature-flag-registry.json +9 -9
  100. package/src/config/loader.ts +5 -1
  101. package/src/config/profile-text-generation.ts +25 -21
  102. package/src/config/schemas/conversations.ts +14 -0
  103. package/src/config/schemas/llm.ts +1 -0
  104. package/src/config/schemas/notifications.ts +8 -1
  105. package/src/contacts/gateway-channel-read.ts +1 -1
  106. package/src/conversations/__tests__/auto-archive-activity.test.ts +136 -0
  107. package/src/conversations/__tests__/auto-archive.test.ts +304 -0
  108. package/src/conversations/auto-archive-activity.ts +61 -0
  109. package/src/conversations/auto-archive.ts +201 -0
  110. package/src/daemon/computer-use-observation.test.ts +81 -0
  111. package/src/daemon/computer-use-observation.ts +23 -0
  112. package/src/daemon/conversation-agent-loop-handlers.ts +18 -2
  113. package/src/daemon/conversation-agent-loop.ts +14 -6
  114. package/src/daemon/conversation-evictor.ts +50 -16
  115. package/src/daemon/conversation-messaging.ts +4 -0
  116. package/src/daemon/conversation-process.ts +15 -0
  117. package/src/daemon/conversation-queue-manager.ts +2 -0
  118. package/src/daemon/conversation.ts +2 -0
  119. package/src/daemon/doordash-steps.ts +1 -3
  120. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +43 -1
  121. package/src/daemon/handlers/__tests__/config-channels.test.ts +1 -1
  122. package/src/daemon/handlers/config-a2a.ts +75 -2
  123. package/src/daemon/handlers/shared.ts +13 -2
  124. package/src/daemon/host-cu-proxy.ts +34 -3
  125. package/src/daemon/lifecycle.ts +15 -2
  126. package/src/daemon/process-message.ts +5 -0
  127. package/src/daemon/shutdown-handlers.ts +3 -0
  128. package/src/daemon/turn-finalization.ts +4 -0
  129. package/src/desktop/desktop-browser-endpoint.ts +11 -2
  130. package/src/desktop/desktop-chrome-policy.test.ts +1 -0
  131. package/src/desktop/desktop-chrome-policy.ts +7 -1
  132. package/src/desktop/desktop-computer-use.test.ts +14 -4
  133. package/src/desktop/desktop-session-manager.ts +5 -3
  134. package/src/documents/document-revisions-store.ts +153 -0
  135. package/src/documents/document-store.ts +471 -133
  136. package/src/followups/followup-store.ts +0 -4
  137. package/src/i18n/__tests__/i18n.test.ts +25 -0
  138. package/src/i18n/assistant-text.ts +18 -0
  139. package/src/i18n/index.ts +1 -0
  140. package/src/i18n/messages.ts +11 -0
  141. package/src/inbound/__tests__/oauth-callback-url.test.ts +25 -2
  142. package/src/inbound/oauth-callback-url.ts +30 -1
  143. package/src/inbound/public-ingress-urls.ts +12 -2
  144. package/src/ipc/gateway-client.test.ts +40 -0
  145. package/src/ipc/gateway-client.ts +12 -29
  146. package/src/ipc/gateway-flag-listener.ts +7 -10
  147. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +71 -0
  148. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +83 -0
  149. package/src/live-voice/__tests__/protocol.test.ts +77 -0
  150. package/src/live-voice/__tests__/share-targets.test.ts +163 -0
  151. package/src/live-voice/live-voice-session.ts +23 -6
  152. package/src/live-voice/protocol.ts +22 -0
  153. package/src/live-voice/share-targets.ts +225 -0
  154. package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +144 -3
  155. package/src/mcp/__tests__/mcp-oauth-provider.test.ts +2 -0
  156. package/src/mcp/client.ts +1 -19
  157. package/src/mcp/mcp-oauth-provider.ts +66 -49
  158. package/src/media/openrouter-image-service.ts +39 -22
  159. package/src/monitoring/__tests__/oom-inheritance-sweep.test.ts +86 -0
  160. package/src/monitoring/__tests__/oom-kill-reporter.test.ts +374 -0
  161. package/src/monitoring/control.ts +2 -1
  162. package/src/monitoring/oom-inheritance-sweep.ts +96 -0
  163. package/src/monitoring/oom-kill-reporter.ts +469 -0
  164. package/src/monitoring/resource-sampler.ts +30 -0
  165. package/src/notifications/README.md +53 -4
  166. package/src/notifications/__tests__/assistant-initiated-thread.test.ts +254 -0
  167. package/src/notifications/__tests__/broadcaster.test.ts +558 -6
  168. package/src/notifications/__tests__/completion-policy.test.ts +37 -0
  169. package/src/notifications/__tests__/from-me-alert-integration.test.ts +192 -0
  170. package/src/notifications/adapters/platform.ts +14 -0
  171. package/src/notifications/assistant-initiated-thread.ts +101 -0
  172. package/src/notifications/broadcaster.ts +94 -16
  173. package/src/notifications/chat-reply-policy.ts +11 -0
  174. package/src/notifications/completion-policy.ts +12 -2
  175. package/src/notifications/conversation-pairing.ts +15 -70
  176. package/src/notifications/emit-signal.ts +34 -3
  177. package/src/notifications/types.ts +2 -0
  178. package/src/oauth/connect-orchestrator.ts +1 -2
  179. package/src/persistence/__tests__/conversation-auto-archive.test.ts +330 -0
  180. package/src/persistence/conversation-auto-archive.ts +170 -0
  181. package/src/persistence/conversation-crud.ts +2 -1
  182. package/src/persistence/conversation-queries.ts +15 -8
  183. package/src/persistence/embeddings/embed.test.ts +96 -0
  184. package/src/persistence/embeddings/embed.ts +20 -2
  185. package/src/persistence/embeddings/embedding-gemini.test.ts +39 -3
  186. package/src/persistence/embeddings/embedding-gemini.ts +39 -1
  187. package/src/persistence/embeddings/embedding-local.ts +6 -0
  188. package/src/persistence/embeddings/qdrant-manager.ts +5 -0
  189. package/src/persistence/job-utils.ts +10 -0
  190. package/src/persistence/migrations/384-add-document-revision.test.ts +83 -0
  191. package/src/persistence/migrations/384-add-document-revision.ts +21 -0
  192. package/src/persistence/migrations/385-create-document-revisions.test.ts +67 -0
  193. package/src/persistence/migrations/385-create-document-revisions.ts +28 -0
  194. package/src/persistence/migrations/386-conversations-last-reopened-at.ts +10 -0
  195. package/src/persistence/migrations/__tests__/386-conversations-last-reopened-at.test.ts +32 -0
  196. package/src/persistence/schema/conversations.ts +1 -0
  197. package/src/persistence/schema/documents.ts +20 -0
  198. package/src/persistence/steps.ts +6 -0
  199. package/src/platform/managed-speech.ts +42 -46
  200. package/src/plugin-api/vision-support.test.ts +21 -21
  201. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +11 -6
  202. package/src/providers/__tests__/dispatch-connection-routing.test.ts +1 -1
  203. package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
  204. package/src/providers/__tests__/unparseable-tool-args.test.ts +25 -0
  205. package/src/providers/anthropic/client.ts +1 -1
  206. package/src/providers/fireworks/client.test.ts +168 -0
  207. package/src/providers/fireworks/client.ts +24 -0
  208. package/src/providers/inference/adapter-factory.ts +6 -0
  209. package/src/providers/ionet/client.test.ts +143 -0
  210. package/src/providers/ionet/client.ts +37 -0
  211. package/src/providers/model-catalog.ts +141 -84
  212. package/src/providers/model-intents.ts +4 -4
  213. package/src/providers/openai/chat-completions-provider.ts +33 -2
  214. package/src/providers/retry.ts +56 -4
  215. package/src/providers/search-provider-catalog.ts +10 -0
  216. package/src/providers/types.ts +10 -2
  217. package/src/providers/unparseable-tool-args.ts +29 -5
  218. package/src/providers/usage-tracking.ts +5 -3
  219. package/src/runtime/assistant-event-hub.ts +23 -0
  220. package/src/runtime/auth/__tests__/guard-tests.test.ts +0 -7
  221. package/src/runtime/auth/__tests__/route-policy.test.ts +1 -1
  222. package/src/runtime/channel-reply-delivery.ts +2 -0
  223. package/src/runtime/channel-retry-sweep.ts +1 -2
  224. package/src/runtime/http-types.ts +3 -0
  225. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +35 -10
  226. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +48 -0
  227. package/src/runtime/migrations/vbundle-builder.ts +19 -3
  228. package/src/runtime/migrations/vbundle-streaming-importer.ts +14 -19
  229. package/src/runtime/migrations/vbundle-streaming-validator.ts +14 -7
  230. package/src/runtime/migrations/vbundle-validator.ts +54 -1
  231. package/src/runtime/pending-interactions.ts +20 -2
  232. package/src/runtime/routes/__tests__/contact-routes.test.ts +5 -3
  233. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +36 -0
  234. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +375 -2
  235. package/src/runtime/routes/__tests__/inference-send-routes.test.ts +1 -1
  236. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +12 -0
  237. package/src/runtime/routes/contact-routes.ts +2 -2
  238. package/src/runtime/routes/conversation-list-routes.ts +10 -2
  239. package/src/runtime/routes/conversation-query-routes.ts +48 -10
  240. package/src/runtime/routes/conversation-routes.ts +7 -0
  241. package/src/runtime/routes/documents-routes.ts +185 -47
  242. package/src/runtime/routes/events-routes.ts +1 -3
  243. package/src/runtime/routes/host-app-control-routes.ts +1 -0
  244. package/src/runtime/routes/host-bash-routes.ts +1 -0
  245. package/src/runtime/routes/host-browser-routes.ts +15 -0
  246. package/src/runtime/routes/host-cu-routes.ts +4 -1
  247. package/src/runtime/routes/host-file-routes.ts +1 -0
  248. package/src/runtime/routes/host-proxy-result-binding.test.ts +83 -0
  249. package/src/runtime/routes/host-proxy-result-binding.ts +25 -0
  250. package/src/runtime/routes/host-transfer-routes.ts +1 -0
  251. package/src/runtime/routes/identity-routes.ts +3 -3
  252. package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -0
  253. package/src/runtime/routes/integrations/a2a.ts +4 -1
  254. package/src/runtime/routes/migration-routes.ts +3 -1
  255. package/src/runtime/routes/notification-routes.ts +1 -2
  256. package/src/runtime/routes/ui-snapshot-routes.test.ts +1 -0
  257. package/src/runtime/routes/user-route-dispatcher.ts +5 -1
  258. package/src/runtime/verification-templates.ts +1 -2
  259. package/src/sequence/analytics.ts +0 -4
  260. package/src/sequence/engine.ts +1 -11
  261. package/src/sequence/guardrails.ts +1 -6
  262. package/src/sequence/importer.ts +0 -8
  263. package/src/sequence/store.ts +0 -10
  264. package/src/signals/user-message.ts +0 -6
  265. package/src/skills/catalog-install.ts +52 -23
  266. package/src/tools/browser/browser-execution.ts +20 -23
  267. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +1 -3
  268. package/src/tools/document/document-tool.ts +13 -11
  269. package/src/tools/executor.ts +1 -2
  270. package/src/tools/host-terminal/host-shell.ts +4 -2
  271. package/src/tools/network/__tests__/web-search.test.ts +105 -0
  272. package/src/tools/network/web-fetch.ts +2 -4
  273. package/src/tools/network/web-search.ts +239 -12
  274. package/src/tools/shared/filesystem/format-diff.ts +1 -2
  275. package/src/tools/shared/shell-output.test.ts +42 -0
  276. package/src/tools/shared/shell-output.ts +27 -4
  277. package/src/tools/skills/execute.ts +10 -7
  278. package/src/tools/terminal/shell.ts +3 -3
  279. package/src/tools/tool-approval-handler.ts +9 -1
  280. package/src/tools/ui-surface/visual-validation.ts +1 -2
  281. package/src/usage/attribution.ts +18 -1
  282. package/src/util/__tests__/oom-priority.test.ts +55 -0
  283. package/src/util/__tests__/worker-memory.test.ts +16 -0
  284. package/src/util/debounce.ts +0 -4
  285. package/src/util/oom-priority.ts +79 -0
  286. package/src/util/platform.ts +0 -2
  287. package/src/util/plugin-manifest.ts +5 -7
  288. package/src/util/pricing.ts +25 -0
  289. package/src/util/worker-memory.ts +9 -3
  290. package/src/util/worker-process.ts +8 -0
  291. package/src/workspace/byok-default-profile-ensure.ts +11 -0
  292. package/src/workspace/hatched-date.ts +11 -4
  293. package/src/workspace/migrations/160-repair-retired-fireworks-model-ids.ts +209 -0
  294. package/src/workspace/migrations/161-repair-retired-fireworks-glm-5p2-model-id.ts +197 -0
  295. package/src/workspace/migrations/registry.ts +4 -0
@@ -7,7 +7,9 @@
7
7
 
8
8
  import { beforeEach, describe, expect, mock, test } from "bun:test";
9
9
 
10
- import type { PairingResult } from "../conversation-pairing.js";
10
+ import { saveRawConfig } from "../../config/loader.js";
11
+ import type { PairingOptions, PairingResult } from "../conversation-pairing.js";
12
+ import type { NotificationDeliveryRow } from "../deliveries-store.js";
11
13
  import type { NotificationSignal } from "../signal.js";
12
14
  import type {
13
15
  ChannelAdapter,
@@ -33,11 +35,15 @@ mock.module("../copy-composer.js", () => ({
33
35
 
34
36
  // Stub only getGuardianDelivery; keep the real selectors so this mock is
35
37
  // harmless if it leaks into destination-resolver.test.ts under a shared run.
38
+ let onGuardianRead: () => Promise<void> = async () => {};
36
39
  const realGuardianReader =
37
40
  await import("../../contacts/guardian-delivery-reader.js");
38
41
  mock.module("../../contacts/guardian-delivery-reader.js", () => ({
39
42
  ...realGuardianReader,
40
- getGuardianDelivery: async () => null,
43
+ getGuardianDelivery: async () => {
44
+ await onGuardianRead();
45
+ return null;
46
+ },
41
47
  }));
42
48
 
43
49
  function defaultPairing(): PairingResult {
@@ -53,10 +59,22 @@ function defaultPairing(): PairingResult {
53
59
  let pairingByChannel: Record<string, PairingResult> = {};
54
60
  let pairingErrorByChannel: Record<string, Error> = {};
55
61
  let pairedChannels: string[] = [];
62
+ let pairingInputs: Array<{
63
+ signal: NotificationSignal;
64
+ options?: PairingOptions;
65
+ }> = [];
66
+ let onPairing: (channel: string) => Promise<void> = async () => {};
56
67
 
57
68
  mock.module("../conversation-pairing.js", () => ({
58
- pairDeliveryWithConversation: async (_signal: unknown, channel: string) => {
69
+ pairDeliveryWithConversation: async (
70
+ signal: NotificationSignal,
71
+ channel: string,
72
+ _copy: unknown,
73
+ options?: PairingOptions,
74
+ ) => {
75
+ pairingInputs.push({ signal, options });
59
76
  pairedChannels.push(channel);
77
+ await onPairing(channel);
60
78
  const error = pairingErrorByChannel[channel];
61
79
  if (error) {
62
80
  throw error;
@@ -71,6 +89,10 @@ let updateDeliveryStatusImpl: (
71
89
  status: string,
72
90
  patch?: { messageId?: string; canonicalMessageId?: string },
73
91
  ) => void = () => {};
92
+ let deliveryStatuses: string[] = [];
93
+ let existingDeliveries: Partial<
94
+ Record<NotificationChannel, Partial<NotificationDeliveryRow>>
95
+ > = {};
74
96
 
75
97
  mock.module("../deliveries-store.js", () => ({
76
98
  createDelivery: () => {},
@@ -79,8 +101,14 @@ mock.module("../deliveries-store.js", () => ({
79
101
  status: string,
80
102
  _error?: unknown,
81
103
  patch?: { messageId?: string; canonicalMessageId?: string },
82
- ) => updateDeliveryStatusImpl(status, patch),
83
- findDeliveryByDecisionAndChannel: () => undefined,
104
+ ) => {
105
+ deliveryStatuses.push(status);
106
+ updateDeliveryStatusImpl(status, patch);
107
+ },
108
+ findDeliveryByDecisionAndChannel: (
109
+ _id: string,
110
+ channel: NotificationChannel,
111
+ ) => existingDeliveries[channel],
84
112
  }));
85
113
 
86
114
  // The post-acknowledgement row write is observed, never performed: the
@@ -104,9 +132,23 @@ mock.module("../adapters/macos.js", () => ({
104
132
  // Mock conversation-crud so deep-link fallback tests can control which
105
133
  // conversation ids resolve to real rows.
106
134
  let knownConversations: Set<string> = new Set();
135
+ let persistedThreads = new Map<
136
+ string,
137
+ {
138
+ source: string;
139
+ conversationType: string;
140
+ archivedAt: number | null;
141
+ }
142
+ >();
143
+ let threadPlacement = { isPinned: false, groupId: null as string | null };
144
+ let persistedSeed = true;
107
145
  mock.module("../../persistence/conversation-crud.js", () => ({
108
146
  getConversation: (id: string) =>
109
- knownConversations.has(id) ? { id } : undefined,
147
+ persistedThreads.get(id) ??
148
+ (knownConversations.has(id) ? { id } : undefined),
149
+ getDisplayMetaForConversations: (ids: string[]) =>
150
+ new Map(ids.map((id) => [id, threadPlacement])),
151
+ getMessageById: () => (persistedSeed ? { id: "msg-new" } : null),
110
152
  }));
111
153
 
112
154
  // Mock destination-resolver so platform channel tests get a destination
@@ -198,11 +240,20 @@ function makeCapturingAdapter(
198
240
  }
199
241
 
200
242
  beforeEach(() => {
243
+ saveRawConfig({});
201
244
  composeFallbackReturn = {};
202
245
  knownConversations = new Set();
203
246
  pairingByChannel = {};
204
247
  pairingErrorByChannel = {};
205
248
  pairedChannels = [];
249
+ pairingInputs = [];
250
+ persistedThreads = new Map();
251
+ threadPlacement = { isPinned: false, groupId: null };
252
+ persistedSeed = true;
253
+ existingDeliveries = {};
254
+ onPairing = async () => {};
255
+ onGuardianRead = async () => {};
256
+ deliveryStatuses = [];
206
257
  updateDeliveryStatusImpl = () => {};
207
258
  destinationBindingContexts = {};
208
259
  destinationGuardianPrincipalId = undefined;
@@ -212,6 +263,248 @@ beforeEach(() => {
212
263
 
213
264
  // ── Tests ───────────────────────────────────────────────────────────────
214
265
 
266
+ describe("NotificationBroadcaster chat reply preference", () => {
267
+ const copy = { title: "Answer ready", body: "The result is ready." };
268
+ const reply = () => makeSignal({ sourceEventName: "chat.assistant_reply" });
269
+ const decision = () =>
270
+ makeDecision({
271
+ selectedChannels: ["vellum", "platform"],
272
+ renderedCopy: { vellum: copy, platform: copy, slack: copy },
273
+ });
274
+
275
+ beforeEach(() => {
276
+ destinationGuardianPrincipalId = "principal-1";
277
+ pairingByChannel.vellum = {
278
+ ...defaultPairing(),
279
+ conversationId: "conv-1",
280
+ messageId: "msg-1",
281
+ createdNewConversation: true,
282
+ };
283
+ });
284
+
285
+ test("keeps pairing and external delivery while suppressing local and native alerts", async () => {
286
+ saveRawConfig({ notifications: { newMessageEnabled: false } });
287
+ const local = makeCapturingAdapter("vellum");
288
+ const mobile = makeCapturingAdapter("platform", {
289
+ success: true,
290
+ remotePushAccepted: true,
291
+ });
292
+ const slack = makeCapturingAdapter("slack");
293
+ const broadcaster = new NotificationBroadcaster([
294
+ local.adapter,
295
+ mobile.adapter,
296
+ slack.adapter,
297
+ ]);
298
+ const paired = mock(() => {});
299
+ broadcaster.setOnConversationCreated(paired);
300
+ const results = await broadcaster.broadcastDecision(reply(), {
301
+ ...decision(),
302
+ selectedChannels: ["vellum", "platform", "slack"],
303
+ });
304
+
305
+ expect(local.sends[0]?.payload).toMatchObject({
306
+ silent: true,
307
+ remotePushDispatched: false,
308
+ deepLinkTarget: { conversationId: "conv-1", messageId: "msg-1" },
309
+ });
310
+ expect(paired).toHaveBeenCalledWith(
311
+ expect.objectContaining({ conversationId: "conv-1", silent: true }),
312
+ );
313
+ expect(mobile.sends).toHaveLength(0);
314
+ expect(slack.sends).toHaveLength(1);
315
+ expect(results).toEqual([
316
+ expect.objectContaining({ channel: "platform", status: "skipped" }),
317
+ expect.objectContaining({
318
+ channel: "vellum",
319
+ status: "sent",
320
+ messageId: "msg-1",
321
+ }),
322
+ expect.objectContaining({ channel: "slack", status: "sent" }),
323
+ ]);
324
+ expect(deliveryStatuses).toEqual(["skipped", "sent", "sent"]);
325
+ });
326
+
327
+ test.each([
328
+ "schedule.result",
329
+ "schedule.notify",
330
+ "activity.complete",
331
+ "guardian.question",
332
+ "ingress.access_request",
333
+ "chat.assistant_reply.extra",
334
+ ])("does not suppress %s", async (sourceEventName) => {
335
+ saveRawConfig({ notifications: { newMessageEnabled: false } });
336
+ const local = makeCapturingAdapter("vellum");
337
+ const mobile = makeCapturingAdapter("platform");
338
+ await new NotificationBroadcaster([
339
+ local.adapter,
340
+ mobile.adapter,
341
+ ]).broadcastDecision(
342
+ makeSignal({
343
+ sourceEventName,
344
+ attentionHints: { ...reply().attentionHints, urgency: "critical" },
345
+ contextPayload: {
346
+ completion: {
347
+ workId: "task-1",
348
+ conversationId: "conv-1",
349
+ recipientPrincipalId: "principal-1",
350
+ owner: "parent_continuation",
351
+ },
352
+ },
353
+ }),
354
+ decision(),
355
+ );
356
+ expect(local.sends[0]?.payload.silent).toBe(false);
357
+ expect(mobile.sends).toHaveLength(1);
358
+ });
359
+
360
+ test("rechecks the preference after pairing and an awaited creation callback", async () => {
361
+ const local = makeCapturingAdapter("vellum");
362
+ const mobile = makeCapturingAdapter("platform");
363
+ const broadcaster = new NotificationBroadcaster([
364
+ local.adapter,
365
+ mobile.adapter,
366
+ ]);
367
+ const created = mock(() => {});
368
+ broadcaster.setOnConversationCreated(created);
369
+ await broadcaster.broadcastDecision(reply(), decision(), {
370
+ onConversationCreated: async () => {
371
+ saveRawConfig({ notifications: { newMessageEnabled: false } });
372
+ },
373
+ });
374
+
375
+ expect(created).toHaveBeenCalledWith(
376
+ expect.objectContaining({ silent: true }),
377
+ );
378
+ expect(local.sends[0]?.payload.silent).toBe(true);
379
+ expect(mobile.sends).toHaveLength(0);
380
+ });
381
+
382
+ test.each(["guardians", "vellum", "platform"])(
383
+ "delivers a reply re-enabled during %s preparation",
384
+ async (stage) => {
385
+ saveRawConfig({ notifications: { newMessageEnabled: false } });
386
+ const enable = async () => {
387
+ await Promise.resolve();
388
+ saveRawConfig({ notifications: { newMessageEnabled: true } });
389
+ };
390
+ onGuardianRead = async () => {
391
+ if (stage === "guardians") {
392
+ await enable();
393
+ }
394
+ };
395
+ onPairing = async (channel) => {
396
+ if (channel === stage) {
397
+ await enable();
398
+ }
399
+ };
400
+ const local = makeCapturingAdapter("vellum");
401
+ const mobile = makeCapturingAdapter("platform", {
402
+ success: true,
403
+ remotePushAccepted: true,
404
+ remotePushPlatforms: ["ios"],
405
+ });
406
+ const results = await new NotificationBroadcaster([
407
+ local.adapter,
408
+ mobile.adapter,
409
+ ]).broadcastDecision(reply(), decision());
410
+
411
+ expect(mobile.sends).toHaveLength(1);
412
+ expect(local.sends[0]?.payload).toMatchObject({
413
+ silent: false,
414
+ remotePushDispatched: true,
415
+ remotePushPlatforms: ["ios"],
416
+ });
417
+ expect(results[0]).toMatchObject({
418
+ channel: "platform",
419
+ status: "sent",
420
+ });
421
+ expect(deliveryStatuses).toEqual(["sent", "sent"]);
422
+ },
423
+ );
424
+
425
+ test("rechecks after platform pairing and silently flushes the deferred local intent", async () => {
426
+ onPairing = async (channel) => {
427
+ if (channel === "platform") {
428
+ saveRawConfig({ notifications: { newMessageEnabled: false } });
429
+ }
430
+ };
431
+ const local = makeCapturingAdapter("vellum");
432
+ const mobile = makeCapturingAdapter("platform");
433
+ const results = await new NotificationBroadcaster([
434
+ local.adapter,
435
+ mobile.adapter,
436
+ ]).broadcastDecision(reply(), decision());
437
+
438
+ expect(mobile.sends).toHaveLength(0);
439
+ expect(local.sends[0]?.payload).toMatchObject({
440
+ silent: true,
441
+ remotePushDispatched: false,
442
+ });
443
+ expect(results[0]).toMatchObject({
444
+ channel: "platform",
445
+ status: "skipped",
446
+ });
447
+ });
448
+
449
+ test("keeps an accepted push truthful while silencing a deferred local intent", async () => {
450
+ const local = makeCapturingAdapter("vellum");
451
+ const mobile: ChannelAdapter = {
452
+ channel: "platform",
453
+ async send() {
454
+ saveRawConfig({ notifications: { newMessageEnabled: false } });
455
+ return {
456
+ success: true,
457
+ remotePushAccepted: true,
458
+ remotePushPlatforms: ["ios"],
459
+ };
460
+ },
461
+ };
462
+ await new NotificationBroadcaster([
463
+ local.adapter,
464
+ mobile,
465
+ ]).broadcastDecision(reply(), decision());
466
+ expect(local.sends[0]?.payload).toMatchObject({
467
+ silent: true,
468
+ remotePushDispatched: true,
469
+ remotePushPlatforms: ["ios"],
470
+ });
471
+ });
472
+
473
+ test("records an adapter suppression as skipped without claiming a push", async () => {
474
+ const local = makeCapturingAdapter("vellum");
475
+ const mobile = makeCapturingAdapter("platform", {
476
+ success: false,
477
+ skipped: true,
478
+ error: "Chat reply notifications are disabled",
479
+ remotePushAccepted: false,
480
+ });
481
+ const results = await new NotificationBroadcaster([
482
+ local.adapter,
483
+ mobile.adapter,
484
+ ]).broadcastDecision(reply(), decision());
485
+ expect(results[0]).toMatchObject({
486
+ channel: "platform",
487
+ status: "skipped",
488
+ });
489
+ expect(deliveryStatuses).toEqual(["skipped", "sent"]);
490
+ expect(local.sends[0]?.payload.remotePushDispatched).toBe(false);
491
+ });
492
+
493
+ test("reads re-enabled settings for a delayed decision", async () => {
494
+ saveRawConfig({ notifications: { newMessageEnabled: false } });
495
+ const selected = decision();
496
+ saveRawConfig({ notifications: { newMessageEnabled: true } });
497
+ const local = makeCapturingAdapter("vellum");
498
+ const mobile = makeCapturingAdapter("platform");
499
+ await new NotificationBroadcaster([
500
+ local.adapter,
501
+ mobile.adapter,
502
+ ]).broadcastDecision(reply(), selected);
503
+ expect(local.sends[0]?.payload.silent).toBe(false);
504
+ expect(mobile.sends).toHaveLength(1);
505
+ });
506
+ });
507
+
215
508
  describe("NotificationBroadcaster completion delivery", () => {
216
509
  test.each([
217
510
  undefined,
@@ -1249,3 +1542,262 @@ describe("NotificationBroadcaster question option actions", () => {
1249
1542
  ]);
1250
1543
  });
1251
1544
  });
1545
+
1546
+ describe("NotificationBroadcaster new From me threads", () => {
1547
+ const copy = { title: "A useful update", body: "Here is the result." };
1548
+ const share = () =>
1549
+ makeSignal({
1550
+ sourceChannel: "assistant_tool",
1551
+ sourceContextId: "conv-background",
1552
+ attentionHints: {
1553
+ urgency: "low",
1554
+ requiresAction: false,
1555
+ isAsyncBackground: true,
1556
+ visibleInSourceNow: false,
1557
+ },
1558
+ });
1559
+ const decision = () =>
1560
+ makeDecision({
1561
+ selectedChannels: ["vellum", "platform"],
1562
+ renderedCopy: { vellum: copy },
1563
+ });
1564
+ const options = (signal: NotificationSignal, platformAdded = true) => ({
1565
+ assistantInitiatedThread: {
1566
+ resolution: {
1567
+ isCandidate: true,
1568
+ vellumSignal: {
1569
+ ...signal,
1570
+ requiresConversation: true,
1571
+ conversationMetadata: { source: "assistant_initiated" },
1572
+ },
1573
+ },
1574
+ platformAdded,
1575
+ },
1576
+ });
1577
+
1578
+ beforeEach(() => {
1579
+ pairingByChannel.vellum = {
1580
+ ...defaultPairing(),
1581
+ conversationId: "conv-new",
1582
+ messageId: "msg-new",
1583
+ createdNewConversation: true,
1584
+ };
1585
+ persistedThreads.set("conv-new", {
1586
+ source: "assistant_initiated",
1587
+ conversationType: "standard",
1588
+ archivedAt: null,
1589
+ });
1590
+ knownConversations.add("conv-background");
1591
+ });
1592
+
1593
+ test("alerts only after creation and shares canonical IDs, copy, urgency, and source metadata", async () => {
1594
+ const vellum = makeCapturingAdapter("vellum");
1595
+ const platform = makeCapturingAdapter("platform", {
1596
+ success: true,
1597
+ remotePushAccepted: true,
1598
+ remotePushPlatforms: ["ios"],
1599
+ });
1600
+ const broadcaster = new NotificationBroadcaster([
1601
+ platform.adapter,
1602
+ vellum.adapter,
1603
+ ]);
1604
+ const events: Array<{ silent: boolean; source?: string }> = [];
1605
+ broadcaster.setOnConversationCreated((info) => {
1606
+ events.push(info);
1607
+ });
1608
+ const signal = share();
1609
+ const dispatchOptions = options(signal);
1610
+ await broadcaster.broadcastDecision(signal, decision(), dispatchOptions);
1611
+
1612
+ expect(events).toEqual([
1613
+ expect.objectContaining({ source: "assistant_initiated", silent: false }),
1614
+ ]);
1615
+ expect(vellum.sends[0]?.payload).toMatchObject({
1616
+ silent: false,
1617
+ urgency: "low",
1618
+ remotePushDispatched: true,
1619
+ remotePushPlatforms: ["ios"],
1620
+ correlationId: signal.signalId,
1621
+ deepLinkTarget: { conversationId: "conv-new", messageId: "msg-new" },
1622
+ });
1623
+ expect(platform.sends[0]?.payload).toMatchObject({
1624
+ urgency: "low",
1625
+ copy,
1626
+ correlationId: signal.signalId,
1627
+ deepLinkTarget: { conversationId: "conv-new", messageId: "msg-new" },
1628
+ });
1629
+ expect(pairingInputs[0]?.options?.assistantInitiatedThread).toBe(
1630
+ dispatchOptions.assistantInitiatedThread.resolution,
1631
+ );
1632
+ expect(pairingInputs[1]?.options?.assistantInitiatedThread).toBeUndefined();
1633
+ expect(pairingInputs.every((input) => input.signal === signal)).toBe(true);
1634
+ expect(signal.conversationMetadata).toBeUndefined();
1635
+ });
1636
+
1637
+ test.each([
1638
+ "reuse",
1639
+ "missing_conversation",
1640
+ "missing_seed",
1641
+ "unpersisted_seed",
1642
+ "grouped",
1643
+ "pinned",
1644
+ "archived",
1645
+ "flag_off",
1646
+ "quiet",
1647
+ ])(
1648
+ "%s retains baseline silence and skips only feature-added push",
1649
+ async (scenario) => {
1650
+ if (scenario === "reuse") {
1651
+ pairingByChannel.vellum!.createdNewConversation = false;
1652
+ }
1653
+ if (scenario === "missing_conversation") {
1654
+ persistedThreads.clear();
1655
+ }
1656
+ if (scenario === "missing_seed") {
1657
+ pairingByChannel.vellum!.messageId = null;
1658
+ }
1659
+ if (scenario === "unpersisted_seed") {
1660
+ persistedSeed = false;
1661
+ }
1662
+ if (scenario === "grouped") {
1663
+ threadPlacement.groupId = "group-123";
1664
+ }
1665
+ if (scenario === "pinned") {
1666
+ threadPlacement.isPinned = true;
1667
+ }
1668
+ if (scenario === "archived") {
1669
+ persistedThreads.get("conv-new")!.archivedAt = 1;
1670
+ }
1671
+ const signal = share();
1672
+ if (scenario === "quiet") {
1673
+ signal.contextPayload.quiet = true;
1674
+ }
1675
+ const dispatchOptions = options(signal);
1676
+ if (scenario === "flag_off") {
1677
+ dispatchOptions.assistantInitiatedThread.resolution.isCandidate = false;
1678
+ }
1679
+ const vellum = makeCapturingAdapter("vellum");
1680
+ const platform = makeCapturingAdapter("platform");
1681
+ const results = await new NotificationBroadcaster([
1682
+ vellum.adapter,
1683
+ platform.adapter,
1684
+ ]).broadcastDecision(signal, decision(), dispatchOptions);
1685
+ expect(vellum.sends[0]?.payload.silent).toBe(true);
1686
+ expect(platform.sends).toHaveLength(0);
1687
+ expect(results).toContainEqual(
1688
+ expect.objectContaining({
1689
+ channel: "platform",
1690
+ status: "skipped",
1691
+ errorMessage: "No newly persisted From me conversation for push",
1692
+ }),
1693
+ );
1694
+ },
1695
+ );
1696
+
1697
+ test("failed pairing never sends feature push to the hidden producer", async () => {
1698
+ pairingByChannel.vellum = defaultPairing();
1699
+ const vellum = makeCapturingAdapter("vellum");
1700
+ const platform = makeCapturingAdapter("platform");
1701
+ const signal = share();
1702
+ await new NotificationBroadcaster([
1703
+ vellum.adapter,
1704
+ platform.adapter,
1705
+ ]).broadcastDecision(signal, decision(), options(signal));
1706
+ expect(platform.sends).toHaveLength(0);
1707
+ expect(vellum.sends[0]?.payload.silent).toBe(true);
1708
+ });
1709
+
1710
+ test("independently selected urgent push survives failed creation and quiet intent", async () => {
1711
+ pairingByChannel.vellum = defaultPairing();
1712
+ const vellum = makeCapturingAdapter("vellum");
1713
+ const platform = makeCapturingAdapter("platform");
1714
+ const signal = share();
1715
+ signal.attentionHints.urgency = "high";
1716
+ signal.contextPayload.quiet = true;
1717
+ await new NotificationBroadcaster([
1718
+ vellum.adapter,
1719
+ platform.adapter,
1720
+ ]).broadcastDecision(signal, decision(), options(signal, false));
1721
+ expect(platform.sends).toHaveLength(1);
1722
+ expect(platform.sends[0]?.payload.deepLinkTarget?.conversationId).toBe(
1723
+ "conv-background",
1724
+ );
1725
+ expect(vellum.sends[0]?.payload.silent).toBe(true);
1726
+ });
1727
+
1728
+ test("invalid reuse that creates a new thread qualifies", async () => {
1729
+ pairingByChannel.vellum!.conversationFallbackUsed = true;
1730
+ const signal = share();
1731
+ const vellum = makeCapturingAdapter("vellum");
1732
+ const platform = makeCapturingAdapter("platform");
1733
+ await new NotificationBroadcaster([
1734
+ vellum.adapter,
1735
+ platform.adapter,
1736
+ ]).broadcastDecision(
1737
+ signal,
1738
+ {
1739
+ ...decision(),
1740
+ conversationActions: {
1741
+ vellum: { action: "reuse_existing", conversationId: "conv-stale" },
1742
+ },
1743
+ },
1744
+ options(signal),
1745
+ );
1746
+ expect(vellum.sends[0]?.payload.silent).toBe(false);
1747
+ expect(platform.sends).toHaveLength(1);
1748
+ });
1749
+
1750
+ test.each([
1751
+ ["start_new", false, true],
1752
+ ["reuse_existing", true, true],
1753
+ ["reuse_existing", false, false],
1754
+ [null, false, false],
1755
+ ] as const)(
1756
+ "replay proof action=%s fallback=%s permits push=%s",
1757
+ async (action, fallback, permitted) => {
1758
+ existingDeliveries.vellum = {
1759
+ id: "delivery-original",
1760
+ conversationId: "conv-new",
1761
+ messageId: "msg-original",
1762
+ conversationStrategy: "start_new_conversation",
1763
+ conversationAction: action,
1764
+ conversationFallbackUsed: fallback ? 1 : 0,
1765
+ };
1766
+ const signal = share();
1767
+ const vellum = makeCapturingAdapter("vellum");
1768
+ const platform = makeCapturingAdapter("platform");
1769
+ await new NotificationBroadcaster([
1770
+ vellum.adapter,
1771
+ platform.adapter,
1772
+ ]).broadcastDecision(signal, decision(), options(signal));
1773
+ expect(pairedChannels).not.toContain("vellum");
1774
+ expect(vellum.sends).toHaveLength(0);
1775
+ expect(platform.sends).toHaveLength(permitted ? 1 : 0);
1776
+ if (permitted) {
1777
+ expect(platform.sends[0]?.payload.deepLinkTarget).toMatchObject({
1778
+ conversationId: "conv-new",
1779
+ messageId: "msg-original",
1780
+ });
1781
+ }
1782
+ },
1783
+ );
1784
+
1785
+ test("platform rejection preserves the local alert through the deferred send", async () => {
1786
+ const signal = share();
1787
+ const vellum = makeCapturingAdapter("vellum");
1788
+ const platform = makeCapturingAdapter("platform", {
1789
+ success: false,
1790
+ skipped: true,
1791
+ remotePushAccepted: false,
1792
+ error: "no_tokens",
1793
+ });
1794
+ await new NotificationBroadcaster([
1795
+ vellum.adapter,
1796
+ platform.adapter,
1797
+ ]).broadcastDecision(signal, decision(), options(signal));
1798
+ expect(vellum.sends[0]?.payload).toMatchObject({
1799
+ silent: false,
1800
+ remotePushDispatched: false,
1801
+ });
1802
+ });
1803
+ });
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
+ import { saveRawConfig } from "../../config/loader.js";
3
4
  import {
4
5
  hasCompletionOwnership,
5
6
  isCompletionNotification,
@@ -241,3 +242,39 @@ describe("completion local delivery policy", () => {
241
242
  },
242
243
  );
243
244
  });
245
+
246
+ describe("new From me thread presentation", () => {
247
+ const presentation = { assistantInitiatedThreadCreated: true };
248
+ const share = { sourceEventName: "assistant.share", urgency: "low" as const };
249
+
250
+ test("can alert without completion ownership or urgency escalation", () => {
251
+ expect(isLocalNotificationSilent(share)).toBe(true);
252
+ expect(isLocalNotificationSilent(share, presentation)).toBe(false);
253
+ expect(isCompletionNotification(share)).toBe(false);
254
+ expect(hasCompletionOwnership(share)).toBe(false);
255
+ });
256
+
257
+ test("quiet retains priority over new thread presentation", () => {
258
+ expect(
259
+ isLocalNotificationSilent(
260
+ { ...share, contextPayload: { quiet: true } },
261
+ presentation,
262
+ ),
263
+ ).toBe(true);
264
+ });
265
+
266
+ test("chat reply preference retains priority over presentation overrides", () => {
267
+ saveRawConfig({ notifications: { newMessageEnabled: false } });
268
+ try {
269
+ expect(
270
+ isLocalNotificationSilent(
271
+ { sourceEventName: "chat.assistant_reply", urgency: "high" },
272
+ presentation,
273
+ ),
274
+ ).toBe(true);
275
+ expect(isLocalNotificationSilent(share, presentation)).toBe(false);
276
+ } finally {
277
+ saveRawConfig({});
278
+ }
279
+ });
280
+ });