@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
@@ -3,16 +3,25 @@ import type { MessageAudience } from "@vellumai/gateway-client";
3
3
  import { stripVellumLinks } from "../daemon/assistant-attachments.js";
4
4
  import type { RenderedHistoryContent } from "../daemon/handlers/shared.js";
5
5
  import { renderHistoryContent } from "../daemon/handlers/shared.js";
6
- import { readProviderMessageMetadata } from "../messaging/provider-message-metadata.js";
6
+ import type { ProviderMessageMetadata } from "../messaging/provider-message-metadata.js";
7
+ import {
8
+ everyPostDeleted,
9
+ readProviderMessageMetadata,
10
+ } from "../messaging/provider-message-metadata.js";
7
11
  import { editChannelMessage } from "../messaging/providers/index.js";
8
- import { readSlackMetadata } from "../messaging/providers/slack/message-metadata.js";
12
+ import { providerMetadataOfPreSendSlackEnvelope } from "../messaging/providers/slack/message-metadata.js";
9
13
  import { getAttachmentMetadataForMessage } from "../persistence/attachments-store.js";
10
14
  import {
11
15
  getMessageById,
12
16
  getMessages,
17
+ parseMessageMetadata,
13
18
  updateMessageMetadata,
14
19
  } from "../persistence/conversation-crud.js";
20
+ import { isReactionMessageMetadata } from "../persistence/conversation-types.js";
21
+ import { recordOutboundPost } from "../persistence/delivery-crud.js";
22
+ import { safeParseRecord } from "../util/json.js";
15
23
  import { getLogger } from "../util/logger.js";
24
+ import { withSqliteRetry } from "../util/sqlite-retry.js";
16
25
  import type { ChannelDeliveryResult } from "./gateway-client.js";
17
26
  import { deliverChannelReply } from "./gateway-client.js";
18
27
  import type { RuntimeAttachmentMetadata } from "./http-types.js";
@@ -48,8 +57,10 @@ type DeliverRenderedReplyParams = {
48
57
  * identified by this ts instead of posting a new one (Slack-specific). */
49
58
  messageTs?: string;
50
59
  /** Called with the ts of the delivered/updated message so callers
51
- * can use it for subsequent updates. */
52
- onMessageTs?: (ts: string) => void;
60
+ * can use it for subsequent updates. Awaited when it returns a promise,
61
+ * so an async handler (the sent-message-id reconciler) completes its
62
+ * durable write before the next segment posts. */
63
+ onMessageTs?: (ts: string) => void | Promise<void>;
53
64
  };
54
65
 
55
66
  function sleep(ms: number): Promise<void> {
@@ -172,7 +183,7 @@ export async function deliverRenderedReplyViaCallback(
172
183
  },
173
184
  );
174
185
  if (result.ts) {
175
- onMessageTs?.(result.ts);
186
+ await onMessageTs?.(result.ts);
176
187
  }
177
188
  }
178
189
  return;
@@ -191,10 +202,10 @@ export async function deliverRenderedReplyViaCallback(
191
202
  );
192
203
  const deliveredTs = result.ts ?? messageTs;
193
204
  if (deliveredTs) {
194
- onMessageTs?.(deliveredTs);
205
+ await onMessageTs?.(deliveredTs);
195
206
  }
196
207
  } else if (messageTs) {
197
- onMessageTs?.(messageTs);
208
+ await onMessageTs?.(messageTs);
198
209
  }
199
210
  return;
200
211
  }
