@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
@@ -32,16 +32,12 @@
32
32
  import type { ConversationStrategy } from "../channels/config.js";
33
33
  import { getConversationStrategy } from "../channels/config.js";
34
34
  import { type ChannelId, isChannelId } from "../channels/types.js";
35
- import { isAssistantInitiatedThreadsEnabled } from "../config/assistant-initiated-threads-gate.js";
36
35
  import {
37
36
  addMessage,
38
37
  createConversation,
39
38
  getConversation,
40
39
  } from "../persistence/conversation-crud.js";
41
- import {
42
- ASSISTANT_INITIATED_SOURCE,
43
- type ConversationCreateType,
44
- } from "../persistence/conversation-types.js";
40
+ import type { ConversationCreateType } from "../persistence/conversation-types.js";
45
41
  import {
46
42
  findInboundConversationId,
47
43
  resolveInboundConversation,
@@ -56,6 +52,10 @@ import {
56
52
  } from "../runtime/sync/resource-sync-events.js";
57
53
  import { getLogger } from "../util/logger.js";
58
54
  import { withSqliteRetry } from "../util/sqlite-retry.js";
55
+ import {
56
+ type AssistantInitiatedThreadResolution,
57
+ resolveAssistantInitiatedThread,
58
+ } from "./assistant-initiated-thread.js";
59
59
  import {
60
60
  hasPersistedCompletionResult,
61
61
  notificationConversationId,
@@ -103,6 +103,8 @@ export interface PairingOptions {
103
103
  conversationAction?: ConversationAction;
104
104
  /** Destination binding data for channel-scoped conversation continuation. */
105
105
  bindingContext?: DestinationBindingContext;
106
+ /** Reuse the resolution made before delivery policy selects channels. */
107
+ assistantInitiatedThread?: AssistantInitiatedThreadResolution;
106
108
  }
107
109
 
108
110
  /**
@@ -126,72 +128,9 @@ export interface PairingOptions {
126
128
  * The returned `conversationId` is that producing conversation, with
127
129
  * `createdNewConversation` false.
128
130
  *
129
- * Errors are caught and logged — this function never throws so the
131
+ * Errors are caught and logged; this function never throws so the
130
132
  * notification pipeline is not disrupted by pairing failures.
131
133
  */
132
- /**
133
- * The event the assistant emits when it has a thought worth the user's time
134
- * ("assistant.share" - the notifications skill's default), as opposed to a
135
- * transactional request or a system alert.
136
- */
137
- const ASSISTANT_SHARE_EVENT = "assistant.share";
138
-
139
- /**
140
- * Promote a background share into an assistant-initiated thread, under the
141
- * `assistant-initiated-threads` flag.
142
- *
143
- * The heartbeat's "have a thought, share it" path emits an assistant.share
144
- * signal from its own background conversation, and the passive-vellum rule
145
- * below would append the body there - a row the sidebar never shows. When the
146
- * section exists, that share is exactly what it is for, so the signal is
147
- * rewritten to materialize a fresh standard conversation stamped
148
- * {@link ASSISTANT_INITIATED_SOURCE}, which is the section's membership mark.
149
- *
150
- * Deliberately narrow, in every direction it can be:
151
- * - vellum channel only: other channels deliver the share as a native
152
- * message and need no in-app thread;
153
- * - assistant.share only: transactional events keep their own pairing
154
- * rules, and their threads stay out of the section by source;
155
- * - only when the producing conversation is a background/scheduled run (or
156
- * nothing resolvable): a share emitted from inside a user-facing thread
157
- * keeps the append, since the user is already looking at that thread;
158
- * - never over an explicit `conversationMetadata.source` or an existing
159
- * `requiresConversation`: a producer that declared its own filing wins.
160
- *
161
- * Flag off, the signal passes through untouched and shares keep the passive
162
- * append: nothing changes for anyone outside the rollout.
163
- */
164
- function withAssistantInitiatedThread(
165
- signal: NotificationSignal,
166
- channel: NotificationChannel,
167
- ): NotificationSignal {
168
- if (
169
- channel !== "vellum" ||
170
- signal.sourceEventName !== ASSISTANT_SHARE_EVENT ||
171
- signal.requiresConversation === true ||
172
- signal.conversationMetadata?.source !== undefined ||
173
- !isAssistantInitiatedThreadsEnabled()
174
- ) {
175
- return signal;
176
- }
177
- const producing = getConversation(signal.sourceContextId);
178
- if (
179
- producing &&
180
- producing.conversationType !== "background" &&
181
- producing.conversationType !== "scheduled"
182
- ) {
183
- return signal;
184
- }
185
- return {
186
- ...signal,
187
- requiresConversation: true,
188
- conversationMetadata: {
189
- ...signal.conversationMetadata,
190
- source: ASSISTANT_INITIATED_SOURCE,
191
- },
192
- };
193
- }
194
-
195
134
  export async function pairDeliveryWithConversation(
196
135
  rawSignal: NotificationSignal,
197
136
  channel: NotificationChannel,
@@ -199,7 +138,13 @@ export async function pairDeliveryWithConversation(
199
138
  options?: PairingOptions,
200
139
  ): Promise<PairingResult> {
201
140
  try {
202
- const signal = withAssistantInitiatedThread(rawSignal, channel);
141
+ const signal =
142
+ channel === "vellum"
143
+ ? (
144
+ options?.assistantInitiatedThread ??
145
+ resolveAssistantInitiatedThread(rawSignal)
146
+ ).vellumSignal
147
+ : rawSignal;
203
148
  const strategy = getConversationStrategy(channel as ChannelId);
204
149
 
205
150
  if (strategy === "not_deliverable" || strategy === "push_only") {
@@ -27,6 +27,7 @@ import { VellumAdapter } from "./adapters/macos.js";
27
27
  import { PlatformPushAdapter } from "./adapters/platform.js";
28
28
  import { SlackAdapter } from "./adapters/slack.js";
29
29
  import { TelegramAdapter } from "./adapters/telegram.js";
30
+ import { resolveAssistantInitiatedThread } from "./assistant-initiated-thread.js";
30
31
  import {
31
32
  type ConversationCreatedInfo,
32
33
  NotificationBroadcaster,
@@ -428,6 +429,8 @@ export async function emitNotificationSignal<TEventName extends string>(
428
429
  });
429
430
  }
430
431
 
432
+ const assistantInitiatedThread = resolveAssistantInitiatedThread(signal);
433
+
431
434
  // Step 2: Evaluate the signal through the decision engine
432
435
  const connectedChannels = await getConnectedChannels();
433
436
 
@@ -444,6 +447,9 @@ export async function emitNotificationSignal<TEventName extends string>(
444
447
  // steps (2.5a/2.5b/2.5c) so any of them replacing the decision triggers
445
448
  // the re-persist and the stored row matches what is dispatched.
446
449
  const prePolicyDecision = decision;
450
+ let platformConfigured: Promise<boolean> | undefined;
451
+ const platformIsConfigured = () =>
452
+ (platformConfigured ??= isPlatformClientConfigured());
447
453
 
448
454
  // Step 2.5a: High/critical urgency signals always get both the in-app
449
455
  // system notification (vellum) and the remote push (platform),
@@ -497,7 +503,7 @@ export async function emitNotificationSignal<TEventName extends string>(
497
503
  }
498
504
  if (
499
505
  !selectedChannels.includes("platform") &&
500
- (await isPlatformClientConfigured())
506
+ (await platformIsConfigured())
501
507
  ) {
502
508
  selectedChannels.push("platform");
503
509
  forcedChannels.push("platform");
@@ -559,9 +565,30 @@ export async function emitNotificationSignal<TEventName extends string>(
559
565
  };
560
566
  }
561
567
 
568
+ let assistantInitiatedPlatformAdded = false;
569
+ if (
570
+ assistantInitiatedThread.isCandidate &&
571
+ decision.shouldNotify &&
572
+ decision.selectedChannels.includes("vellum") &&
573
+ !decision.selectedChannels.includes("platform") &&
574
+ !(
575
+ "quiet" in signal.contextPayload && signal.contextPayload.quiet === true
576
+ ) &&
577
+ signal.routingIntent !== "single_channel" &&
578
+ !exclusiveAllowlist &&
579
+ (await platformIsConfigured())
580
+ ) {
581
+ assistantInitiatedPlatformAdded = true;
582
+ decision = {
583
+ ...decision,
584
+ selectedChannels: [...decision.selectedChannels, "platform"],
585
+ reasoningSummary: `${decision.reasoningSummary} (platform added: assistant-initiated thread candidate)`,
586
+ };
587
+ }
588
+
562
589
  // Re-persist the decision if any policy step changed it (urgency channel
563
- // forcing, routing intent enforcement, or the access-request vellum
564
- // floor), so the stored decision row matches what is actually dispatched.
590
+ // forcing, routing intent enforcement, the access-request vellum floor,
591
+ // or the From me push default), so the audit matches the dispatch.
565
592
  if (decision !== prePolicyDecision && decision.persistedDecisionId) {
566
593
  try {
567
594
  updateDecision(decision.persistedDecisionId, {
@@ -634,6 +661,10 @@ export async function emitNotificationSignal<TEventName extends string>(
634
661
  {
635
662
  onConversationCreated: params.onConversationCreated,
636
663
  resultsSink: channelResults,
664
+ assistantInitiatedThread: {
665
+ resolution: assistantInitiatedThread,
666
+ platformAdded: assistantInitiatedPlatformAdded,
667
+ },
637
668
  },
638
669
  );
639
670
 
@@ -65,6 +65,8 @@ export type NotificationReceiptClass = z.infer<
65
65
 
66
66
  export const DeliveryResultSchema = z.object({
67
67
  success: z.boolean(),
68
+ /** Delivery was intentionally suppressed before the provider accepted it. */
69
+ skipped: z.boolean().optional(),
68
70
  error: z.string().optional(),
69
71
  messageId: z.string().optional(),
70
72
  /** Set only by the platform push adapter: true when the platform accepted
@@ -147,7 +147,7 @@ export async function orchestrateOAuthConnect(
147
147
  }
148
148
 
149
149
  // Deserialize JSON fields from the DB row
150
- const dbAuthorizeParams = safeJsonParse<Record<string, string> | undefined>(
150
+ const authorizeParams = safeJsonParse<Record<string, string> | undefined>(
151
151
  providerRow.authorizeParams,
152
152
  undefined,
153
153
  );
@@ -155,7 +155,6 @@ export async function orchestrateOAuthConnect(
155
155
  // Resolve all protocol-level config from the DB
156
156
  const authorizeUrl = providerRow.authorizeUrl;
157
157
  const tokenExchangeUrl = providerRow.tokenExchangeUrl;
158
- const authorizeParams = dbAuthorizeParams;
159
158
  const userinfoUrl = providerRow.userinfoUrl ?? undefined;
160
159
  const tokenEndpointAuthMethod = providerRow.tokenEndpointAuthMethod as
161
160
  | TokenEndpointAuthMethod
@@ -0,0 +1,330 @@
1
+ import {
2
+ afterAll,
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ mock,
7
+ setSystemTime,
8
+ test,
9
+ } from "bun:test";
10
+
11
+ import { eq } from "drizzle-orm";
12
+
13
+ mock.module("../../config/sidebar-done-gate.js", () => ({
14
+ isSidebarDoneEnabled: () => true,
15
+ SIDEBAR_DONE_FLAG_KEY: "sidebar-done",
16
+ }));
17
+
18
+ import {
19
+ archiveInactiveConversation,
20
+ listAutoArchiveCandidates,
21
+ } from "../conversation-auto-archive.js";
22
+ import {
23
+ archiveConversation,
24
+ resurfaceArchivedConversation,
25
+ unarchiveConversation,
26
+ } from "../conversation-crud.js";
27
+ import { ensureGroupMigration } from "../conversation-group-migration.js";
28
+ import { getDb } from "../db-connection.js";
29
+ import { initializeDb } from "../db-init.js";
30
+ import { rawGet, rawRun } from "../raw-query.js";
31
+ import {
32
+ acpSessionHistory,
33
+ conversationAssistantAttentionState,
34
+ conversationModeSessions,
35
+ conversations,
36
+ externalConversationBindings,
37
+ workflowRuns,
38
+ } from "../schema/index.js";
39
+ import { upsertSubagentRecord } from "../subagent-store.js";
40
+
41
+ await initializeDb();
42
+ ensureGroupMigration();
43
+ const NOW = 1_800_000_000_000;
44
+ const DAY = 86_400_000;
45
+ const CUTOFF = NOW - 7 * DAY;
46
+ const ID = "conv-123";
47
+
48
+ function insert(
49
+ id = ID,
50
+ values: Partial<typeof conversations.$inferInsert> = {},
51
+ ) {
52
+ getDb()
53
+ .insert(conversations)
54
+ .values({
55
+ id,
56
+ createdAt: CUTOFF - DAY,
57
+ updatedAt: NOW,
58
+ lastMessageAt: CUTOFF,
59
+ ...values,
60
+ })
61
+ .run();
62
+ }
63
+ function change(values: Partial<typeof conversations.$inferInsert>) {
64
+ getDb()
65
+ .update(conversations)
66
+ .set(values)
67
+ .where(eq(conversations.id, ID))
68
+ .run();
69
+ }
70
+ function candidates(limit = 100) {
71
+ return listAutoArchiveCandidates({ cutoff: CUTOFF, limit });
72
+ }
73
+ function state() {
74
+ return getDb()
75
+ .select({
76
+ archivedAt: conversations.archivedAt,
77
+ lastReopenedAt: conversations.lastReopenedAt,
78
+ })
79
+ .from(conversations)
80
+ .where(eq(conversations.id, ID))
81
+ .get();
82
+ }
83
+
84
+ beforeEach(() => {
85
+ setSystemTime(NOW);
86
+ const db = getDb();
87
+ db.delete(conversationAssistantAttentionState).run();
88
+ db.delete(conversationModeSessions).run();
89
+ db.delete(externalConversationBindings).run();
90
+ db.delete(acpSessionHistory).run();
91
+ db.delete(workflowRuns).run();
92
+ rawRun("test:clearSubagents", "DELETE FROM subagents");
93
+ db.delete(conversations).run();
94
+ insert();
95
+ });
96
+ afterAll(() => setSystemTime());
97
+
98
+ test("includes the exact cutoff, excludes newer activity, and ignores title/update timestamps", () => {
99
+ expect(candidates().map((row) => row.id)).toEqual([ID]);
100
+ change({ lastMessageAt: CUTOFF + 1 });
101
+ expect(candidates()).toEqual([]);
102
+ change({ lastMessageAt: null, createdAt: CUTOFF });
103
+ expect(candidates()).toHaveLength(1);
104
+ });
105
+
106
+ const exclusions: Array<[string, () => void]> = [
107
+ ["already Done", () => change({ archivedAt: NOW })],
108
+ ["processing", () => change({ processingStartedAt: NOW })],
109
+ ["background", () => change({ conversationType: "background" })],
110
+ ["scheduled", () => change({ conversationType: "scheduled" })],
111
+ ["private", () => change({ conversationType: "private" })],
112
+ ["system", () => change({ source: "system" })],
113
+ ["subagent", () => change({ source: "subagent" })],
114
+ ["spawned child", () => change({ parentConversationId: "conv-parent" })],
115
+ ["memory", () => change({ source: "memory_v2_consolidation" })],
116
+ ["schedule owner", () => change({ scheduleJobId: "schedule-123" })],
117
+ [
118
+ "pin",
119
+ () => {
120
+ rawRun(
121
+ "test:pin",
122
+ "UPDATE conversations SET group_id = 'system:pinned' WHERE id = ?",
123
+ ID,
124
+ );
125
+ },
126
+ ],
127
+ [
128
+ "background group",
129
+ () => {
130
+ rawRun(
131
+ "test:background",
132
+ "UPDATE conversations SET group_id = 'system:background' WHERE id = ?",
133
+ ID,
134
+ );
135
+ },
136
+ ],
137
+ ["external origin", () => change({ originChannel: "slack" })],
138
+ [
139
+ "external binding",
140
+ () => {
141
+ getDb()
142
+ .insert(externalConversationBindings)
143
+ .values({
144
+ conversationId: ID,
145
+ sourceChannel: "telegram",
146
+ externalChatId: "chat-123",
147
+ createdAt: NOW,
148
+ updatedAt: NOW,
149
+ })
150
+ .run();
151
+ },
152
+ ],
153
+ [
154
+ "unread",
155
+ () => {
156
+ getDb()
157
+ .insert(conversationAssistantAttentionState)
158
+ .values({
159
+ conversationId: ID,
160
+ latestAssistantMessageAt: CUTOFF,
161
+ lastSeenAssistantMessageAt: null,
162
+ createdAt: NOW,
163
+ updatedAt: NOW,
164
+ })
165
+ .run();
166
+ },
167
+ ],
168
+ [
169
+ "ACP run",
170
+ () => {
171
+ getDb()
172
+ .insert(acpSessionHistory)
173
+ .values({
174
+ id: "acp-123",
175
+ agentId: "agent-123",
176
+ acpSessionId: "session-123",
177
+ parentConversationId: ID,
178
+ status: "running",
179
+ startedAt: CUTOFF,
180
+ })
181
+ .run();
182
+ },
183
+ ],
184
+ [
185
+ "workflow run",
186
+ () => {
187
+ getDb()
188
+ .insert(workflowRuns)
189
+ .values({
190
+ id: "run-123",
191
+ scriptSource: "",
192
+ scriptHash: "hash-123",
193
+ status: "running",
194
+ conversationId: ID,
195
+ })
196
+ .run();
197
+ },
198
+ ],
199
+ [
200
+ "mode session",
201
+ () => {
202
+ getDb()
203
+ .insert(conversationModeSessions)
204
+ .values({
205
+ id: "mode-123",
206
+ conversationId: ID,
207
+ mode: "browser",
208
+ status: "active",
209
+ sourceStartedAt: CUTOFF,
210
+ lastActivityAt: CUTOFF,
211
+ })
212
+ .run();
213
+ },
214
+ ],
215
+ [
216
+ "subagent run",
217
+ () => {
218
+ upsertSubagentRecord({
219
+ id: "task-123",
220
+ parentConversationId: ID,
221
+ conversationId: "conv-child",
222
+ label: "Example task",
223
+ objective: "Example objective",
224
+ role: "builder",
225
+ isFork: false,
226
+ sendResultToUser: true,
227
+ parentToolUseId: null,
228
+ status: "running",
229
+ error: null,
230
+ createdAt: CUTOFF,
231
+ startedAt: CUTOFF,
232
+ completedAt: null,
233
+ inputTokens: 0,
234
+ outputTokens: 0,
235
+ estimatedCost: 0,
236
+ });
237
+ },
238
+ ],
239
+ ];
240
+
241
+ describe.each(exclusions)("%s exclusion", (_name, exclude) => {
242
+ test("is filtered from candidates and rechecked after sampling", () => {
243
+ const sampled = candidates()[0]!;
244
+ exclude();
245
+ expect(candidates()).toEqual([]);
246
+ expect(archiveInactiveConversation(sampled, CUTOFF, NOW)).toBe(false);
247
+ });
248
+ });
249
+
250
+ test("native notification origins and read chats stay eligible", () => {
251
+ change({ originChannel: "notification:reminder" });
252
+ getDb()
253
+ .insert(conversationAssistantAttentionState)
254
+ .values({
255
+ conversationId: ID,
256
+ latestAssistantMessageAt: CUTOFF,
257
+ lastSeenAssistantMessageAt: CUTOFF,
258
+ createdAt: NOW,
259
+ updatedAt: NOW,
260
+ })
261
+ .run();
262
+ expect(candidates()).toHaveLength(1);
263
+ });
264
+
265
+ test("a sampled old message change cannot be overwritten even if it remains below the cutoff", () => {
266
+ const sampled = candidates()[0]!;
267
+ change({ lastMessageAt: CUTOFF - 1 });
268
+ expect(archiveInactiveConversation(sampled, CUTOFF, NOW)).toBe(false);
269
+ });
270
+
271
+ test("manual reopen receives a full interval and repeated unarchive does not extend it", () => {
272
+ archiveConversation(ID);
273
+ unarchiveConversation(ID);
274
+ expect(state()).toEqual({ archivedAt: null, lastReopenedAt: NOW });
275
+ expect(candidates()).toEqual([]);
276
+ setSystemTime(NOW + DAY);
277
+ unarchiveConversation(ID);
278
+ expect(state()?.lastReopenedAt).toBe(NOW);
279
+ expect(listAutoArchiveCandidates({ cutoff: NOW - 1, limit: 100 })).toEqual(
280
+ [],
281
+ );
282
+ expect(listAutoArchiveCandidates({ cutoff: NOW, limit: 100 })).toHaveLength(
283
+ 1,
284
+ );
285
+ });
286
+
287
+ test("reopen after sampling prevents the conditional archive", () => {
288
+ const sampled = candidates()[0]!;
289
+ archiveConversation(ID);
290
+ unarchiveConversation(ID);
291
+ expect(archiveInactiveConversation(sampled, CUTOFF, NOW)).toBe(false);
292
+ });
293
+
294
+ test("archive is idempotent and new visible activity resurfaces it", () => {
295
+ const sampled = candidates()[0]!;
296
+ expect(archiveInactiveConversation(sampled, CUTOFF, NOW)).toBe(true);
297
+ expect(archiveInactiveConversation(sampled, CUTOFF, NOW + 1)).toBe(false);
298
+ expect(resurfaceArchivedConversation(ID, NOW + 1)).toBe(true);
299
+ expect(state()).toEqual({ archivedAt: null, lastReopenedAt: null });
300
+ });
301
+
302
+ test("keyset paging covers all rows while earlier pages are marked Done", () => {
303
+ for (let i = 0; i < 205; i++) {
304
+ insert(`conv-page-${String(i).padStart(3, "0")}`);
305
+ }
306
+ const visited: string[] = [];
307
+ let after: ReturnType<typeof candidates>[number] | undefined;
308
+ do {
309
+ const page = listAutoArchiveCandidates({
310
+ cutoff: CUTOFF,
311
+ limit: 100,
312
+ after,
313
+ });
314
+ if (page.length === 0) {
315
+ break;
316
+ }
317
+ for (const candidate of page) {
318
+ expect(archiveInactiveConversation(candidate, CUTOFF, NOW)).toBe(true);
319
+ visited.push(candidate.id);
320
+ }
321
+ after = page[page.length - 1];
322
+ } while (true);
323
+ expect(new Set(visited).size).toBe(206);
324
+ expect(
325
+ rawGet<{ count: number }>(
326
+ "test:doneCount",
327
+ "SELECT count(*) AS count FROM conversations WHERE archived_at IS NOT NULL",
328
+ )?.count,
329
+ ).toBe(206);
330
+ });