@vellumai/assistant 0.11.8 → 0.11.9-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (326) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/Dockerfile +8 -48
  3. package/docker-entrypoint.sh +5 -1
  4. package/docker-kata-apt-env.sh +3 -0
  5. package/docker-kata-apt-shims.sh +127 -0
  6. package/docker-kata-apt-wrapper.sh +45 -0
  7. package/docker-kata-chroot-exec.sh +35 -0
  8. package/docker-kata-pip.sh +8 -2
  9. package/docs/architecture/memory.md +8 -4
  10. package/docs/guardian-request-flow.md +18 -14
  11. package/docs/trusted-contact-access.md +1 -7
  12. package/node_modules/@vellumai/avatar-catalog/src/catalog.ts +7 -1
  13. package/node_modules/@vellumai/avatar-catalog/src/index.ts +1 -1
  14. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +4 -1
  15. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  17. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  18. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  19. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +4 -1
  20. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  21. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  22. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  23. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  24. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +0 -18
  25. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +110 -5
  26. package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +5 -33
  27. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +3 -0
  28. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +100 -9
  29. package/node_modules/@vellumai/gateway-client/src/index.ts +1 -2
  30. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +9 -0
  31. package/node_modules/@vellumai/service-contracts/package.json +4 -1
  32. package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  33. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  34. package/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  35. package/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  36. package/openapi.yaml +193 -4
  37. package/package.json +2 -2
  38. package/src/__tests__/access-request-card-view.test.ts +6 -5
  39. package/src/__tests__/access-request-seed-content-blocks.test.ts +5 -2
  40. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +43 -82
  41. package/src/__tests__/always-loaded-tools-guard.test.ts +8 -2
  42. package/src/__tests__/attachment-stored-path-annotation.test.ts +80 -0
  43. package/src/__tests__/attachments-store.test.ts +27 -0
  44. package/src/__tests__/attachments.test.ts +43 -0
  45. package/src/__tests__/canned-reply-release.test.ts +48 -5
  46. package/src/__tests__/channel-delivery-store.test.ts +129 -18
  47. package/src/__tests__/channel-reply-delivery.test.ts +553 -93
  48. package/src/__tests__/channel-retry-sweep.test.ts +199 -0
  49. package/src/__tests__/client-os-metadata-persistence.test.ts +32 -3
  50. package/src/__tests__/conversation-error.test.ts +15 -0
  51. package/src/__tests__/conversation-load-history-repair.test.ts +118 -0
  52. package/src/__tests__/conversation-pairing.test.ts +141 -1
  53. package/src/__tests__/conversation-routes-disk-view.test.ts +28 -1
  54. package/src/__tests__/conversation-routes-slash-commands.test.ts +125 -0
  55. package/src/__tests__/conversation-runtime-assembly.test.ts +79 -0
  56. package/src/__tests__/conversation-store-ephemeral.test.ts +323 -10
  57. package/src/__tests__/conversation-sync-tags.test.ts +2 -37
  58. package/src/__tests__/credential-health-service.test.ts +98 -10
  59. package/src/__tests__/delete-propagation.test.ts +469 -0
  60. package/src/__tests__/dm-persistence.test.ts +16 -0
  61. package/src/__tests__/docker-kata-apt-shims.test.ts +135 -0
  62. package/src/__tests__/forbidden-legacy-symbols.test.ts +12 -0
  63. package/src/__tests__/gemini-provider.test.ts +46 -0
  64. package/src/__tests__/guardian-card-withdrawal.test.ts +0 -22
  65. package/src/__tests__/guardian-gateway-sim.ts +0 -23
  66. package/src/__tests__/guardian-question-mode.test.ts +108 -0
  67. package/src/__tests__/guardian-reply-router-answer-mode.test.ts +0 -2
  68. package/src/__tests__/guardian-routing-invariants.test.ts +26 -12
  69. package/src/__tests__/host-proxy-interface.test.ts +10 -0
  70. package/src/__tests__/inbound-slack-persistence.test.ts +16 -0
  71. package/src/__tests__/injector-v3-suppression.test.ts +43 -9
  72. package/src/__tests__/list-messages-page-latest.test.ts +127 -0
  73. package/src/__tests__/list-messages-system-card.test.ts +103 -0
  74. package/src/__tests__/media-resolve-image-validation.test.ts +77 -0
  75. package/src/__tests__/notification-decision-fallback.test.ts +199 -77
  76. package/src/__tests__/notification-decision-strategy.test.ts +198 -213
  77. package/src/__tests__/notification-discord-adapter.test.ts +25 -0
  78. package/src/__tests__/notification-slack-adapter.test.ts +133 -0
  79. package/src/__tests__/notification-telegram-adapter.test.ts +36 -0
  80. package/src/__tests__/openai-provider.test.ts +5 -5
  81. package/src/__tests__/outbound-slack-persistence.test.ts +85 -72
  82. package/src/__tests__/persist-user-message-set-processing-failure.test.ts +83 -65
  83. package/src/__tests__/platform-client-verify-credential.test.ts +100 -0
  84. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -1
  85. package/src/__tests__/pricing.test.ts +11 -0
  86. package/src/__tests__/process-message-display-content.test.ts +20 -9
  87. package/src/__tests__/processing-acquire-fenced-guard.test.ts +56 -0
  88. package/src/__tests__/provider-meta-persistence.test.ts +67 -0
  89. package/src/__tests__/reaction-persistence.test.ts +22 -198
  90. package/src/__tests__/run-conversation-turn-persistence.test.ts +5 -2
  91. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +16 -0
  92. package/src/__tests__/skill-load-tool.test.ts +27 -0
  93. package/src/__tests__/skills.test.ts +34 -1
  94. package/src/__tests__/strip-memory-injections.test.ts +3 -4
  95. package/src/__tests__/terminal-tools.test.ts +9 -0
  96. package/src/__tests__/thread-backfill.test.ts +5 -3
  97. package/src/__tests__/unified-turn-context-location.test.ts +76 -0
  98. package/src/__tests__/voice-session-bridge.test.ts +18 -0
  99. package/src/__tests__/watch-retro-report-payload.test.ts +185 -0
  100. package/src/__tests__/watch-retro-tool-availability.test.ts +82 -0
  101. package/src/__tests__/workspace-migration-151-repair-renamed-fireworks-deepseek-pro-model-id.test.ts +235 -0
  102. package/src/__tests__/workspace-migration-152-repair-retired-fireworks-minimax-m2p7-model-id.test.ts +233 -0
  103. package/src/agent/attachments.ts +13 -2
  104. package/src/agent/loop.ts +3 -19
  105. package/src/api/README.md +9 -5
  106. package/src/api/index.ts +9 -8
  107. package/src/api/package.json +1 -0
  108. package/src/api/responses/conversation-message.ts +8 -0
  109. package/src/api/responses/home.ts +7 -18
  110. package/src/api/surfaces.ts +114 -7
  111. package/src/approvals/AGENTS.md +1 -1
  112. package/src/channels/__tests__/gateway-guardian-requests.test.ts +1 -23
  113. package/src/channels/__tests__/types.test.ts +22 -1
  114. package/src/channels/gateway-guardian-requests.ts +0 -22
  115. package/src/channels/types.ts +8 -6
  116. package/src/cli/__tests__/catalog-search-help.test.ts +18 -0
  117. package/src/cli/commands/channels/__tests__/channels.test.ts +26 -0
  118. package/src/cli/commands/channels/index.help.ts +19 -6
  119. package/src/cli/commands/channels/index.ts +6 -2
  120. package/src/cli/commands/db/__tests__/status.test.ts +22 -0
  121. package/src/cli/commands/db/index.help.ts +1 -1
  122. package/src/cli/commands/db/status.ts +172 -1
  123. package/src/cli/commands/platform/__tests__/connect.test.ts +29 -0
  124. package/src/cli/commands/platform/connect.ts +14 -5
  125. package/src/cli/commands/plugins.help.ts +6 -5
  126. package/src/cli/lib/__tests__/install-from-github.test.ts +0 -8
  127. package/src/cli/lib/__tests__/install-from-platform.test.ts +72 -0
  128. package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +33 -0
  129. package/src/cli/lib/bundled-marketplace.json +14 -0
  130. package/src/cli/lib/install-from-github.ts +9 -5
  131. package/src/cli/lib/install-from-platform.ts +12 -1
  132. package/src/config/__tests__/assistant-initiated-threads-gate.test.ts +61 -0
  133. package/src/config/assistant-initiated-threads-gate.ts +50 -0
  134. package/src/config/bundled-skills/acp/SKILL.md +10 -3
  135. package/src/config/bundled-skills/schedule/SKILL.md +25 -11
  136. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +3 -1
  137. package/src/config/call-site-defaults.ts +4 -1
  138. package/src/config/feature-flag-registry.json +21 -4
  139. package/src/config/schemas/memory-v3.ts +4 -3
  140. package/src/context/strip-injections.ts +17 -56
  141. package/src/conversations/__tests__/message-consolidation.test.ts +39 -0
  142. package/src/conversations/message-consolidation.ts +4 -3
  143. package/src/credential-health/credential-health-service.ts +130 -0
  144. package/src/daemon/__tests__/conversation-tool-setup.test.ts +31 -0
  145. package/src/daemon/conversation-agent-loop-handlers.ts +55 -59
  146. package/src/daemon/conversation-error.ts +2 -0
  147. package/src/daemon/conversation-messaging.ts +195 -47
  148. package/src/daemon/conversation-process.ts +20 -5
  149. package/src/daemon/conversation-runtime-assembly.ts +48 -39
  150. package/src/daemon/conversation-store.ts +159 -11
  151. package/src/daemon/conversation-tool-setup.ts +9 -2
  152. package/src/daemon/conversation.ts +290 -19
  153. package/src/daemon/dictation-text-processing.ts +2 -7
  154. package/src/daemon/handlers/config-channels.ts +33 -3
  155. package/src/daemon/handlers/config-model.test.ts +1 -0
  156. package/src/daemon/handlers/shared.ts +9 -0
  157. package/src/daemon/port-oversized-content.test.ts +117 -0
  158. package/src/daemon/port-oversized-content.ts +110 -0
  159. package/src/daemon/process-message.ts +87 -16
  160. package/src/daemon/reaction-record.test.ts +23 -11
  161. package/src/daemon/reaction-record.ts +6 -9
  162. package/src/documents/document-store.ts +1 -5
  163. package/src/home/conversation-starter-validation.ts +1 -5
  164. package/src/live-voice/__tests__/live-voice-photo.test.ts +910 -50
  165. package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +31 -25
  166. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +70 -1
  167. package/src/live-voice/live-voice-photo.ts +544 -61
  168. package/src/live-voice/live-voice-session.ts +6 -2
  169. package/src/live-voice/protocol.ts +20 -1
  170. package/src/messaging/provider-message-metadata.ts +40 -1
  171. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +10 -2
  172. package/src/messaging/providers/channel-transport.ts +28 -0
  173. package/src/messaging/providers/discord/send.ts +3 -2
  174. package/src/messaging/providers/slack/api.ts +11 -1
  175. package/src/messaging/providers/slack/message-metadata.test.ts +133 -0
  176. package/src/messaging/providers/slack/message-metadata.ts +141 -1
  177. package/src/messaging/providers/slack/send.test.ts +58 -0
  178. package/src/messaging/providers/slack/send.ts +4 -4
  179. package/src/messaging/providers/slack/transport.ts +28 -2
  180. package/src/messaging/providers/telegram-bot/send.test.ts +159 -0
  181. package/src/messaging/providers/telegram-bot/send.ts +93 -0
  182. package/src/messaging/providers/telegram-bot/transport.ts +71 -0
  183. package/src/messaging/reaction-envelopes.test.ts +73 -0
  184. package/src/messaging/reaction-envelopes.ts +33 -5
  185. package/src/messaging/read-provider-metadata.ts +4 -3
  186. package/src/monitoring/recovery/__tests__/stranded-delivery-events.test.ts +208 -0
  187. package/src/monitoring/recovery/db.ts +35 -0
  188. package/src/monitoring/recovery/orphaned-channel-events.ts +3 -22
  189. package/src/monitoring/recovery/run-recovery.ts +6 -2
  190. package/src/monitoring/recovery/stale-processing.ts +3 -20
  191. package/src/monitoring/recovery/stranded-delivery-events.ts +68 -0
  192. package/src/notifications/AGENTS.md +2 -2
  193. package/src/notifications/README.md +2 -2
  194. package/src/notifications/__tests__/assistant-reply-producer.test.ts +11 -9
  195. package/src/notifications/__tests__/broadcaster.test.ts +35 -4
  196. package/src/notifications/__tests__/notification-utils.test.ts +31 -0
  197. package/src/notifications/access-request-copy.ts +126 -114
  198. package/src/notifications/adapters/discord.ts +9 -5
  199. package/src/notifications/adapters/shared.ts +17 -4
  200. package/src/notifications/adapters/slack.ts +14 -4
  201. package/src/notifications/adapters/telegram.ts +8 -4
  202. package/src/notifications/approval-card-data.ts +5 -2
  203. package/src/notifications/assistant-reply-producer.ts +7 -7
  204. package/src/notifications/broadcaster.ts +57 -25
  205. package/src/notifications/conversation-pairing.ts +68 -2
  206. package/src/notifications/copy-composer.ts +13 -32
  207. package/src/notifications/decision-engine.ts +56 -237
  208. package/src/notifications/guardian-delivery-recorder.ts +5 -9
  209. package/src/notifications/guardian-feed-projection.ts +0 -15
  210. package/src/notifications/guardian-question-mode.ts +147 -6
  211. package/src/notifications/notification-utils.ts +117 -1
  212. package/src/permissions/confirmation-guardian-request.ts +2 -2
  213. package/src/permissions/prompter.ts +1 -1
  214. package/src/persistence/conversation-crud.ts +93 -14
  215. package/src/persistence/conversation-queries.ts +144 -17
  216. package/src/persistence/conversation-types.ts +39 -5
  217. package/src/persistence/delivery-crud.ts +224 -28
  218. package/src/persistence/delivery-status.ts +21 -2
  219. package/src/persistence/migrations/374-channel-inbound-message-id-index.ts +26 -0
  220. package/src/persistence/migrations/375-create-channel-outbound-posts.ts +52 -0
  221. package/src/persistence/migrations/__tests__/375-create-channel-outbound-posts.test.ts +84 -0
  222. package/src/persistence/schema/conversations.ts +71 -2
  223. package/src/persistence/schema-contract.test.ts +78 -0
  224. package/src/persistence/schema-contract.ts +96 -0
  225. package/src/persistence/steps.ts +4 -0
  226. package/src/platform/client.ts +55 -0
  227. package/src/plugins/__tests__/mcp-servers.test.ts +46 -0
  228. package/src/plugins/defaults/injector-order.ts +2 -1
  229. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +2 -2
  230. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +13 -15
  231. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +10 -0
  232. package/src/plugins/defaults/memory/injectors.ts +1 -1
  233. package/src/plugins/defaults/memory/memory-marker.ts +17 -7
  234. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +1 -5
  235. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +17 -0
  236. package/src/plugins/defaults/memory/substrate/skill-content.ts +7 -0
  237. package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +54 -41
  238. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +3 -3
  239. package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +22 -0
  240. package/src/plugins/defaults/memory/v3/injector.ts +14 -15
  241. package/src/plugins/defaults/memory/v3/prune.test.ts +20 -2
  242. package/src/plugins/defaults/memory/v3/render-injection.ts +10 -1
  243. package/src/plugins/defaults/memory/v3/types.ts +15 -4
  244. package/src/plugins/defaults/turn-context/injectors.ts +3 -0
  245. package/src/plugins/defaults/turn-context/unified-turn-context.ts +24 -0
  246. package/src/plugins/mcp-servers.ts +14 -2
  247. package/src/providers/__tests__/dispatch-connection-routing.test.ts +50 -0
  248. package/src/providers/__tests__/registry-native-web-search.test.ts +49 -2
  249. package/src/providers/anthropic/client.ts +21 -24
  250. package/src/providers/call-site-routing.ts +5 -4
  251. package/src/providers/connection-resolution.ts +29 -1
  252. package/src/providers/content-block-size.test.ts +82 -0
  253. package/src/providers/content-block-size.ts +99 -0
  254. package/src/providers/file-block-text.test.ts +64 -0
  255. package/src/providers/file-block-text.ts +28 -0
  256. package/src/providers/gemini/client.ts +15 -9
  257. package/src/providers/inference/auth.ts +6 -6
  258. package/src/providers/media-resolve.ts +14 -0
  259. package/src/providers/model-catalog.ts +47 -34
  260. package/src/providers/openai/chat-completions-provider.ts +9 -25
  261. package/src/providers/openai/responses-provider.ts +11 -18
  262. package/src/providers/registry.ts +10 -7
  263. package/src/providers/routing-identity.ts +2 -1
  264. package/src/providers/types.ts +1 -4
  265. package/src/providers/vellum-model-routing.ts +3 -2
  266. package/src/runtime/AGENTS.md +40 -15
  267. package/src/runtime/approval-message-composer.ts +1 -6
  268. package/src/runtime/assistant-event-hub.ts +2 -39
  269. package/src/runtime/channel-approval-types.ts +1 -5
  270. package/src/runtime/channel-reply-delivery.ts +182 -115
  271. package/src/runtime/{slack-reply-session.test.ts → channel-reply-session.test.ts} +294 -156
  272. package/src/runtime/{slack-reply-session.ts → channel-reply-session.ts} +107 -86
  273. package/src/runtime/channel-retry-sweep.ts +102 -1
  274. package/src/runtime/finalize-event-delivery.ts +4 -4
  275. package/src/runtime/guardian-action-message-composer.ts +1 -4
  276. package/src/runtime/guardian-reply-router.ts +4 -75
  277. package/src/runtime/http-router.ts +1 -5
  278. package/src/runtime/question-request-guardian-bridge.ts +2 -3
  279. package/src/runtime/routes/__tests__/conversation-list-assistant-section.test.ts +359 -0
  280. package/src/runtime/routes/__tests__/dictation-command-mode.test.ts +120 -0
  281. package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +487 -0
  282. package/src/runtime/routes/acp-routes.ts +1 -1
  283. package/src/runtime/routes/canned-reply-release.ts +19 -9
  284. package/src/runtime/routes/channel-route-shared.ts +0 -39
  285. package/src/runtime/routes/channel-verification-routes.ts +4 -1
  286. package/src/runtime/routes/conversation-list-routes.ts +44 -3
  287. package/src/runtime/routes/conversation-management-routes.ts +2 -1
  288. package/src/runtime/routes/conversation-routes.ts +138 -59
  289. package/src/runtime/routes/diagnostics-routes.ts +111 -60
  290. package/src/runtime/routes/guardian-action-routes.ts +10 -14
  291. package/src/runtime/routes/guardian-approval-interception.ts +0 -5
  292. package/src/runtime/routes/inbound-message-handler.ts +170 -48
  293. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +4 -3
  294. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +169 -8
  295. package/src/runtime/routes/inbound-stages/background-dispatch.ts +82 -14
  296. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +23 -48
  297. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +454 -94
  298. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +309 -102
  299. package/src/runtime/routes/index.ts +2 -0
  300. package/src/runtime/routes/platform-routes.ts +99 -10
  301. package/src/runtime/routes/sight-frame-routes.ts +136 -0
  302. package/src/runtime/sync/resource-sync-events.ts +14 -37
  303. package/src/runtime/sync/sync-publisher.test.ts +5 -4
  304. package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
  305. package/src/tools/client-os.ts +11 -1
  306. package/src/tools/host-filesystem/edit.ts +2 -2
  307. package/src/tools/host-filesystem/read.ts +2 -2
  308. package/src/tools/host-filesystem/transfer.ts +2 -2
  309. package/src/tools/host-filesystem/write.ts +2 -2
  310. package/src/tools/host-terminal/host-shell.ts +2 -2
  311. package/src/tools/skills/load.ts +1 -1
  312. package/src/tools/terminal/safe-env.ts +4 -0
  313. package/src/tools/tool-input-schemas.ts +2 -0
  314. package/src/tools/tool-manifest.ts +2 -0
  315. package/src/tools/ui-surface/definitions.ts +4 -3
  316. package/src/tools/watch/watch-retro-report.ts +205 -0
  317. package/src/watch/__tests__/watch-retro.test.ts +268 -40
  318. package/src/watch/watch-retro.ts +190 -77
  319. package/src/workspace/migrations/151-repair-renamed-fireworks-deepseek-pro-model-id.ts +195 -0
  320. package/src/workspace/migrations/152-repair-retired-fireworks-minimax-m2p7-model-id.ts +198 -0
  321. package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +0 -10
  322. package/src/workspace/migrations/registry.ts +4 -0
  323. package/docker-kata-pip-chroot.sh +0 -22
  324. package/src/__tests__/slack-reaction-approvals.test.ts +0 -97
  325. package/src/__tests__/slack-reaction-guardian-approval.test.ts +0 -307
  326. package/src/api/events/conversation-list-invalidated.ts +0 -38
