@vellumai/assistant 0.11.9 → 0.11.10-dev.202609091114.7b6342c

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 (433) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/Dockerfile +6 -12
  3. package/docs/architecture/memory.md +17 -9
  4. package/docs/guardian-request-flow.md +7 -4
  5. package/docs/runbook-trusted-contacts.md +13 -2
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +3 -1
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +2 -0
  9. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/desktop-presence.ts +11 -0
  10. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  11. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
  12. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +7 -0
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +3 -1
  14. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
  15. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +2 -0
  16. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/desktop-presence.ts +11 -0
  17. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  18. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
  19. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +7 -0
  20. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +72 -0
  21. package/node_modules/@vellumai/service-contracts/package.json +3 -1
  22. package/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
  23. package/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +2 -0
  24. package/node_modules/@vellumai/service-contracts/src/desktop-presence.ts +11 -0
  25. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  26. package/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
  27. package/node_modules/@vellumai/service-contracts/src/secret-detection.ts +7 -0
  28. package/openapi.yaml +729 -63
  29. package/package.json +1 -1
  30. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +19 -21
  31. package/src/__tests__/assistant-event-hub-machine-name.test.ts +40 -0
  32. package/src/__tests__/assistant-event-hub.test.ts +45 -0
  33. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +0 -1
  34. package/src/__tests__/compactor-assistant-tail-preview.test.ts +111 -0
  35. package/src/__tests__/compactor-tail-resolution.test.ts +116 -0
  36. package/src/__tests__/computer-use-native-history.test.ts +30 -0
  37. package/src/__tests__/computer-use-window-schema.test.ts +24 -0
  38. package/src/__tests__/config-get-vision-flag.test.ts +36 -0
  39. package/src/__tests__/config-loader-validation-reset-notice.test.ts +47 -15
  40. package/src/__tests__/context-search-conversations-source.test.ts +27 -5
  41. package/src/__tests__/conversation-agent-loop-overflow.test.ts +189 -195
  42. package/src/__tests__/conversation-agent-loop.test.ts +690 -50
  43. package/src/__tests__/conversation-fork-crud.test.ts +132 -45
  44. package/src/__tests__/conversation-fork-retrospective.test.ts +9 -7
  45. package/src/__tests__/conversation-lifecycle.test.ts +684 -10
  46. package/src/__tests__/conversation-pairing.test.ts +10 -9
  47. package/src/__tests__/conversation-runtime-assembly.test.ts +288 -1
  48. package/src/__tests__/conversation-surfaces-history-restored-completion.test.ts +21 -4
  49. package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +150 -0
  50. package/src/__tests__/conversation-surfaces-point-at-capability.test.ts +161 -0
  51. package/src/__tests__/copy-composer-tc-templates.test.ts +2 -2
  52. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  53. package/src/__tests__/delivered-post-record.test.ts +107 -0
  54. package/src/__tests__/guardian-action-service-scope.test.ts +53 -0
  55. package/src/__tests__/guardian-question-mode.test.ts +88 -138
  56. package/src/__tests__/headless-browser-mode.test.ts +63 -13
  57. package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
  58. package/src/__tests__/heartbeat-service.test.ts +118 -27
  59. package/src/__tests__/host-cu-proxy.test.ts +159 -1
  60. package/src/__tests__/host-proxy-interface.test.ts +15 -0
  61. package/src/__tests__/host-proxy-preactivation.test.ts +61 -0
  62. package/src/__tests__/injector-chain.test.ts +3 -3
  63. package/src/__tests__/injector-registry-order-guard.test.ts +1 -1
  64. package/src/__tests__/injector-v3-suppression.test.ts +587 -45
  65. package/src/__tests__/llm-schema.test.ts +70 -2
  66. package/src/__tests__/logger.test.ts +33 -32
  67. package/src/__tests__/mcp-config-secret-boundary.test.ts +2 -0
  68. package/src/__tests__/memory-config-file-parity.test.ts +18 -0
  69. package/src/__tests__/memory-retrieval-hook.test.ts +53 -3
  70. package/src/__tests__/messages-read-boundary-guard.test.ts +0 -1
  71. package/src/__tests__/messaging-send-tool.test.ts +251 -128
  72. package/src/__tests__/messaging-shared.test.ts +19 -0
  73. package/src/__tests__/notification-decision-fallback.test.ts +98 -76
  74. package/src/__tests__/notification-decision-strategy.test.ts +10 -38
  75. package/src/__tests__/notification-events-source-context-probe.test.ts +211 -0
  76. package/src/__tests__/openai-responses-prompt-cache.test.ts +7 -2
  77. package/src/__tests__/openai-responses-provider.test.ts +42 -0
  78. package/src/__tests__/outlook-messaging-provider.test.ts +9 -0
  79. package/src/__tests__/platform-callback-registration.test.ts +162 -3
  80. package/src/__tests__/plugin-api-webhook-url.test.ts +66 -4
  81. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
  82. package/src/__tests__/pricing.test.ts +29 -0
  83. package/src/__tests__/prompt-cache-cross-turn-stability.test.ts +63 -3
  84. package/src/__tests__/scheduler-result-notification.test.ts +209 -0
  85. package/src/__tests__/strip-memory-injections.test.ts +75 -27
  86. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +4 -13
  87. package/src/acp/auto-install.test.ts +35 -1
  88. package/src/acp/resolve-agent.test.ts +3 -3
  89. package/src/agent/loop.ts +116 -45
  90. package/src/api/constants/personality-sliders.test.ts +21 -0
  91. package/src/api/constants/personality-sliders.ts +64 -0
  92. package/src/api/events/contact-record-request.ts +16 -4
  93. package/src/api/events/contact-request.ts +10 -1
  94. package/src/api/index.ts +12 -0
  95. package/src/api/responses/memory-v3-selection-log.ts +64 -13
  96. package/src/api/surfaces.ts +6 -5
  97. package/src/background-wake/next-wake.test.ts +13 -0
  98. package/src/background-wake/next-wake.ts +4 -2
  99. package/src/calls/__tests__/voice-session-bridge.test.ts +166 -0
  100. package/src/calls/__tests__/voice-turn-sight-hold.test.ts +301 -0
  101. package/src/calls/voice-session-bridge.ts +162 -1
  102. package/src/calls/voice-triage-escalate.ts +1 -1
  103. package/src/channels/types.ts +14 -0
  104. package/src/cli/__tests__/catalog-search-help.test.ts +1 -0
  105. package/src/cli/__tests__/contacts-address-prompt-cli.test.ts +701 -0
  106. package/src/cli/__tests__/contacts-record-prompt-cli.test.ts +276 -15
  107. package/src/cli/__tests__/roadmap-cli.test.ts +313 -0
  108. package/src/cli/__tests__/run-assistant-command.ts +59 -20
  109. package/src/cli/commands/__tests__/conversations-slack.test.ts +12 -0
  110. package/src/cli/commands/__tests__/plugins-search.test.ts +81 -0
  111. package/src/cli/commands/config.help.ts +3 -1
  112. package/src/cli/commands/contacts.help.ts +179 -12
  113. package/src/cli/commands/contacts.ts +442 -47
  114. package/src/cli/commands/memory/worker.ts +1 -1
  115. package/src/cli/commands/platform/__tests__/credits-format.test.ts +152 -0
  116. package/src/cli/commands/platform/__tests__/credits.test.ts +20 -0
  117. package/src/cli/commands/platform/credits-format.ts +108 -0
  118. package/src/cli/commands/platform/index.help.ts +33 -11
  119. package/src/cli/commands/platform/index.ts +7 -18
  120. package/src/cli/commands/plugins.help.ts +4 -1
  121. package/src/cli/commands/plugins.ts +2 -1
  122. package/src/cli/commands/roadmap.help.ts +171 -0
  123. package/src/cli/commands/roadmap.ts +358 -0
  124. package/src/cli/index.help.ts +2 -0
  125. package/src/cli/lib/__tests__/cli-output.test.ts +5 -0
  126. package/src/cli/lib/cli-output.ts +6 -6
  127. package/src/cli/program.ts +2 -0
  128. package/src/config/__tests__/webhook-routing.test.ts +42 -0
  129. package/src/config/acp-defaults.test.ts +3 -3
  130. package/src/config/acp-defaults.ts +5 -5
  131. package/src/config/bundled-skills/acp/SKILL.md +8 -11
  132. package/src/config/bundled-skills/computer-use/SKILL.md +21 -0
  133. package/src/config/bundled-skills/computer-use/TOOLS.json +6 -0
  134. package/src/config/bundled-skills/contacts/SKILL.md +61 -11
  135. package/src/config/bundled-skills/messaging/SKILL.md +2 -0
  136. package/src/config/bundled-skills/messaging/TOOLS.json +4 -4
  137. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +160 -36
  138. package/src/config/bundled-skills/messaging/tools/shared.ts +6 -0
  139. package/src/config/bundled-skills/schedule/SKILL.md +10 -2
  140. package/src/config/bundled-skills/screen-annotation/SKILL.md +80 -0
  141. package/src/config/bundled-skills/screen-annotation/TOOLS.json +73 -0
  142. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +74 -0
  143. package/src/config/bundled-skills/screen-annotation/tools/screen-clear-marks.ts +33 -0
  144. package/src/config/bundled-skills/screen-annotation/tools/screen-point-at.ts +25 -0
  145. package/src/config/env-registry.ts +2 -0
  146. package/src/config/feature-flag-registry.json +39 -7
  147. package/src/config/input-modalities.test.ts +71 -0
  148. package/src/config/input-modalities.ts +95 -0
  149. package/src/config/loader.ts +2 -0
  150. package/src/config/schemas/__tests__/memory-v3.test.ts +58 -15
  151. package/src/config/schemas/heartbeat.ts +3 -3
  152. package/src/config/schemas/llm.ts +47 -10
  153. package/src/config/schemas/memory-storage.test.ts +39 -0
  154. package/src/config/schemas/memory-storage.ts +21 -0
  155. package/src/config/schemas/memory-v3.ts +73 -46
  156. package/src/config/schemas/services.ts +18 -0
  157. package/src/config/webhook-routing.ts +12 -2
  158. package/src/context/compactor.ts +12 -13
  159. package/src/context/strip-injections.ts +29 -25
  160. package/src/context/token-estimator.ts +2 -2
  161. package/src/daemon/__tests__/embedding-reconcile.test.ts +77 -0
  162. package/src/daemon/__tests__/lifecycle-embedding-reconcile.test.ts +3 -0
  163. package/src/daemon/__tests__/turn-actor.test.ts +68 -0
  164. package/src/daemon/conversation-agent-loop-handlers.ts +152 -22
  165. package/src/daemon/conversation-agent-loop.ts +19 -3
  166. package/src/daemon/conversation-messaging.ts +3 -2
  167. package/src/daemon/conversation-runtime-assembly.ts +191 -69
  168. package/src/daemon/conversation-surfaces.ts +32 -62
  169. package/src/daemon/conversation.ts +153 -46
  170. package/src/daemon/embedding-reconcile.ts +64 -1
  171. package/src/daemon/handlers/config-embeddings.ts +31 -0
  172. package/src/daemon/host-cu-proxy.ts +66 -50
  173. package/src/daemon/host-cu-target.ts +139 -0
  174. package/src/daemon/host-proxy-preactivation.ts +29 -1
  175. package/src/daemon/mcp-reload-service.ts +6 -0
  176. package/src/daemon/providers-setup.ts +5 -26
  177. package/src/daemon/trust-context.ts +1 -1
  178. package/src/daemon/turn-actor.ts +46 -0
  179. package/src/heartbeat/heartbeat-service.ts +72 -13
  180. package/src/inbound/platform-callback-registration.ts +77 -11
  181. package/src/inbound/pod-webhook-claim.ts +35 -0
  182. package/src/inbound/velay-webhooks-gate.ts +20 -0
  183. package/src/ipc/gateway-client.ts +91 -0
  184. package/src/live-voice/__tests__/live-voice-photo.test.ts +174 -0
  185. package/src/live-voice/__tests__/live-voice-session-guardian.test.ts +127 -0
  186. package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +42 -2
  187. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +81 -0
  188. package/src/live-voice/__tests__/protocol.test.ts +67 -0
  189. package/src/live-voice/live-voice-connection.ts +14 -1
  190. package/src/live-voice/live-voice-photo.ts +109 -4
  191. package/src/live-voice/live-voice-session-manager.ts +10 -1
  192. package/src/live-voice/live-voice-session.ts +196 -31
  193. package/src/live-voice/protocol.ts +134 -1
  194. package/src/mcp/__tests__/reload-signal-emission.test.ts +54 -0
  195. package/src/mcp/__tests__/reload-signal.test.ts +57 -0
  196. package/src/mcp/__tests__/startup.test.ts +150 -0
  197. package/src/mcp/reload-signal.ts +50 -0
  198. package/src/mcp/startup.ts +101 -0
  199. package/src/messaging/providers/outlook/adapter.ts +2 -13
  200. package/src/messaging/providers/outlook/client.ts +13 -1
  201. package/src/messaging/providers/outlook/types.ts +3 -0
  202. package/src/messaging/providers/telegram-bot/adapter.ts +9 -2
  203. package/src/messaging/providers/telegram-bot/client.ts +8 -2
  204. package/src/messaging/providers/whatsapp/adapter.ts +9 -2
  205. package/src/messaging/providers/whatsapp/client.ts +8 -2
  206. package/src/notifications/AGENTS.md +5 -0
  207. package/src/notifications/README.md +21 -7
  208. package/src/notifications/__tests__/assistant-reply-producer.test.ts +3 -10
  209. package/src/notifications/__tests__/background-failure-signal.test.ts +96 -0
  210. package/src/notifications/__tests__/broadcaster.test.ts +171 -7
  211. package/src/notifications/__tests__/decision-engine.test.ts +79 -6
  212. package/src/notifications/__tests__/edit-notification.test.ts +59 -0
  213. package/src/notifications/__tests__/resolve-visible-in-source.test.ts +96 -0
  214. package/src/notifications/__tests__/schedule-result-producer.test.ts +397 -0
  215. package/src/notifications/access-request-copy.ts +5 -5
  216. package/src/notifications/approval-card-data.ts +29 -13
  217. package/src/notifications/assistant-reply-producer.ts +0 -4
  218. package/src/notifications/background-failure-signal.ts +13 -1
  219. package/src/notifications/broadcaster.ts +65 -2
  220. package/src/notifications/conversation-pairing.ts +263 -155
  221. package/src/notifications/copy-composer.ts +38 -22
  222. package/src/notifications/decision-engine.ts +89 -61
  223. package/src/notifications/delivered-post-record.ts +81 -0
  224. package/src/notifications/deliveries-store.ts +8 -1
  225. package/src/notifications/edit-notification.ts +16 -0
  226. package/src/notifications/events-store.ts +68 -2
  227. package/src/notifications/guardian-question-mode.ts +24 -232
  228. package/src/notifications/home-feed-side-effect.ts +1 -0
  229. package/src/notifications/notification-utils.ts +13 -5
  230. package/src/notifications/resolve-visible-in-source.ts +56 -0
  231. package/src/notifications/schedule-result-producer.ts +275 -0
  232. package/src/notifications/signal.ts +5 -0
  233. package/src/oauth/__tests__/seed-providers-managed.test.ts +82 -0
  234. package/src/oauth/seed-providers.ts +201 -3
  235. package/src/onboarding/onboarding-events-store.ts +14 -8
  236. package/src/persistence/conversation-crud.ts +68 -19
  237. package/src/persistence/embeddings/embedding-backend.test.ts +162 -0
  238. package/src/persistence/embeddings/embedding-backend.ts +148 -11
  239. package/src/persistence/embeddings/embedding-gemini.test.ts +328 -9
  240. package/src/persistence/embeddings/embedding-gemini.ts +211 -27
  241. package/src/persistence/embeddings/embedding-openai.test.ts +113 -0
  242. package/src/persistence/embeddings/embedding-openai.ts +55 -4
  243. package/src/persistence/embeddings/embedding-types.test.ts +24 -0
  244. package/src/persistence/embeddings/embedding-types.ts +25 -1
  245. package/src/persistence/job-utils.ts +6 -2
  246. package/src/persistence/message-metadata.ts +20 -0
  247. package/src/persistence/migrations/283-memory-v3-selections-message-id-and-sections.ts +2 -1
  248. package/src/persistence/migrations/376-notification-deliveries-canonical-message-id.ts +37 -0
  249. package/src/persistence/schema/memory-injection.ts +31 -9
  250. package/src/persistence/schema/notifications.ts +8 -0
  251. package/src/persistence/steps.ts +2 -0
  252. package/src/plugin-api/index.ts +2 -1
  253. package/src/plugin-api/vision-support.test.ts +81 -0
  254. package/src/plugin-api/vision-support.ts +57 -10
  255. package/src/plugin-api/webhook-url.ts +18 -4
  256. package/src/plugins/AGENTS.md +1 -1
  257. package/src/plugins/defaults/channel/injectors.ts +3 -1
  258. package/src/plugins/defaults/injector-order.ts +2 -2
  259. package/src/plugins/defaults/memory/AGENTS.md +263 -30
  260. package/src/plugins/defaults/memory/__tests__/conversation-memory-orphan-sweep.test.ts +4 -0
  261. package/src/plugins/defaults/memory/__tests__/conversation-memory-purge.test.ts +6 -0
  262. package/src/plugins/defaults/memory/__tests__/db-memory-attach.test.ts +9 -3
  263. package/src/plugins/defaults/memory/__tests__/memory-log-stores-degraded.test.ts +3 -7
  264. package/src/plugins/defaults/memory/__tests__/relocated-memory-test-rows.ts +18 -0
  265. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +16 -2
  266. package/src/plugins/defaults/memory/conversation-memory-purge.ts +2 -0
  267. package/src/plugins/defaults/memory/fork-conversation-memory.ts +13 -13
  268. package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +19 -12
  269. package/src/plugins/defaults/memory/graph/conversation-graph-memory.ts +17 -15
  270. package/src/plugins/defaults/memory/graph-topology/build-memory-graph.ts +1 -1
  271. package/src/plugins/defaults/memory/hooks/__tests__/post-compact.test.ts +121 -0
  272. package/src/plugins/defaults/memory/hooks/init.ts +10 -0
  273. package/src/plugins/defaults/memory/hooks/injection-metadata.ts +100 -0
  274. package/src/plugins/defaults/memory/hooks/post-compact.ts +19 -7
  275. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +11 -83
  276. package/src/plugins/defaults/memory/injectors.ts +4 -4
  277. package/src/plugins/defaults/memory/job-handlers.ts +4 -1
  278. package/src/plugins/defaults/memory/jobs/__tests__/embed-concept-page.test.ts +1 -0
  279. package/src/plugins/defaults/memory/jobs/embed-concept-page.ts +11 -3
  280. package/src/plugins/defaults/memory/memory-db.ts +4 -0
  281. package/src/plugins/defaults/memory/memory-marker.ts +28 -17
  282. package/src/plugins/defaults/memory/src/__tests__/memory-v2-simulate-route.test.ts +0 -1
  283. package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +20 -0
  284. package/src/plugins/defaults/memory/src/memory-item-routes.test.ts +0 -1
  285. package/src/plugins/defaults/memory/src/memory-worker-routes.ts +3 -1
  286. package/src/plugins/defaults/memory/substrate/__tests__/cli-command-store.test.ts +1 -1
  287. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +66 -0
  288. package/src/plugins/defaults/memory/substrate/__tests__/injected-block-slugs.test.ts +345 -0
  289. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +0 -1
  290. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +170 -1
  291. package/src/plugins/defaults/memory/substrate/__tests__/reembed-job.test.ts +1 -0
  292. package/src/plugins/defaults/memory/substrate/capability-slugs.ts +53 -0
  293. package/src/plugins/defaults/memory/substrate/cli-command-store.ts +4 -18
  294. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +40 -1
  295. package/src/plugins/defaults/memory/substrate/ingest.ts +11 -9
  296. package/src/plugins/defaults/memory/substrate/injected-block-slugs.ts +426 -42
  297. package/src/plugins/defaults/memory/substrate/page-index.ts +5 -4
  298. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +127 -14
  299. package/src/plugins/defaults/memory/substrate/skill-store.ts +1 -18
  300. package/src/plugins/defaults/memory/v2/__tests__/backfill-jobs.test.ts +1 -0
  301. package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +2 -8
  302. package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +0 -1
  303. package/src/plugins/defaults/memory/v2/injection.ts +4 -4
  304. package/src/plugins/defaults/memory/v3/__tests__/capabilities.test.ts +16 -0
  305. package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +1004 -319
  306. package/src/plugins/defaults/memory/v3/__tests__/dense.test.ts +227 -4
  307. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +836 -263
  308. package/src/plugins/defaults/memory/v3/__tests__/live-integration.test.ts +65 -38
  309. package/src/plugins/defaults/memory/v3/__tests__/maintain-job.test.ts +354 -3
  310. package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +688 -104
  311. package/src/plugins/defaults/memory/v3/__tests__/overlong-sections.test.ts +109 -0
  312. package/src/plugins/defaults/memory/v3/__tests__/page-content.test.ts +144 -0
  313. package/src/plugins/defaults/memory/v3/__tests__/plugin-schema.test.ts +534 -0
  314. package/src/plugins/defaults/memory/v3/__tests__/pool-log-store.test.ts +449 -0
  315. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +265 -27
  316. package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +34 -117
  317. package/src/plugins/defaults/memory/v3/__tests__/section-dense-store.test.ts +491 -4
  318. package/src/plugins/defaults/memory/v3/__tests__/section-needle.test.ts +124 -1
  319. package/src/plugins/defaults/memory/v3/__tests__/sections.test.ts +264 -4
  320. package/src/plugins/defaults/memory/v3/__tests__/selection-log-store.test.ts +560 -22
  321. package/src/plugins/defaults/memory/v3/__tests__/shadow-integration.test.ts +40 -33
  322. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +718 -63
  323. package/src/plugins/defaults/memory/v3/__tests__/types.test.ts +83 -2
  324. package/src/plugins/defaults/memory/v3/candidate-match.ts +1 -1
  325. package/src/plugins/defaults/memory/v3/capabilities.ts +90 -14
  326. package/src/plugins/defaults/memory/v3/card.test.ts +15 -16
  327. package/src/plugins/defaults/memory/v3/card.ts +16 -15
  328. package/src/plugins/defaults/memory/v3/dense.ts +10 -0
  329. package/src/plugins/defaults/memory/v3/ever-injected-store.test.ts +643 -140
  330. package/src/plugins/defaults/memory/v3/ever-injected-store.ts +462 -212
  331. package/src/plugins/defaults/memory/v3/hot-set.test.ts +2 -2
  332. package/src/plugins/defaults/memory/v3/injector.ts +366 -278
  333. package/src/plugins/defaults/memory/v3/learned-edges.test.ts +2 -2
  334. package/src/plugins/defaults/memory/v3/maintain-job.ts +299 -73
  335. package/src/plugins/defaults/memory/v3/orchestrate.ts +327 -214
  336. package/src/plugins/defaults/memory/v3/overlong-sections.ts +83 -0
  337. package/src/plugins/defaults/memory/v3/page-content.ts +72 -92
  338. package/src/plugins/defaults/memory/v3/plugin-schema.ts +346 -0
  339. package/src/plugins/defaults/memory/v3/pool-log-store.ts +291 -0
  340. package/src/plugins/defaults/memory/v3/pool-select.test.ts +2 -2
  341. package/src/plugins/defaults/memory/v3/pool-select.ts +160 -60
  342. package/src/plugins/defaults/memory/v3/prune.test.ts +1042 -329
  343. package/src/plugins/defaults/memory/v3/prune.ts +433 -270
  344. package/src/plugins/defaults/memory/v3/rare-term-lane.test.ts +269 -0
  345. package/src/plugins/defaults/memory/v3/rare-term-lane.ts +141 -0
  346. package/src/plugins/defaults/memory/v3/render-injection.ts +43 -82
  347. package/src/plugins/defaults/memory/v3/section-dense-store.ts +316 -28
  348. package/src/plugins/defaults/memory/v3/section-needle.ts +145 -11
  349. package/src/plugins/defaults/memory/v3/sections.ts +103 -16
  350. package/src/plugins/defaults/memory/v3/selection-log-store.ts +243 -78
  351. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +189 -68
  352. package/src/plugins/defaults/memory/v3/tuning-profile.ts +6 -0
  353. package/src/plugins/defaults/memory/v3/types.ts +169 -23
  354. package/src/plugins/defaults/memory/v3-eval/__tests__/eval-packets.test.ts +21 -4
  355. package/src/plugins/defaults/memory/v3-eval/eval-packets.ts +77 -26
  356. package/src/plugins/types.ts +14 -0
  357. package/src/providers/__tests__/connection-model-compat.test.ts +1 -0
  358. package/src/providers/__tests__/provider-secret-catalog.test.ts +4 -0
  359. package/src/providers/__tests__/xml-tool-call-salvage.test.ts +172 -0
  360. package/src/providers/inline-audio-support.test.ts +34 -0
  361. package/src/providers/inline-audio-support.ts +27 -0
  362. package/src/providers/model-catalog.ts +102 -0
  363. package/src/providers/openai/__tests__/chat-completions-extra-body.test.ts +51 -0
  364. package/src/providers/openai/__tests__/chat-completions-provider-xml-salvage.test.ts +291 -0
  365. package/src/providers/openai/chat-completions-provider.ts +114 -14
  366. package/src/providers/openai/codex-models.ts +1 -0
  367. package/src/providers/openai/responses-provider.ts +65 -15
  368. package/src/providers/opencode/client.test.ts +24 -0
  369. package/src/providers/opencode/client.ts +6 -1
  370. package/src/providers/provider-secret-catalog.ts +11 -1
  371. package/src/providers/retry.ts +6 -0
  372. package/src/providers/vellum/client.ts +14 -1
  373. package/src/providers/vellum/personality-directions.test.ts +163 -0
  374. package/src/providers/vellum/personality-directions.ts +120 -0
  375. package/src/providers/xml-tool-call-salvage.ts +212 -0
  376. package/src/runtime/AGENTS.md +7 -2
  377. package/src/runtime/__tests__/background-job-runner.test.ts +115 -0
  378. package/src/runtime/__tests__/guardian-form-registry.test.ts +0 -32
  379. package/src/runtime/__tests__/host-observe.test.ts +44 -0
  380. package/src/runtime/assistant-event-hub.ts +14 -9
  381. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -4
  382. package/src/runtime/auth/__tests__/route-policy.test.ts +65 -0
  383. package/src/runtime/auth/same-actor.ts +4 -3
  384. package/src/runtime/background-job-runner.ts +10 -0
  385. package/src/runtime/channel-reply-delivery.ts +1 -162
  386. package/src/runtime/guardian-action-service.ts +20 -1
  387. package/src/runtime/host-observe.ts +56 -6
  388. package/src/runtime/http-server.ts +35 -0
  389. package/src/runtime/outbound-post-reconciliation.ts +183 -0
  390. package/src/runtime/routes/__tests__/contact-address-prompt-route.test.ts +231 -0
  391. package/src/runtime/routes/__tests__/contact-record-prompt-route.test.ts +59 -3
  392. package/src/runtime/routes/__tests__/guardian-form-routes.test.ts +85 -0
  393. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +24 -0
  394. package/src/runtime/routes/__tests__/platform-credits-routes.test.ts +239 -0
  395. package/src/runtime/routes/__tests__/roadmap-routes.test.ts +469 -0
  396. package/src/runtime/routes/__tests__/watch-routes.test.ts +70 -1
  397. package/src/runtime/routes/__tests__/webhook-routes.test.ts +83 -0
  398. package/src/runtime/routes/contact-prompt-routes.ts +177 -28
  399. package/src/runtime/routes/conversation-query-routes.ts +53 -11
  400. package/src/runtime/routes/events-routes.ts +13 -3
  401. package/src/runtime/routes/guardian-action-routes.ts +7 -0
  402. package/src/runtime/routes/guardian-form-routes.ts +6 -4
  403. package/src/runtime/routes/heartbeat-routes.ts +41 -53
  404. package/src/runtime/routes/index.ts +2 -2
  405. package/src/runtime/routes/integrations/a2a.ts +12 -3
  406. package/src/runtime/routes/integrations/vercel.ts +13 -3
  407. package/src/runtime/routes/platform-routes.ts +175 -23
  408. package/src/runtime/routes/roadmap-routes.ts +744 -0
  409. package/src/runtime/routes/watch-routes.ts +50 -0
  410. package/src/runtime/routes/webhook-routes.ts +24 -2
  411. package/src/runtime/web-presence.ts +13 -9
  412. package/src/schedule/__tests__/schedule-timezone.test.ts +21 -3
  413. package/src/schedule/__tests__/worker-mcp-tools.test.ts +88 -0
  414. package/src/schedule/schedule-timezone.ts +25 -0
  415. package/src/schedule/scheduler.ts +19 -0
  416. package/src/schedule/worker.ts +119 -2
  417. package/src/telemetry/__tests__/live-voice-funnel.test.ts +32 -0
  418. package/src/telemetry/live-voice-funnel.ts +33 -0
  419. package/src/tools/acp/list-agents.test.ts +1 -1
  420. package/src/tools/browser/__tests__/browser-status.test.ts +3 -0
  421. package/src/tools/browser/browser-execution.ts +117 -53
  422. package/src/tools/browser/browser-status-constants.ts +6 -6
  423. package/src/tools/capability-offer.test.ts +87 -0
  424. package/src/tools/capability-offer.ts +93 -0
  425. package/src/tools/computer-use/definitions.ts +6 -0
  426. package/src/tools/computer-use/skill-proxy-bridge.ts +10 -0
  427. package/src/tools/terminal/safe-env.ts +2 -0
  428. package/src/watch/__tests__/watch-retro.test.ts +11 -10
  429. package/src/watch/__tests__/watch-session-manager.test.ts +75 -1
  430. package/src/watch/watch-retro.ts +10 -9
  431. package/src/watch/watch-session-manager.ts +17 -0
  432. package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +0 -487
  433. package/src/runtime/routes/sight-frame-routes.ts +0 -136