@@ -255,7 +266,7 @@ export async function deliverRenderedReplyViaCallback(
255
266
 
256
267
  if (result.ts) {
257
268
  currentMessageTs = result.ts;
258
- onMessageTs?.(result.ts);
269
+ await onMessageTs?.(result.ts);
259
270
  }
260
271
 
261
272
  onSegmentDelivered?.(i + 1);
@@ -282,16 +293,55 @@ export type DeliverReplyOptions = {
282
293
  audience?: MessageAudience;
283
294
  /** Update an existing message instead of posting a new one. */
284
295
  messageTs?: string;
285
- /** Called with the ts of the delivered/updated message. */
286
- onMessageTs?: (ts: string) => void;
296
+ /** Called with the ts of the delivered/updated message. Awaited when it
297
+ * returns a promise. */
298
+ onMessageTs?: (ts: string) => void | Promise<void>;
287
299
  };
288
300
 
289
301
  type PersistedMessage = ReturnType<typeof getMessages>[number];
290
302
 
303
+ /**
304
+ * A row read that contributes nothing to deliver. Spelled out rather than
305
+ * rendered from empty content so the read stays independent of the renderer;
306
+ * the annotation is what keeps it complete as the shape grows.
307
+ */
308
+ const NO_REPLY_CONTENT: RenderedHistoryContent = {
309
+ text: "",
310
+ toolCalls: [],
311
+ toolCallsBeforeText: false,
312
+ textSegments: [],
313
+ contentOrder: [],
314
+ surfaces: [],
315
+ thinkingSegments: [],
316
+ attachments: [],
317
+ contentBlocks: [],
318
+ };
319
+
320
+ /**
321
+ * Read a persisted assistant row as a candidate channel reply.
322
+ *
323
+ * A reaction row reads as empty on purpose. Its `"[reaction]"` body is a
324
+ * storage sentinel for an emoji the react tool already delivered to the
325
+ * channel, not speech the turn owes anyone, and the row is drained at the
326
+ * turn boundary, so it is the newest assistant row of any turn that
327
+ * reacted. Read literally, its non-empty text counts as a real deliverable
328
+ * reply and outranks the turn's actual reply (or its `<no_response/>`
329
+ * silence) in every newest-first scan below, posting the raw sentinel to the
330
+ * channel as visible text. Reading it as empty is what makes a reaction-only
331
+ * turn silent, and it is the one seam every delivery path shares: the turn
332
+ * scan, the unbounded fallback sweep, and the targeted `messageId` path,
333
+ * which needs it too because the sweep durably stores whatever the scan
334
+ * returns. The history renderers project these rows as a structured reaction
335
+ * fact for the same reason; delivery owes the channel nothing for them.
336
+ */
291
337
  function readPersistedAssistantReply(msg: PersistedMessage): {
292
338
  rendered: RenderedHistoryContent;
293
339
  replyAttachments: RuntimeAttachmentMetadata[];
294
340
  } {
341
+ if (isReactionMessageMetadata(parseMessageMetadata(msg.metadata))) {
342
+ return { rendered: NO_REPLY_CONTENT, replyAttachments: [] };
343
+ }
344
+
295
345
  const parsed: unknown = msg.content;
296
346
  const rendered = renderHistoryContent(parsed);
297
347
 
@@ -387,17 +437,15 @@ async function deliverPersistedAssistantMessageViaCallback(
387
437
  // Compose an `onMessageTs` that reconciles the persisted assistant row's
388
438
  // provider message ids as the transport reports the authoritative ones.
389
439
  // The assistant row is written BEFORE the gateway POST, so its pre-send
390
- // envelope names no id of its own: a Slack row's partial `slackMeta` lacks
391
- // `channelTs` and reads as null through `readSlackMetadata`, and a
392
- // neutral-envelope row lacks the `messageId` a later reaction
393
- // naming it resolves by. A reply split into several segments reports one
394
- // id per posted provider message, all reconciled onto this one row; see
395
- // `makeSentMessageIdReconciler` for the per-envelope rules.
440
+ // envelope lacks the `messageId` a later reaction or delete naming it
441
+ // resolves by. A reply split into several segments reports one id per
442
+ // posted provider message, all reconciled onto this one row; see
443
+ // `makeSentMessageIdReconciler`.
396
444
  const reconcileOnMessageTs = makeSentMessageIdReconciler(msg.id);
397
445
  const callerOnMessageTs = options?.onMessageTs;
398
- const composedOnMessageTs = (ts: string): void => {
399
- reconcileOnMessageTs(ts);
400
- callerOnMessageTs?.(ts);
446
+ const composedOnMessageTs = async (ts: string): Promise<void> => {
447
+ await reconcileOnMessageTs(ts);
448
+ await callerOnMessageTs?.(ts);
401
449
  };
402
450
 
403
451
  await deliverRenderedReplyViaCallback({
@@ -493,133 +541,152 @@ export async function deliverReplyViaCallback(
493
541
 
494
542
  /**
495
543
  * Build an `onMessageTs` handler that reconciles the persisted assistant
496
- * row's provider message ids from the transport's authoritative ones:
497
- * `slackMeta.channelTs` for a Slack row, `providerMeta.messageId` plus
498
- * `additionalMessageIds` for a row carrying the neutral envelope (Discord
499
- * today, any transport whose delivery result reports the sent id). The
500
- * back-filled ids are what let a later reaction naming the
501
- * assistant's own post resolve back to this row.
544
+ * row's provider message ids from the transport's authoritative ones. Every
545
+ * channel's reply row carries the neutral envelope (`readReplyEnvelope`
546
+ * names the one pending row that may not yet), so there is one rule:
547
+ * the first reported id becomes `providerMeta.messageId`, the further posts
548
+ * of a reply split at tool boundaries or length limits accumulate under
549
+ * `additionalMessageIds`, and every id lands in the `channel_outbound_posts`
550
+ * index, which is what lets a later reaction or delete naming any of the
551
+ * assistant's posts resolve back to this row.
502
552
  *
503
- * Behavior:
504
- * - A Slack row acts on the first invocation only: `channelTs` is the one
505
- * id its envelope names, and later split segments are independent Slack
506
- * messages it cannot carry.
507
- * - A neutral-envelope row records every reported id: the first as
508
- * `messageId`, the rest under `additionalMessageIds`, all naming this
509
- * same row. An id already recorded (a redelivery) is skipped.
510
- * - No-op when the row was persisted with neither envelope (e.g. vellum
511
- * outbound).
512
- * - Failures are logged and swallowed so a transient DB error cannot break
513
- * the outbound delivery itself.
553
+ * An id the row already names is a redelivery and is skipped. A row
554
+ * persisted with no envelope (e.g. vellum outbound) is left untouched. Every
555
+ * write retries transient SQLite contention (`withSqliteRetry`), because
556
+ * this is the only durable record of the sent message's id and nothing
557
+ * revisits it once the event is marked delivered. Remaining failures are
558
+ * logged and swallowed so a DB error cannot break the outbound delivery
559
+ * itself.
514
560
  */
515
- function makeSentMessageIdReconciler(messageId: string): (ts: string) => void {
516
- let slackApplied = false;
517
- return (ts: string): void => {
561
+ function makeSentMessageIdReconciler(
562
+ messageId: string,
563
+ ): (ts: string) => Promise<void> {
564
+ return async (ts: string): Promise<void> => {
518
565
  if (!ts) {
519
566
  return;
520
567
  }
521
568
  try {
522
- // Re-read the row's current metadata so a concurrent edit-propagation
523
- // write (e.g. `editedAt`) is not clobbered. `updateMessageMetadata`
524
- // shallow-merges into the top-level envelope, and the per-channel
525
- // sub-object is merged manually below so we can preserve fields on
526
- // the partial pre-send envelope (`mergeSlackMetadata` would call
527
- // `readSlackMetadata` which rejects the partial form for lacking
528
- // channelTs, exactly the state we are reconciling).
529
569
  const row = getMessageById(messageId);
530
570
  if (row === null || row.metadata === null) {
531
571
  return;
532
572
  }
533
- let envelope: Record<string, unknown>;
534
- try {
535
- envelope = JSON.parse(row.metadata) as Record<string, unknown>;
536
- } catch {
537
- return;
538
- }
539
- const slackMetaRaw =
540
- typeof envelope.slackMeta === "string" ? envelope.slackMeta : null;
541
- if (slackMetaRaw === null) {
542
- reconcileProviderMessageId(messageId, envelope, ts);
543
- return;
544
- }
545
- if (slackApplied) {
546
- return;
547
- }
548
- slackApplied = true;
549
- // If the existing slackMeta already parses cleanly via the strict
550
- // reader, channelTs is already present (a prior reconciliation ran,
551
- // or backfill stamped the field) — nothing to do.
552
- if (readSlackMetadata(slackMetaRaw) !== null) {
553
- return;
554
- }
555
- // Lenient parse of the partial slackMeta so we can preserve every
556
- // field already written by `handleMessageComplete` (source,
557
- // eventKind, channelId, threadTs, ...) while patching channelTs in.
558
- let existingSlackMeta: Record<string, unknown>;
559
- try {
560
- const parsed = JSON.parse(slackMetaRaw) as unknown;
561
- if (
562
- parsed === null ||
563
- typeof parsed !== "object" ||
564
- Array.isArray(parsed)
565
- ) {
566
- return;
567
- }
568
- existingSlackMeta = parsed as Record<string, unknown>;
569
- } catch {
573
+ const providerMeta = readReplyEnvelope(safeParseRecord(row.metadata));
574
+ if (
575
+ providerMeta === null ||
576
+ providerIdPatchFor(providerMeta, ts) === null
577
+ ) {
570
578
  return;
571
579
  }
572
- const mergedSlackMeta = JSON.stringify({
573
- ...existingSlackMeta,
574
- channelTs: ts,
575
- // Force `source: "slack"` for parity with `mergeSlackMetadata`'s
576
- // invariant — the reader rejects anything else and we never want a
577
- // reconciled row to slip through with a stale source.
578
- source: "slack",
580
+ // The index first: it is the resolution contract, and the insert is
581
+ // conflict-ignoring, so a redelivered id is a no-op there too.
582
+ await withSqliteRetry(
583
+ () =>
584
+ recordOutboundPost({
585
+ sourceChannel: providerMeta.source,
586
+ externalChatId: providerMeta.conversationExternalId,
587
+ providerMessageId: ts,
588
+ messageId,
589
+ conversationId: row.conversationId,
590
+ }),
591
+ { op: "record_outbound_post", context: { messageId } },
592
+ );
593
+ // The envelope is read and written in one synchronous step, re-read on
594
+ // every attempt, and the delete stage stamps its rows the same way, so
595
+ // neither overwrites the other's patch: bun:sqlite is synchronous and
596
+ // nothing yields between the read and the write.
597
+ await withSqliteRetry(() => stampSentMessageId(messageId, ts), {
598
+ op: "reconcile_sent_message_id",
599
+ context: { messageId },
579
600
  });
580
- updateMessageMetadata(messageId, { slackMeta: mergedSlackMeta });
581
601
  } catch (err) {
582
602
  log.warn(
583
603
  { err, messageId },
584
- "Failed to reconcile slackMeta.channelTs on outbound assistant row",
604
+ "Failed to reconcile the sent message id on the outbound assistant row",
585
605
  );
586
606
  }
587
607
  };
588
608
  }
589
609
 
590
610
  /**
591
- * The neutral-envelope arm of the reconciliation: record the transport's
592
- * sent-message id on the row's `providerMeta`. The first reported id
593
- * becomes `messageId`; further ids (a reply split at tool boundaries posts
594
- * several provider messages) accumulate under `additionalMessageIds`. An id
595
- * the envelope already names is a redelivery and is skipped, and a row
596
- * carrying no valid neutral envelope is left untouched.
611
+ * Record a reported id on the row's envelope. A post just reported is on the
612
+ * channel, so the row-level deletion marker is recomputed: it stays only
613
+ * when every post the row now names, this one included, is already recorded
614
+ * as deleted (a deletion can land between the index write and this stamp,
615
+ * and it can name this very post); a marker left by an earlier state in
616
+ * which every known post was gone is cleared. Synchronous by design; see
617
+ * the caller.
597
618
  */
598
- function reconcileProviderMessageId(
599
- messageId: string,
600
- envelope: Record<string, unknown>,
601
- ts: string,
602
- ): void {
603
- const providerMeta = readProviderMessageMetadata(envelope.providerMeta);
604
- if (providerMeta === null) {
619
+ function stampSentMessageId(messageId: string, ts: string): void {
620
+ const row = getMessageById(messageId);
621
+ if (row === null || row.metadata === null) {
605
622
  return;
606
623
  }
607
- if (providerMeta.messageId === undefined) {
608
- updateMessageMetadata(messageId, {
609
- providerMeta: JSON.stringify({ ...providerMeta, messageId: ts }),
610
- });
624
+ const envelope = safeParseRecord(row.metadata);
625
+ const providerMeta = readReplyEnvelope(envelope);
626
+ if (providerMeta === null) {
611
627
  return;
612
628
  }
613
- if (
614
- providerMeta.messageId === ts ||
615
- providerMeta.additionalMessageIds?.includes(ts)
616
- ) {
629
+ const patch = providerIdPatchFor(providerMeta, ts);
630
+ if (patch === null) {
617
631
  return;
618
632
  }
633
+ const posts = [
634
+ ...(providerMeta.messageId !== undefined ? [providerMeta.messageId] : []),
635
+ ...(providerMeta.additionalMessageIds ?? []),
636
+ ts,
637
+ ];
638
+ const { deletedAt, ...rest } = providerMeta;
619
639
  updateMessageMetadata(messageId, {
620
640
  providerMeta: JSON.stringify({
621
- ...providerMeta,
622
- additionalMessageIds: [...(providerMeta.additionalMessageIds ?? []), ts],
641
+ ...rest,
642
+ ...patch,
643
+ ...(deletedAt !== undefined &&
644
+ everyPostDeleted(posts, providerMeta.deletedMessageIds)
645
+ ? { deletedAt }
646
+ : {}),
623
647
  }),
648
+ // A row reserved with Slack's pre-send envelope converges on the neutral
649
+ // one here: `updateMessageMetadata` serializes the merged envelope with
650
+ // `JSON.stringify`, which drops a key set to undefined.
651
+ ...(envelope.slackMeta !== undefined ? { slackMeta: undefined } : {}),
624
652
  });
625
653
  }
654
+
655
+ /**
656
+ * The envelope a reply row stamps its sent ids onto: the neutral one every
657
+ * reply the daemon reserves carries, or, for a reply still pending from a
658
+ * daemon that reserved Slack replies under Slack's own envelope, the neutral
659
+ * reading of that pre-send envelope (transitional; see
660
+ * `providerMetadataOfPreSendSlackEnvelope`).
661
+ */
662
+ function readReplyEnvelope(
663
+ envelope: Record<string, unknown>,
664
+ ): ProviderMessageMetadata | null {
665
+ return (
666
+ readProviderMessageMetadata(envelope.providerMeta) ??
667
+ providerMetadataOfPreSendSlackEnvelope(envelope)
668
+ );
669
+ }
670
+
671
+ /**
672
+ * Where a newly reported id belongs on the neutral envelope: `messageId` when
673
+ * the row names none yet, otherwise appended to `additionalMessageIds`.
674
+ * `null` when the envelope already names it, which is a redelivery.
675
+ */
676
+ function providerIdPatchFor(
677
+ providerMeta: ProviderMessageMetadata,
678
+ ts: string,
679
+ ): Partial<ProviderMessageMetadata> | null {
680
+ if (providerMeta.messageId === undefined) {
681
+ return { messageId: ts };
682
+ }
683
+ if (
684
+ providerMeta.messageId === ts ||
685
+ providerMeta.additionalMessageIds?.includes(ts)
686
+ ) {
687
+ return null;
688
+ }
689
+ return {
690
+ additionalMessageIds: [...(providerMeta.additionalMessageIds ?? []), ts],
691
+ };
692
+ }