@@ -13,6 +13,7 @@
13
13
  import { z } from "zod";
14
14
 
15
15
  import { CHANNEL_IDS } from "../../channels/types.js";
16
+ import { isAssistantInitiatedThreadsEnabled } from "../../config/assistant-initiated-threads-gate.js";
16
17
  import { channelBindingSchema } from "../../messaging/channel-binding-schema.js";
17
18
  import {
18
19
  type Confidence,
@@ -35,7 +36,10 @@ import {
35
36
  listConversations,
36
37
  listPinnedConversations,
37
38
  } from "../../persistence/conversation-queries.js";
38
- import type { ConversationType } from "../../persistence/conversation-types.js";
39
+ import {
40
+ ASSISTANT_INITIATED_GROUP_ID,
41
+ type ConversationType,
42
+ } from "../../persistence/conversation-types.js";
39
43
  import { getBindingsForConversations } from "../../persistence/external-conversation-store.js";
40
44
  import { listGroups } from "../../persistence/group-crud.js";
41
45
  import { UserError } from "../../util/errors.js";
@@ -170,9 +174,16 @@ const sectionCountFields = {
170
174
  * `chats` is the ungrouped-native bucket. In a view without channel sections
171
175
  * the Chats card holds every ungrouped row: the buckets are disjoint, so
172
176
  * that reading is `chats` plus the sum of `channel` entries.
177
+ *
178
+ * `assistant` is emitted only under the `assistant-initiated-threads` flag,
179
+ * and while it is emitted its rows are withheld from `chats`, so the
180
+ * disjointness above holds in both states. A client that does not know the
181
+ * kind ignores it, which is the same thing an older client does with a
182
+ * `channel` section for a channel it has no label for.
173
183
  */
174
184
  const conversationSectionSchema = z.discriminatedUnion("kind", [
175
185
  z.object({ kind: z.literal("pinned"), ...sectionCountFields }),
186
+ z.object({ kind: z.literal("assistant"), ...sectionCountFields }),
176
187
  z.object({
177
188
  kind: z.literal("group"),
178
189
  groupId: z.string(),
@@ -274,6 +285,23 @@ function handleListConversations({ queryParams = {} }: RouteHandlerArgs) {
274
285
  ? true
275
286
  : undefined;
276
287
 
288
+ /* The list half of the section split. A read already scoped to the
289
+ assistant section obviously keeps its rows; every other standard-listing
290
+ read withholds them, so they appear in their own section and nowhere
291
+ else. Confined to `standard`: the background and scheduled buckets are
292
+ back-compat umbrellas addressed by conversation type, and narrowing them
293
+ by source would drop rows their callers still page through. Confined to
294
+ active reads too: the Archive view asks `archiveStatus=archived` with no
295
+ `groupId`, and the section only ever lists active rows, so withholding
296
+ there would leave an archived section thread visible nowhere. */
297
+ const excludeAssistantInitiated =
298
+ isAssistantInitiatedThreadsEnabled() &&
299
+ conversationType === "standard" &&
300
+ archiveStatus === "active" &&
301
+ groupId !== ASSISTANT_INITIATED_GROUP_ID
302
+ ? (true as const)
303
+ : undefined;
304
+
277
305
  const filter: ConversationListFilter = {
278
306
  conversationType,
279
307
  archiveStatus,
@@ -281,6 +309,7 @@ function handleListConversations({ queryParams = {} }: RouteHandlerArgs) {
281
309
  groupId,
282
310
  needsAttention,
283
311
  foregroundOnly,
312
+ excludeAssistantInitiated,
284
313
  };
285
314
  let rows = listConversations({ ...filter, limit, offset });
286
315
  const totalCount = countConversations(filter);
@@ -371,10 +400,22 @@ function handleGetUnreadConversationCount() {
371
400
  * dangling id is a transient state around group deletion, not a section.
372
401
  */
373
402
  function handleGetConversationSections() {
374
- const counts = countConversationSections();
403
+ const splitAssistantInitiated = isAssistantInitiatedThreadsEnabled();
404
+ const counts = countConversationSections({ splitAssistantInitiated });
375
405
  const groupsById = new Map(listGroups().map((g) => [g.id, g]));
376
406
  const sections: Array<z.infer<typeof conversationSectionSchema>> = [];
377
407
 
408
+ /* Unlike every other section this one renders at zero, the way Chats does:
409
+ it is where the assistant's own threads accumulate, and a user who has
410
+ none yet is exactly who the section's empty state is written for. */
411
+ if (counts.assistantInitiated) {
412
+ sections.push({
413
+ kind: "assistant",
414
+ total: counts.assistantInitiated.total,
415
+ unread: counts.assistantInitiated.unread,
416
+ });
417
+ }
418
+
378
419
  for (const row of counts.groups) {
379
420
  if (row.groupId === "system:pinned") {
380
421
  sections.push({ kind: "pinned", total: row.total, unread: row.unread });
@@ -655,7 +696,7 @@ export const ROUTES: RouteDefinition[] = [
655
696
  },
656
697
  summary: "Sidebar section index",
657
698
  description:
658
- "One row per renderable sidebar section (Pinned, each non-empty custom group, each origin channel with unassigned conversations, Chats) with total and unread counts and no conversation rows. Lets a client know which sections exist, and what their badges say, without fetching any conversation list. Totals follow the standard listing visibility; unread applies the same rules as GET /v1/conversations/unread-count scoped to the section. Chats is always present, even at zero: it is the leftover bucket and renders regardless.",
699
+ "One row per renderable sidebar section (Pinned, each non-empty custom group, each origin channel with unassigned conversations, Chats) with total and unread counts and no conversation rows. Lets a client know which sections exist, and what their badges say, without fetching any conversation list. Totals follow the standard listing visibility; unread applies the same rules as GET /v1/conversations/unread-count scoped to the section. Chats is always present, even at zero: it is the leftover bucket and renders regardless. Under the `assistant-initiated-threads` flag an `assistant` section is also present at every count — the threads the assistant started on its own, selectable as a list via `groupId=system:assistant` — and its rows are withheld from Chats so the buckets stay disjoint. With the flag off no `assistant` row is emitted and those conversations are counted in Chats as before.",
659
700
  tags: ["conversations"],
660
701
  responseBody: conversationSectionsResponseSchema,
661
702
  handler: handleGetConversationSections,
@@ -107,7 +107,8 @@ const createConversationTypeSchema = z.enum([
107
107
  "background",
108
108
  ] as const satisfies readonly NonScheduledConversationType[]);
109
109
 
110
- function resolveOrThrow(rawId: string): string {
110
+ /** The conversation an id or key names, or a 404 naming what the caller asked for. */
111
+ export function resolveOrThrow(rawId: string): string {
111
112
  const id = resolveConversationId(rawId);
112
113
  if (!id) {
113
114
  throw new NotFoundError(`Conversation ${rawId} not found`);
@@ -8,6 +8,7 @@ import {
8
8
  type ClientMetadataField,
9
9
  sanitizeClientMetadataValue,
10
10
  } from "@vellumai/service-contracts/client-metadata";
11
+ import { pickReactionEmojiFields } from "@vellumai/service-contracts/reactions";
11
12
  import { v7 as uuidv7 } from "uuid";
12
13
  import { z } from "zod";
13
14
 
@@ -49,7 +50,10 @@ import {
49
50
  } from "../../conversations/message-consolidation.js";
50
51
  import { createApprovalConversationGenerator } from "../../daemon/approval-generators.js";
51
52
  import type { Conversation } from "../../daemon/conversation.js";
52
- import { persistQueuedMessageBody } from "../../daemon/conversation-messaging.js";
53
+ import {
54
+ isConversationBusyError,
55
+ persistQueuedMessageBody,
56
+ } from "../../daemon/conversation-messaging.js";
53
57
  import {
54
58
  buildModelInfoEvent,
55
59
  formatCleanResult,
@@ -599,7 +603,6 @@ async function tryConsumeGuardianReply(params: {
599
603
 
600
604
  const routerResult = await routeGuardianReply({
601
605
  messageText: trimmedContent,
602
- channel: sourceChannel,
603
606
  actor: {
604
607
  actorPrincipalId: verifiedActorPrincipalId,
605
608
  actorExternalUserId: verifiedActorExternalUserId,
@@ -1002,6 +1005,7 @@ export async function handleListMessages({
1002
1005
  let noResponse: boolean | undefined;
1003
1006
  let reaction: ConversationMessage["reaction"];
1004
1007
  let providerError: ConversationMessage["providerError"];
1008
+ let deletedAt: number | undefined;
1005
1009
  if (msg.metadata) {
1006
1010
  try {
1007
1011
  const meta = JSON.parse(msg.metadata) as Record<string, unknown>;
@@ -1016,21 +1020,34 @@ export async function handleListMessages({
1016
1020
  if (isNoResponseMetadata(meta)) {
1017
1021
  noResponse = true;
1018
1022
  }
1019
- // A reaction row, either direction, projects its structured fact so
1020
- // clients never render the stored "[reaction]" sentinel.
1021
- if (msg.metadata.includes("reaction")) {
1022
- const reactionMeta = readProviderMetadata(msg.metadata);
1023
- if (reactionMeta?.eventKind === "reaction" && reactionMeta.reaction) {
1023
+ // Channel facts live in the row's provider envelope. A reaction row,
1024
+ // either direction, projects its structured fact so clients never
1025
+ // render the stored "[reaction]" sentinel; a row deleted on its
1026
+ // channel projects the deletion so clients render a tombstone over
1027
+ // the content the row keeps for audit. The substring guard keeps the
1028
+ // envelope parse off rows that can carry neither fact: both
1029
+ // envelopes spell these keys literally.
1030
+ if (
1031
+ msg.metadata.includes("reaction") ||
1032
+ msg.metadata.includes("deletedAt")
1033
+ ) {
1034
+ const providerMeta = readProviderMetadata(msg.metadata);
1035
+ if (providerMeta?.eventKind === "reaction" && providerMeta.reaction) {
1036
+ const r = providerMeta.reaction;
1024
1037
  reaction = {
1025
- emoji: reactionMeta.reaction.emoji,
1026
- op: reactionMeta.reaction.op,
1027
- targetMessageId: reactionMeta.reaction.targetMessageId,
1028
- ...(reactionMeta.reaction.actorDisplayName
1029
- ? { actorDisplayName: reactionMeta.reaction.actorDisplayName }
1038
+ emoji: r.emoji,
1039
+ ...pickReactionEmojiFields(r),
1040
+ op: r.op,
1041
+ targetMessageId: r.targetMessageId,
1042
+ ...(r.actorDisplayName
1043
+ ? { actorDisplayName: r.actorDisplayName }
1030
1044
  : {}),
1031
1045
  ...(msg.role === "assistant" ? { selfAuthored: true } : {}),
1032
1046
  };
1033
1047
  }
1048
+ if (providerMeta?.deletedAt !== undefined) {
1049
+ deletedAt = providerMeta.deletedAt;
1050
+ }
1034
1051
  }
1035
1052
  // Daemon-persisted provider-failure notices carry the classified
1036
1053
  // error code/category so clients can render a themed card instead
@@ -1088,6 +1105,7 @@ export async function handleListMessages({
1088
1105
  reaction,
1089
1106
  providerError,
1090
1107
  slackMessage,
1108
+ deletedAt,
1091
1109
  clientMessageId: msg.clientMessageId ?? undefined,
1092
1110
  };
1093
1111
  });
@@ -1298,6 +1316,7 @@ export async function handleListMessages({
1298
1316
  ...(m.reaction ? { reaction: m.reaction } : {}),
1299
1317
  ...(m.providerError ? { providerError: m.providerError } : {}),
1300
1318
  ...(m.slackMessage ? { slackMessage: m.slackMessage } : {}),
1319
+ ...(m.deletedAt !== undefined ? { deletedAt: m.deletedAt } : {}),
1301
1320
  };
1302
1321
  }),
1303
1322
  );
@@ -2043,23 +2062,15 @@ export async function handleSendMessage(
2043
2062
  ? buildSelfIntroMessage(body.onboarding ?? undefined)
2044
2063
  : undefined;
2045
2064
 
2046
- let effectiveContent: string | undefined;
2047
- if (isScanPath) {
2048
- const scanVariant = websiteUrl
2049
- ? ("website" as const)
2050
- : ("content-source" as const);
2051
- effectiveContent = buildScanFirstMessage(scanUrl, scanVariant);
2052
- // Fall through to normal inference path below
2053
- } else if (selfIntroGreetingEnabled && body.onboarding?.initialMessage) {
2054
- effectiveContent = body.onboarding.initialMessage;
2055
- } else if (isWakeUp && selfIntro) {
2056
- // Rewrite to the self-introduction and fall through to real inference
2057
- // (mirrors the scan path above).
2058
- effectiveContent = selfIntro;
2059
- } else if (isWakeUp) {
2060
- const cannedGreeting = getCannedFirstGreeting(body.onboarding ?? undefined);
2061
-
2062
- conversation.setProcessing(true);
2065
+ /**
2066
+ * Answer a first wake-up with the canned greeting, under a claim the caller
2067
+ * already holds. Its own function so the branch below can decline the claim
2068
+ * and fall through to the ordinary path instead.
2069
+ */
2070
+ const serveCannedFirstGreeting = async (
2071
+ cannedGreeting: string,
2072
+ greetingOwner: number,
2073
+ ) => {
2063
2074
  let cleanupDeferred = false;
2064
2075
  try {
2065
2076
  const rawContent = content ?? "";
@@ -2122,6 +2133,7 @@ export async function handleSendMessage(
2122
2133
 
2123
2134
  scheduleCannedReplyRelease({
2124
2135
  conversation,
2136
+ owner: greetingOwner,
2125
2137
  origin: "canned_greeting",
2126
2138
  emit: () => {
2127
2139
  broadcastMessage({
@@ -2159,11 +2171,41 @@ export async function handleSendMessage(
2159
2171
  cleanupDeferred = true;
2160
2172
  return response;
2161
2173
  } finally {
2162
- if (!cleanupDeferred && conversation.isProcessing()) {
2163
- conversation.setProcessing(false);
2174
+ if (!cleanupDeferred && conversation.releaseProcessing(greetingOwner)) {
2164
2175
  void conversation.kickDrainQueue("loop_complete", "send_error_path");
2165
2176
  }
2166
2177
  }
2178
+ };
2179
+
2180
+ let effectiveContent: string | undefined;
2181
+ if (isScanPath) {
2182
+ const scanVariant = websiteUrl
2183
+ ? ("website" as const)
2184
+ : ("content-source" as const);
2185
+ effectiveContent = buildScanFirstMessage(scanUrl, scanVariant);
2186
+ // Fall through to normal inference path below
2187
+ } else if (selfIntroGreetingEnabled && body.onboarding?.initialMessage) {
2188
+ effectiveContent = body.onboarding.initialMessage;
2189
+ } else if (isWakeUp && selfIntro) {
2190
+ // Rewrite to the self-introduction and fall through to real inference
2191
+ // (mirrors the scan path above).
2192
+ effectiveContent = selfIntro;
2193
+ } else if (isWakeUp) {
2194
+ const cannedGreeting = getCannedFirstGreeting(body.onboarding ?? undefined);
2195
+
2196
+ // Taken rather than set. The idle read behind this branch is several
2197
+ // awaits old, so a hold taken since belongs to a real turn, and greeting
2198
+ // over it would run two operations into one history. Declining and
2199
+ // falling through is what the sibling branches above already do, and the
2200
+ // gate below turns that into the queued answer a busy conversation owes.
2201
+ const greetingOwner = await conversation.acquireProcessingFenced();
2202
+ if (greetingOwner !== null) {
2203
+ return serveCannedFirstGreeting(cannedGreeting, greetingOwner);
2204
+ }
2205
+ log.info(
2206
+ { conversationId: mapping.conversationId },
2207
+ "Canned first greeting yielded: the conversation is already held",
2208
+ );
2167
2209
  }
2168
2210
 
2169
2211
  if (isFirstOnboarding) {
@@ -2236,11 +2278,23 @@ export async function handleSendMessage(
2236
2278
  );
2237
2279
  }
2238
2280
 
2239
- if (conversation.isProcessing()) {
2281
+ /**
2282
+ * Put the message on the conversation's queue and answer with the
2283
+ * accepted-and-queued shape.
2284
+ *
2285
+ * Reached from the check just below, and again from every point past it that
2286
+ * finds the conversation busy after all. That check and the acquire which
2287
+ * actually starts a turn are separated by guardian cleanup, history scoping
2288
+ * and slash resolution, so anything taking the flag inside those awaits
2289
+ * leaves a send that has to queue rather than fail. Answering the same way
2290
+ * from both means a client cannot tell which side of the awaits the
2291
+ * conversation went busy on.
2292
+ */
2293
+ const queueSend = async (content: string) => {
2240
2294
  // Queue the message so it's processed when the current turn completes
2241
2295
  const requestId = uuidv7();
2242
2296
  const enqueueResult = conversation.enqueueMessage({
2243
- content: contentAfterScan,
2297
+ content,
2244
2298
  attachments,
2245
2299
  onEvent: broadcastMessage,
2246
2300
  requestId,
@@ -2325,6 +2379,10 @@ export async function handleSendMessage(
2325
2379
  conversationId: mapping.conversationId,
2326
2380
  requestId,
2327
2381
  };
2382
+ };
2383
+
2384
+ if (conversation.isProcessing()) {
2385
+ return queueSend(contentAfterScan);
2328
2386
  }
2329
2387
 
2330
2388
  // Auto-deny pending confirmations for idle conversations. The legacy
@@ -2405,7 +2463,10 @@ export async function handleSendMessage(
2405
2463
  const slashResult = await resolveSlash(rawContent, slashContext);
2406
2464
 
2407
2465
  if (slashResult.kind === "unknown") {
2408
- conversation.setProcessing(true);
2466
+ const slashOwner = await conversation.acquireProcessingFenced();
2467
+ if (slashOwner === null) {
2468
+ return queueSend(rawContent);
2469
+ }
2409
2470
  let cleanupDeferred = false;
2410
2471
  try {
2411
2472
  const slashMeta = {
@@ -2470,6 +2531,7 @@ export async function handleSendMessage(
2470
2531
  const message = slashResult.message;
2471
2532
  scheduleCannedReplyRelease({
2472
2533
  conversation,
2534
+ owner: slashOwner,
2473
2535
  origin: "slash_command",
2474
2536
  emit: () => {
2475
2537
  broadcastMessage({
@@ -2503,15 +2565,17 @@ export async function handleSendMessage(
2503
2565
  } finally {
2504
2566
  // No-op for the slash-command early-return path (handled inside
2505
2567
  // setTimeout above), but still needed for error paths.
2506
- if (!cleanupDeferred && conversation.isProcessing()) {
2507
- conversation.setProcessing(false);
2568
+ if (!cleanupDeferred && conversation.releaseProcessing(slashOwner)) {
2508
2569
  void conversation.kickDrainQueue("loop_complete", "send_error_path");
2509
2570
  }
2510
2571
  }
2511
2572
  }
2512
2573
 
2513
2574
  if (slashResult.kind === "compact") {
2514
- conversation.setProcessing(true);
2575
+ const compactOwner = await conversation.acquireProcessingFenced();
2576
+ if (compactOwner === null) {
2577
+ return queueSend(rawContent);
2578
+ }
2515
2579
  const slashMeta = {
2516
2580
  userMessageChannel: sourceChannel,
2517
2581
  assistantMessageChannel: sourceChannel,
@@ -2532,12 +2596,12 @@ export async function handleSendMessage(
2532
2596
  // The fire-and-forget compaction below owns clearing `processing`, but a
2533
2597
  // throw from this initial persist never reaches it — reset here so the
2534
2598
  // conversation isn't stranded in queued mode.
2535
- conversation.setProcessing(false);
2599
+ conversation.releaseProcessing(compactOwner);
2536
2600
  void conversation.kickDrainQueue("loop_complete", "compact_command");
2537
2601
  throw err;
2538
2602
  }
2539
2603
  if (persisted.deduplicated) {
2540
- conversation.setProcessing(false);
2604
+ conversation.releaseProcessing(compactOwner);
2541
2605
  void conversation.kickDrainQueue("loop_complete", "compact_dedup");
2542
2606
  return {
2543
2607
  accepted: true,
@@ -2589,7 +2653,7 @@ export async function handleSendMessage(
2589
2653
  retryable: true,
2590
2654
  });
2591
2655
  } finally {
2592
- conversation.setProcessing(false);
2656
+ conversation.releaseProcessing(compactOwner);
2593
2657
  void conversation.kickDrainQueue("loop_complete", "compact_command");
2594
2658
  }
2595
2659
  })();
@@ -2602,7 +2666,10 @@ export async function handleSendMessage(
2602
2666
  }
2603
2667
 
2604
2668
  if (slashResult.kind === "clean") {
2605
- conversation.setProcessing(true);
2669
+ const cleanOwner = await conversation.acquireProcessingFenced();
2670
+ if (cleanOwner === null) {
2671
+ return queueSend(rawContent);
2672
+ }
2606
2673
  const conversationId = mapping.conversationId;
2607
2674
  // Outer try/finally guarantees the processing flag is cleared (and the
2608
2675
  // queue drained) on every failure path — including a throw from the
@@ -2668,7 +2735,7 @@ export async function handleSendMessage(
2668
2735
  conversationId,
2669
2736
  };
2670
2737
  } finally {
2671
- conversation.setProcessing(false);
2738
+ conversation.releaseProcessing(cleanOwner);
2672
2739
  void conversation.kickDrainQueue("loop_complete", "clean_command");
2673
2740
  }
2674
2741
  }
@@ -2676,23 +2743,35 @@ export async function handleSendMessage(
2676
2743
  const resolvedContent = slashResult.content;
2677
2744
 
2678
2745
  const requestId = uuidv7();
2679
- const persistResult = await conversation.persistUserMessage({
2680
- content: resolvedContent,
2681
- attachments,
2682
- requestId,
2683
- metadata: withClientMetadata(
2684
- body.automated === true || body.hidden === true
2685
- ? {
2686
- ...(body.automated === true ? { automated: true } : {}),
2687
- ...(body.hidden === true ? { hidden: true } : {}),
2688
- }
2689
- : undefined,
2690
- clientMetadata,
2691
- ),
2692
- scripted: body.scripted,
2693
- clientMessageId,
2694
- ...(clientOs ? { requestClientOs: clientOs } : {}),
2695
- });
2746
+ let persistResult: Awaited<
2747
+ ReturnType<typeof conversation.persistUserMessage>
2748
+ >;
2749
+ try {
2750
+ persistResult = await conversation.persistUserMessage({
2751
+ content: resolvedContent,
2752
+ attachments,
2753
+ requestId,
2754
+ metadata: withClientMetadata(
2755
+ body.automated === true || body.hidden === true
2756
+ ? {
2757
+ ...(body.automated === true ? { automated: true } : {}),
2758
+ ...(body.hidden === true ? { hidden: true } : {}),
2759
+ }
2760
+ : undefined,
2761
+ clientMetadata,
2762
+ ),
2763
+ scripted: body.scripted,
2764
+ clientMessageId,
2765
+ ...(clientOs ? { requestClientOs: clientOs } : {}),
2766
+ });
2767
+ } catch (err) {
2768
+ if (isConversationBusyError(err)) {
2769
+ // The flag went to someone else inside the awaits above. This is the
2770
+ // same message the check at the top would have queued, so queue it.
2771
+ return queueSend(resolvedContent);
2772
+ }
2773
+ throw err;
2774
+ }
2696
2775
 
2697
2776
  const messageId = persistResult.id;
2698
2777