@@ -1,5 +1,20 @@
1
1
  import { beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
+ import { setOverridesForTesting } from "../../__tests__/feature-flag-test-helpers.js";
4
+
5
+ const PRESENCE_FLAG = "activity-presence-suppression";
6
+
7
+ let webFocused = false;
8
+ const webPresenceArgs: unknown[][] = [];
9
+ const realWebPresence = await import("../../runtime/web-presence.js");
10
+ mock.module("../../runtime/web-presence.js", () => ({
11
+ ...realWebPresence,
12
+ isWebConversationFocused: (...args: unknown[]) => {
13
+ webPresenceArgs.push(args);
14
+ return webFocused;
15
+ },
16
+ }));
17
+
3
18
  const emitCalls: Array<Record<string, unknown>> = [];
4
19
  mock.module("../emit-signal.js", () => ({
5
20
  emitNotificationSignal: (params: Record<string, unknown>) => {
@@ -105,3 +120,84 @@ describe("emitBackgroundFailureSignal", () => {
105
120
  });
106
121
  });
107
122
  });
123
+
124
+ describe("emitBackgroundFailureSignal source-active suppression", () => {
125
+ beforeEach(() => {
126
+ emitCalls.length = 0;
127
+ webFocused = false;
128
+ webPresenceArgs.length = 0;
129
+ setOverridesForTesting({ [PRESENCE_FLAG]: true });
130
+ });
131
+
132
+ function emitWithPresence(presenceConversationId: string): void {
133
+ emitBackgroundFailureSignal({
134
+ jobName: "watcher-a",
135
+ sourceChannel: "assistant_tool",
136
+ sourceContextId: "conv-3",
137
+ errorKind: "exception",
138
+ errorMessage: "boom",
139
+ presenceConversationId,
140
+ });
141
+ }
142
+
143
+ test("reports the source as visible when the carried conversation is focused", () => {
144
+ webFocused = true;
145
+
146
+ emitWithPresence("conv-3");
147
+
148
+ expect(emitCalls[0].attentionHints).toMatchObject({
149
+ visibleInSourceNow: true,
150
+ });
151
+ expect(webPresenceArgs).toEqual([["conv-3"]]);
152
+ });
153
+
154
+ test("reports the source as not visible when the conversation is unfocused", () => {
155
+ webFocused = false;
156
+
157
+ emitWithPresence("conv-3");
158
+
159
+ expect(emitCalls[0].attentionHints).toMatchObject({
160
+ visibleInSourceNow: false,
161
+ });
162
+ expect(webPresenceArgs).toEqual([["conv-3"]]);
163
+ });
164
+
165
+ test("the scheduler and sweep-job reports carry no presence conversation", () => {
166
+ webFocused = true;
167
+
168
+ emitBackgroundFailureSignal({
169
+ jobName: "schedule:job-1",
170
+ sourceChannel: "scheduler",
171
+ sourceContextId: "job-1",
172
+ errorKind: "exception",
173
+ errorMessage: "retries exhausted",
174
+ });
175
+ emitBackgroundFailureSignal({
176
+ jobName: "memory.v2.sweep",
177
+ sourceChannel: "scheduler",
178
+ sourceContextId: "job-9",
179
+ errorKind: "exception",
180
+ errorMessage: "boom",
181
+ });
182
+
183
+ expect(emitCalls).toHaveLength(2);
184
+ for (const emitted of emitCalls) {
185
+ expect(emitted.attentionHints).toMatchObject({
186
+ visibleInSourceNow: false,
187
+ });
188
+ }
189
+ expect(webPresenceArgs).toEqual([]);
190
+ });
191
+
192
+ test("the flag off keeps the hint false without reading presence", () => {
193
+ webFocused = true;
194
+ setOverridesForTesting({ [PRESENCE_FLAG]: false });
195
+
196
+ emitWithPresence("conv-3");
197
+
198
+ expect(emitCalls[0].attentionHints).toMatchObject({
199
+ visibleInSourceNow: false,
200
+ });
201
+ expect(webPresenceArgs).toEqual([]);
202
+ });
203
+ });
@@ -15,6 +15,8 @@ import type {
15
15
  ChannelDeliveryPayload,
16
16
  ChannelDestination,
17
17
  DeliveryResult,
18
+ DestinationBindingContext,
19
+ NotificationChannel,
18
20
  NotificationDecision,
19
21
  } from "../types.js";
20
22
 
@@ -62,16 +64,37 @@ mock.module("../conversation-pairing.js", () => ({
62
64
  }));
63
65
 
64
66
  // Status writes are inert by default; tests that need a failing store swap
65
- // the implementation in.
66
- let updateDeliveryStatusImpl: (status: string) => void = () => {};
67
+ // the implementation in, and tests that need the patch read it.
68
+ let updateDeliveryStatusImpl: (
69
+ status: string,
70
+ patch?: { messageId?: string; canonicalMessageId?: string },
71
+ ) => void = () => {};
67
72
 
68
73
  mock.module("../deliveries-store.js", () => ({
69
74
  createDelivery: () => {},
70
- updateDeliveryStatus: (_deliveryId: string, status: string) =>
71
- updateDeliveryStatusImpl(status),
75
+ updateDeliveryStatus: (
76
+ _deliveryId: string,
77
+ status: string,
78
+ _error?: unknown,
79
+ patch?: { messageId?: string; canonicalMessageId?: string },
80
+ ) => updateDeliveryStatusImpl(status, patch),
72
81
  findDeliveryByDecisionAndChannel: () => undefined,
73
82
  }));
74
83
 
84
+ // The post-acknowledgement row write is observed, never performed: the
85
+ // broadcaster's contract is when it calls this and with what.
86
+ let recordedPosts: Array<Record<string, unknown>> = [];
87
+ let recordDeliveredChannelPostImpl: (
88
+ post: Record<string, unknown>,
89
+ ) => Promise<{ messageId: string }> = async () => ({ messageId: "row-1" });
90
+
91
+ mock.module("../delivered-post-record.js", () => ({
92
+ recordDeliveredChannelPost: async (post: Record<string, unknown>) => {
93
+ recordedPosts.push(post);
94
+ return recordDeliveredChannelPostImpl(post);
95
+ },
96
+ }));
97
+
75
98
  mock.module("../adapters/macos.js", () => ({
76
99
  isGuardianSensitiveEvent: () => false,
77
100
  }));
@@ -85,12 +108,21 @@ mock.module("../../persistence/conversation-crud.js", () => ({
85
108
  }));
86
109
 
87
110
  // Mock destination-resolver so platform channel tests get a destination
88
- // without needing guardian-delivery data.
111
+ // without needing guardian-delivery data. A channel listed in
112
+ // `destinationBindingContexts` gets that binding context, as a resolved
113
+ // external chat would.
114
+ let destinationBindingContexts: Record<string, DestinationBindingContext> = {};
89
115
  mock.module("../destination-resolver.js", () => ({
90
116
  resolveDestinations: (channels: readonly string[], _guardians: unknown) => {
91
117
  const map = new Map();
92
118
  for (const ch of channels) {
93
- map.set(ch, { channel: ch, endpoint: ch, metadata: {} });
119
+ const bindingContext = destinationBindingContexts[ch];
120
+ map.set(ch, {
121
+ channel: ch,
122
+ endpoint: bindingContext?.externalChatId ?? ch,
123
+ metadata: {},
124
+ ...(bindingContext ? { bindingContext } : {}),
125
+ });
94
126
  }
95
127
  return map;
96
128
  },
@@ -142,7 +174,7 @@ interface CapturedSend {
142
174
  }
143
175
 
144
176
  function makeCapturingAdapter(
145
- channel: "vellum" | "platform",
177
+ channel: NotificationChannel,
146
178
  result: DeliveryResult = { success: true },
147
179
  ): {
148
180
  adapter: ChannelAdapter;
@@ -168,6 +200,9 @@ beforeEach(() => {
168
200
  pairingByChannel = {};
169
201
  pairingErrorByChannel = {};
170
202
  updateDeliveryStatusImpl = () => {};
203
+ destinationBindingContexts = {};
204
+ recordedPosts = [];
205
+ recordDeliveredChannelPostImpl = async () => ({ messageId: "row-1" });
171
206
  });
172
207
 
173
208
  // ── Tests ───────────────────────────────────────────────────────────────
@@ -271,6 +306,135 @@ describe("NotificationBroadcaster delivery status recording", () => {
271
306
  });
272
307
  });
273
308
 
309
+ describe("NotificationBroadcaster records a channel delivery after acknowledgement", () => {
310
+ const slackHome: PairingResult = {
311
+ conversationId: "conv-home",
312
+ messageId: null,
313
+ strategy: "continue_existing_conversation",
314
+ createdNewConversation: false,
315
+ conversationFallbackUsed: false,
316
+ };
317
+
318
+ test("a successful Slack send is recorded with the channel's message id, and the audit names the row", async () => {
319
+ composeFallbackReturn = {
320
+ slack: { title: "Check-in", body: "Two items need your eyes today." },
321
+ };
322
+ pairingByChannel = { slack: slackHome };
323
+ destinationBindingContexts = {
324
+ slack: { sourceChannel: "slack", externalChatId: "D0123456789" },
325
+ };
326
+ const patches: Array<{
327
+ status: string;
328
+ patch?: { messageId?: string; canonicalMessageId?: string };
329
+ }> = [];
330
+ updateDeliveryStatusImpl = (status, patch) => {
331
+ patches.push({ status, patch });
332
+ };
333
+
334
+ const { adapter, sends } = makeCapturingAdapter("slack", {
335
+ success: true,
336
+ messageId: "1756800000.000100",
337
+ });
338
+ const broadcaster = new NotificationBroadcaster([adapter]);
339
+
340
+ const results = await broadcaster.broadcastDecision(
341
+ makeSignal(),
342
+ makeDecision({ selectedChannels: ["slack"], renderedCopy: {} }),
343
+ );
344
+
345
+ expect(sends.length).toBe(1);
346
+ expect(results[0]?.status).toBe("sent");
347
+ expect(recordedPosts).toHaveLength(1);
348
+ expect(recordedPosts[0]).toEqual({
349
+ conversationId: "conv-home",
350
+ channel: "slack",
351
+ externalChatId: "D0123456789",
352
+ text: "Two items need your eyes today.",
353
+ providerMessageId: "1756800000.000100",
354
+ });
355
+ const sent = patches.find((p) => p.status === "sent");
356
+ expect(sent?.patch).toEqual({
357
+ messageId: "1756800000.000100",
358
+ canonicalMessageId: "row-1",
359
+ });
360
+ });
361
+
362
+ test("a failed Slack send records nothing", async () => {
363
+ composeFallbackReturn = {
364
+ slack: { title: "Check-in", body: "Two items need your eyes today." },
365
+ };
366
+ pairingByChannel = { slack: slackHome };
367
+ destinationBindingContexts = {
368
+ slack: { sourceChannel: "slack", externalChatId: "D0123456789" },
369
+ };
370
+
371
+ const { adapter } = makeCapturingAdapter("slack", {
372
+ success: false,
373
+ error: "channel_not_found",
374
+ });
375
+ const broadcaster = new NotificationBroadcaster([adapter]);
376
+
377
+ const results = await broadcaster.broadcastDecision(
378
+ makeSignal(),
379
+ makeDecision({ selectedChannels: ["slack"], renderedCopy: {} }),
380
+ );
381
+
382
+ expect(results[0]?.status).toBe("failed");
383
+ expect(recordedPosts).toHaveLength(0);
384
+ });
385
+
386
+ test("a successful send whose row write fails still reads as sent, without a row reference", async () => {
387
+ composeFallbackReturn = {
388
+ slack: { title: "Check-in", body: "Two items need your eyes today." },
389
+ };
390
+ pairingByChannel = { slack: slackHome };
391
+ destinationBindingContexts = {
392
+ slack: { sourceChannel: "slack", externalChatId: "D0123456789" },
393
+ };
394
+ recordDeliveredChannelPostImpl = async () => {
395
+ throw new Error("messages table is locked");
396
+ };
397
+ const patches: Array<{
398
+ status: string;
399
+ patch?: { messageId?: string; canonicalMessageId?: string };
400
+ }> = [];
401
+ updateDeliveryStatusImpl = (status, patch) => {
402
+ patches.push({ status, patch });
403
+ };
404
+
405
+ const { adapter } = makeCapturingAdapter("slack", {
406
+ success: true,
407
+ messageId: "1756800000.000100",
408
+ });
409
+ const broadcaster = new NotificationBroadcaster([adapter]);
410
+
411
+ const results = await broadcaster.broadcastDecision(
412
+ makeSignal(),
413
+ makeDecision({ selectedChannels: ["slack"], renderedCopy: {} }),
414
+ );
415
+
416
+ expect(results[0]?.status).toBe("sent");
417
+ const sent = patches.find((p) => p.status === "sent");
418
+ expect(sent?.patch).toEqual({ messageId: "1756800000.000100" });
419
+ });
420
+
421
+ test("a vellum delivery is not recorded through this path", async () => {
422
+ composeFallbackReturn = {
423
+ vellum: { title: "Reminder", body: "Time to drink water" },
424
+ };
425
+
426
+ const { adapter } = makeCapturingAdapter("vellum");
427
+ const broadcaster = new NotificationBroadcaster([adapter]);
428
+
429
+ await broadcaster.broadcastDecision(
430
+ makeSignal(),
431
+ makeDecision({ renderedCopy: {} }),
432
+ );
433
+
434
+ expect(recordedPosts).toHaveLength(0);
435
+ });
436
+ });
437
+
274
438
  describe("NotificationBroadcaster platform deep-link from contextPayload", () => {
275
439
  test("uses deepLinkConversationId from contextPayload when no pairing exists", async () => {
276
440
  composeFallbackReturn = {
@@ -570,14 +570,15 @@ describe("decision tool title field specification", () => {
570
570
  tool.input_schema.properties.renderedCopy.properties.vellum.properties
571
571
  .title.description;
572
572
 
573
- expect(description).toContain("2 to 5 words");
573
+ expect(description).toContain("2 to 6 words");
574
574
  expect(description).toContain("40 characters");
575
- expect(description).toContain("noun phrase");
576
- expect(description).toContain(
577
- "Do NOT restate, summarize, or echo the body",
578
- );
575
+ // The title carries a bell row on its own, so it states the outcome
576
+ // rather than naming a topic the body then explains.
577
+ expect(description).toContain("WHAT HAPPENED or WHAT IS NEEDED");
578
+ expect(description).toContain("Lead with the outcome");
579
+ expect(description).toContain("Do NOT restate the body word for word");
579
580
  expect(description).toContain("no markdown");
580
- expect(description).toContain("Missing Context");
581
+ expect(description).toContain("Missing context");
581
582
  expect(description.match(/NOT '/g)?.length ?? 0).toBeGreaterThanOrEqual(2);
582
583
  });
583
584
  });
@@ -663,3 +664,75 @@ describe("title normalization", () => {
663
664
  expect(decision.renderedCopy.vellum?.title).toBe("Backup finished.");
664
665
  });
665
666
  });
667
+
668
+ function makeScheduleResultSignal(
669
+ overrides?: Partial<NotificationSignal>,
670
+ ): NotificationSignal {
671
+ return {
672
+ signalId: "sig-schedule-result-test-1",
673
+ createdAt: Date.now(),
674
+ sourceChannel: "scheduler",
675
+ sourceContextId: "conv-schedule-run",
676
+ sourceEventName: "schedule.result",
677
+ contextPayload: {
678
+ requestedMessage: "- **3 new emails**\n- `deploy.sh` failed overnight",
679
+ requestedTitle: "Morning briefing",
680
+ scheduleName: "Morning briefing",
681
+ scheduleId: "sched-1",
682
+ },
683
+ attentionHints: {
684
+ requiresAction: false,
685
+ urgency: "medium",
686
+ isAsyncBackground: true,
687
+ visibleInSourceNow: false,
688
+ },
689
+ ...overrides,
690
+ };
691
+ }
692
+
693
+ describe("schedule.result pass-through in notification decision engine", () => {
694
+ beforeEach(() => {
695
+ persistedDecisions = [];
696
+ });
697
+
698
+ test("carries the run's own output verbatim rather than rewriting it", async () => {
699
+ // The whole value of this notification is the digest itself. The
700
+ // classifier compresses a signal into a short alert, which would throw
701
+ // away the content the user set the schedule up to receive.
702
+ const decision = await evaluateSignal(makeScheduleResultSignal(), [
703
+ "vellum",
704
+ "platform",
705
+ ] as NotificationChannel[]);
706
+
707
+ expect(decision.renderedCopy.vellum?.body).toBe(
708
+ "- **3 new emails**\n- `deploy.sh` failed overnight",
709
+ );
710
+ expect(decision.renderedCopy.vellum?.title).toBe("Morning briefing");
711
+ expect(decision.reasoningSummary).toBe("schedule_result pass-through");
712
+ expect(decision.verbatimCopy).toBe(true);
713
+ expect(decision.fallbackUsed).toBe(false);
714
+ });
715
+
716
+ test("delivers to the notification center as well as push", async () => {
717
+ // Unlike an unseen chat reply, a scheduled run's output has no
718
+ // conversation the user is already in. `vellum` is where it persists.
719
+ const decision = await evaluateSignal(makeScheduleResultSignal(), [
720
+ "vellum",
721
+ "telegram",
722
+ "platform",
723
+ ] as NotificationChannel[]);
724
+
725
+ expect(decision.selectedChannels).toEqual(["vellum", "platform"]);
726
+ expect(decision.shouldNotify).toBe(true);
727
+ });
728
+
729
+ test("still reaches the inbox when push is unavailable", async () => {
730
+ const decision = await evaluateSignal(makeScheduleResultSignal(), [
731
+ "vellum",
732
+ "telegram",
733
+ ] as NotificationChannel[]);
734
+
735
+ expect(decision.selectedChannels).toEqual(["vellum"]);
736
+ expect(decision.shouldNotify).toBe(true);
737
+ });
738
+ });
@@ -74,6 +74,22 @@ mock.module("../../persistence/conversation-crud.js", () => ({
74
74
  getConversation: () => null,
75
75
  }));
76
76
 
77
+ const reindexedMessageIds: string[] = [];
78
+ mock.module("../../persistence/job-handlers/message-lexical.js", () => ({
79
+ enqueueLexicalIndexForMessage: (messageId: string) => {
80
+ reindexedMessageIds.push(messageId);
81
+ },
82
+ }));
83
+
84
+ const staleMarkedConversations: string[] = [];
85
+ mock.module("../../daemon/conversation-registry.js", () => ({
86
+ findConversation: (conversationId: string) => ({
87
+ markHistoryStale: () => {
88
+ staleMarkedConversations.push(conversationId);
89
+ },
90
+ }),
91
+ }));
92
+
77
93
  mock.module("../emit-signal.js", () => ({
78
94
  getBroadcaster: () => ({
79
95
  getAdapter: () =>
@@ -133,6 +149,7 @@ function makeDelivery(
133
149
  errorCode: null,
134
150
  errorMessage: null,
135
151
  sentAt: 1700000000000,
152
+ canonicalMessageId: null,
136
153
  conversationId: null,
137
154
  messageId: "1700000000.0001",
138
155
  conversationStrategy: null,
@@ -164,6 +181,8 @@ beforeEach(() => {
164
181
  renderedCopyPatches.length = 0;
165
182
  adapterUpdates.length = 0;
166
183
  messageRewrites.length = 0;
184
+ reindexedMessageIds.length = 0;
185
+ staleMarkedConversations.length = 0;
167
186
  messageOwners.clear();
168
187
  messageOwners.set("msg-9", "conv-source-1");
169
188
  messageLookupShouldThrow = false;
@@ -320,6 +339,46 @@ describe("editNotification", () => {
320
339
  ]);
321
340
  });
322
341
 
342
+ test("a body edit rewrites the delivery's canonical row, re-indexes it, and marks its conversation stale", async () => {
343
+ await appendFeedItem(makeItem());
344
+ decisionRow = { id: "dec-1" };
345
+ deliveryRows = [
346
+ makeDelivery({
347
+ conversationId: "conv-chat-home",
348
+ canonicalMessageId: "row-42",
349
+ }),
350
+ ];
351
+
352
+ await editNotification({ id: FEED_ITEM_ID, body: "Done in 4 minutes" });
353
+
354
+ expect(messageRewrites).toContainEqual({
355
+ messageId: "row-42",
356
+ content: "Done in 4 minutes",
357
+ });
358
+ expect(reindexedMessageIds).toEqual(["row-42"]);
359
+ expect(staleMarkedConversations).toEqual(["conv-chat-home"]);
360
+ });
361
+
362
+ test("a title-only edit leaves the canonical row alone", async () => {
363
+ await appendFeedItem(makeItem());
364
+ decisionRow = { id: "dec-1" };
365
+ deliveryRows = [
366
+ makeDelivery({
367
+ conversationId: "conv-chat-home",
368
+ canonicalMessageId: "row-42",
369
+ }),
370
+ ];
371
+
372
+ await editNotification({
373
+ id: FEED_ITEM_ID,
374
+ title: "Backup finished early",
375
+ });
376
+
377
+ expect(messageRewrites.some((r) => r.messageId === "row-42")).toBe(false);
378
+ expect(reindexedMessageIds).toEqual([]);
379
+ expect(staleMarkedConversations).toEqual([]);
380
+ });
381
+
323
382
  test("reports channels whose adapter cannot edit in place as unsupported", async () => {
324
383
  await appendFeedItem(makeItem());
325
384
  decisionRow = { id: "dec-1" };
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Tests for `resolve-visible-in-source.ts`: when a conversation-scoped
3
+ * producer is allowed to say the user is already watching this.
4
+ */
5
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
6
+
7
+ import type pino from "pino";
8
+
9
+ import { setOverridesForTesting } from "../../__tests__/feature-flag-test-helpers.js";
10
+
11
+ const FLAG = "activity-presence-suppression";
12
+ const CONVERSATION_ID = "conv-1";
13
+
14
+ let webFocused = false;
15
+ let webPresenceShouldThrow = false;
16
+ const webPresenceArgs: unknown[][] = [];
17
+ const realWebPresence = await import("../../runtime/web-presence.js");
18
+ mock.module("../../runtime/web-presence.js", () => ({
19
+ ...realWebPresence,
20
+ isWebConversationFocused: (...args: unknown[]) => {
21
+ webPresenceArgs.push(args);
22
+ if (webPresenceShouldThrow) {
23
+ throw new Error("simulated presence read failure");
24
+ }
25
+ return webFocused;
26
+ },
27
+ }));
28
+
29
+ const { resolveVisibleInSourceNow } =
30
+ await import("../resolve-visible-in-source.js");
31
+
32
+ describe("resolveVisibleInSourceNow", () => {
33
+ beforeEach(() => {
34
+ webFocused = false;
35
+ webPresenceShouldThrow = false;
36
+ webPresenceArgs.length = 0;
37
+ setOverridesForTesting({ [FLAG]: true });
38
+ });
39
+
40
+ test("returns true when the flag is on and the conversation is focused", () => {
41
+ webFocused = true;
42
+
43
+ expect(resolveVisibleInSourceNow({ conversationId: CONVERSATION_ID })).toBe(
44
+ true,
45
+ );
46
+ expect(webPresenceArgs).toEqual([[CONVERSATION_ID]]);
47
+ });
48
+
49
+ test("returns false when the conversation is not focused", () => {
50
+ webFocused = false;
51
+
52
+ expect(resolveVisibleInSourceNow({ conversationId: CONVERSATION_ID })).toBe(
53
+ false,
54
+ );
55
+ expect(webPresenceArgs).toEqual([[CONVERSATION_ID]]);
56
+ });
57
+
58
+ test("returns false without reading presence when the flag is off", () => {
59
+ webFocused = true;
60
+ setOverridesForTesting({ [FLAG]: false });
61
+
62
+ expect(resolveVisibleInSourceNow({ conversationId: CONVERSATION_ID })).toBe(
63
+ false,
64
+ );
65
+ expect(webPresenceArgs).toEqual([]);
66
+ });
67
+
68
+ test("returns false and warns once when the presence read throws", () => {
69
+ webPresenceShouldThrow = true;
70
+ const warn = mock(() => {});
71
+
72
+ expect(
73
+ resolveVisibleInSourceNow({
74
+ conversationId: CONVERSATION_ID,
75
+ logger: { warn } as unknown as pino.Logger,
76
+ }),
77
+ ).toBe(false);
78
+ expect(warn).toHaveBeenCalledTimes(1);
79
+ });
80
+
81
+ test("short-circuits an empty conversation id", () => {
82
+ webFocused = true;
83
+
84
+ expect(resolveVisibleInSourceNow({ conversationId: "" })).toBe(false);
85
+ expect(webPresenceArgs).toEqual([]);
86
+ });
87
+
88
+ test("short-circuits an undefined conversation id", () => {
89
+ webFocused = true;
90
+
91
+ expect(resolveVisibleInSourceNow({ conversationId: undefined })).toBe(
92
+ false,
93
+ );
94
+ expect(webPresenceArgs).toEqual([]);
95
+ });
96
+ });