@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
@@ -144,12 +144,12 @@ export type ConnectionProvider = string;
144
144
  export const CHATGPT_SUBSCRIPTION_CONNECTION_NAME = "chatgpt-subscription";
145
145
 
146
146
  /**
147
- * Provider values that are routing identities rather than adapters: the
148
- * value names HOW a request routes (vellum = the platform-managed route,
149
- * chatgpt = the subscription route), and dispatch translates it to a real
150
- * upstream + connection row per-request (resolveRoutingIdentity). Identity
151
- * profiles carry no provider_connection; backfill and materialization must
152
- * not stamp one.
147
+ * Provider values that name a routing identity. Dispatch translates them
148
+ * to a connection row and an upstream factory id via
149
+ * `resolveRoutingIdentity`. `chatgpt` has no adapter factory (upstream is
150
+ * always openai). `vellum` is also a catalog factory id when the derived
151
+ * upstream is vellum. Identity profiles carry no provider_connection;
152
+ * backfill and materialization must not stamp one.
153
153
  */
154
154
  export const ROUTING_IDENTITY_PROVIDERS: ReadonlySet<string> = new Set([
155
155
  "vellum",
@@ -28,6 +28,7 @@ import {
28
28
  sniffImageMimeType,
29
29
  } from "../util/image-conversion.js";
30
30
  import { getLogger } from "../util/logger.js";
31
+ import { keepFileAsWorkspaceRef } from "./content-block-size.js";
31
32
  import {
32
33
  attachmentIdFragment,
33
34
  type Base64MediaSource,
@@ -265,6 +266,16 @@ async function resolveImageBlock(
265
266
  }
266
267
 
267
268
  function resolveFileBlock(block: FileContent): ContentBlock {
269
+ // Video and over-cap text stay a workspace file. Do not load bytes or
270
+ // carry extracted_text into the provider prompt: serializers name the
271
+ // file and stop there.
272
+ if (keepFileAsWorkspaceRef(block.source)) {
273
+ if (block.extracted_text === undefined) {
274
+ return block;
275
+ }
276
+ const { extracted_text: _extracted, ...rest } = block;
277
+ return rest;
278
+ }
268
279
  if (block.source.type === "base64") {
269
280
  return block;
270
281
  }
@@ -356,6 +367,9 @@ function contentNeedsResolution(
356
367
  : base64ImageNeedsRewrite(block.source, options);
357
368
  }
358
369
  if (block.type === "file") {
370
+ if (keepFileAsWorkspaceRef(block.source)) {
371
+ return block.extracted_text !== undefined;
372
+ }
359
373
  return block.source.type === "workspace_ref";
360
374
  }
361
375
  if (block.type === "tool_result" && block.contentBlocks?.length) {
@@ -631,6 +631,22 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
631
631
  linkLabel: "Open Google AI Studio",
632
632
  },
633
633
  models: [
634
+ {
635
+ id: "gemini-3.8-flash",
636
+ displayName: "Gemini 3.8 Flash",
637
+ contextWindowTokens: 1048576,
638
+ maxOutputTokens: 65536,
639
+ supportsThinking: true,
640
+ thinkingFloor: "low",
641
+ supportsCaching: true,
642
+ supportsVision: true,
643
+ supportsToolUse: true,
644
+ pricing: {
645
+ inputPer1mTokens: 1.5,
646
+ outputPer1mTokens: 7.5,
647
+ cacheReadPer1mTokens: 0.15,
648
+ },
649
+ },
634
650
  {
635
651
  id: "gemini-3.7-flash",
636
652
  displayName: "Gemini 3.7 Flash",
@@ -928,23 +944,6 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
928
944
  cacheReadPer1mTokens: 0.16,
929
945
  },
930
946
  },
931
- {
932
- id: "accounts/fireworks/models/glm-5p2",
933
- displayName: "GLM 5.2",
934
- // Fireworks serves GLM 5.2 with a 1,040K input window.
935
- contextWindowTokens: 1040000,
936
- maxOutputTokens: 131072,
937
- supportsThinking: true,
938
- supportsCaching: true,
939
- supportsVision: false,
940
- supportsToolUse: true,
941
- maxEffort: "max",
942
- pricing: {
943
- inputPer1mTokens: 1.4,
944
- outputPer1mTokens: 4.4,
945
- cacheReadPer1mTokens: 0.26,
946
- },
947
- },
948
947
  {
949
948
  id: "accounts/fireworks/models/glm-5p3",
950
949
  displayName: "GLM 5.3",
@@ -984,6 +983,23 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
984
983
  cacheReadPer1mTokens: 0.029,
985
984
  },
986
985
  },
986
+ {
987
+ id: "accounts/fireworks/models/glm-5p2",
988
+ displayName: "GLM 5.2",
989
+ // Fireworks serves GLM 5.2 with a 1,040K input window.
990
+ contextWindowTokens: 1040000,
991
+ maxOutputTokens: 131072,
992
+ supportsThinking: true,
993
+ supportsCaching: true,
994
+ supportsVision: false,
995
+ supportsToolUse: true,
996
+ maxEffort: "max",
997
+ pricing: {
998
+ inputPer1mTokens: 1.4,
999
+ outputPer1mTokens: 4.4,
1000
+ cacheReadPer1mTokens: 0.26,
1001
+ },
1002
+ },
987
1003
  // Kimi K2.5 (accounts/fireworks/models/kimi-k2p5) is intentionally
988
1004
  // absent: Fireworks serves it on-demand/dedicated only, so serverless
989
1005
  // chat/completions calls 404 ("not found, inaccessible, and/or not
@@ -1006,28 +1022,25 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
1006
1022
  cacheReadPer1mTokens: 0.06,
1007
1023
  },
1008
1024
  },
1025
+ // MiniMax M2.7 (accounts/fireworks/models/minimax-m2p7) is
1026
+ // intentionally absent: Fireworks has no serverless deployment for
1027
+ // it (the model page claims serverless support, but the serving API
1028
+ // returns 404).
1009
1029
  {
1010
- id: "accounts/fireworks/models/minimax-m2p7",
1011
- displayName: "MiniMax M2.7",
1012
- contextWindowTokens: 196608,
1013
- maxOutputTokens: 25000,
1014
- supportsThinking: false,
1015
- supportsCaching: false,
1016
- supportsVision: false,
1017
- supportsToolUse: true,
1018
- pricing: { inputPer1mTokens: 0.3, outputPer1mTokens: 1.2 },
1019
- },
1020
- {
1021
- id: "accounts/fireworks/models/deepseek-v4-pro",
1030
+ id: "accounts/fireworks/models/deepseek-v4-pro-0813",
1022
1031
  displayName: "DeepSeek V4 Pro",
1023
1032
  contextWindowTokens: 1040000,
1024
1033
  maxOutputTokens: 131072,
1025
1034
  supportsThinking: true,
1026
- supportsCaching: false,
1035
+ supportsCaching: true,
1027
1036
  supportsVision: false,
1028
1037
  supportsToolUse: true,
1029
1038
  maxEffort: "max",
1030
- pricing: { inputPer1mTokens: 1.74, outputPer1mTokens: 3.48 },
1039
+ pricing: {
1040
+ inputPer1mTokens: 1.32,
1041
+ outputPer1mTokens: 3.96,
1042
+ cacheReadPer1mTokens: 0.044,
1043
+ },
1031
1044
  },
1032
1045
  {
1033
1046
  id: "accounts/fireworks/models/deepseek-v4-flash-0731",
@@ -1834,7 +1847,7 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
1834
1847
  // Z.ai
1835
1848
  {
1836
1849
  id: "z-ai/glm-5.3",
1837
- displayName: "GLM-5.3",
1850
+ displayName: "GLM 5.3",
1838
1851
  contextWindowTokens: 1048576,
1839
1852
  maxOutputTokens: 131072,
1840
1853
  supportsThinking: true,
@@ -1849,7 +1862,7 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
1849
1862
  },
1850
1863
  {
1851
1864
  id: "z-ai/glm-5.3-flash",
1852
- displayName: "GLM-5.3 Flash",
1865
+ displayName: "GLM 5.3 Flash",
1853
1866
  contextWindowTokens: 1310720,
1854
1867
  maxOutputTokens: 131072,
1855
1868
  supportsThinking: true,
@@ -1864,7 +1877,7 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
1864
1877
  },
1865
1878
  {
1866
1879
  id: "z-ai/glm-5.2",
1867
- displayName: "GLM-5.2",
1880
+ displayName: "GLM 5.2",
1868
1881
  contextWindowTokens: 1048576,
1869
1882
  maxOutputTokens: 131072,
1870
1883
  supportsThinking: true,
@@ -7,7 +7,8 @@ import { getLogger } from "../../util/logger.js";
7
7
  import { isChatTemplateFailureError } from "../../util/provider-error-patterns.js";
8
8
  import { extractRetryAfterMs } from "../../util/retry.js";
9
9
  import { partialTagSuffix as sharedPartialTagSuffix } from "../../util/think-tag-stream.js";
10
- import { escapeXmlAttr } from "../../util/xml.js";
10
+ import { clampProviderString } from "../content-block-size.js";
11
+ import { fileBlockToProviderText } from "../file-block-text.js";
11
12
  import {
12
13
  base64Source,
13
14
  mediaSourceByteLength,
@@ -691,9 +692,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
691
692
  private requestHeaders: Record<string, string>;
692
693
  private parseThinkTags: boolean;
693
694
  private assistantReasoningField:
694
- | "reasoning"
695
- | "reasoning_content"
696
- | undefined;
695
+ "reasoning" | "reasoning_content" | undefined;
697
696
  private coerceObjectArgsToJsonString: boolean;
698
697
  private omitToolChoiceWhenReasoning: boolean;
699
698
 
@@ -741,15 +740,12 @@ export class OpenAIChatCompletionsProvider implements Provider {
741
740
  const modelOverride = configObj?.model as string | undefined;
742
741
  const effort = configObj?.effort as string | undefined;
743
742
  const logitBias = configObj?.logit_bias as
744
- | Record<string, number>
745
- | undefined;
743
+ Record<string, number> | undefined;
746
744
  const topP = configObj?.top_p as number | undefined;
747
745
  const usageAttributionHeaders = configObj?.usageAttributionHeaders as
748
- | Record<string, string>
749
- | undefined;
746
+ Record<string, string> | undefined;
750
747
  const perRequestHeaders = configObj?.requestHeaders as
751
- | Record<string, string>
752
- | undefined;
748
+ Record<string, string> | undefined;
753
749
 
754
750
  // Per-tool keys whose object schemas were rewritten to JSON strings for the
755
751
  // wire, to be decoded back on the response. Empty unless
@@ -1580,14 +1576,14 @@ export class OpenAIChatCompletionsProvider implements Provider {
1580
1576
  ): OpenAI.Chat.Completions.ChatCompletionUserMessageParam {
1581
1577
  // If only a single text block, use plain string (simpler, fewer tokens)
1582
1578
  if (blocks.length === 1 && blocks[0].type === "text") {
1583
- return { role: "user", content: blocks[0].text };
1579
+ return { role: "user", content: clampProviderString(blocks[0].text) };
1584
1580
  }
1585
1581
 
1586
1582
  const parts: OpenAI.Chat.Completions.ChatCompletionContentPart[] = [];
1587
1583
  for (const block of blocks) {
1588
1584
  switch (block.type) {
1589
1585
  case "text":
1590
- parts.push({ type: "text", text: block.text });
1586
+ parts.push({ type: "text", text: clampProviderString(block.text) });
1591
1587
  break;
1592
1588
  case "image":
1593
1589
  if (!OPENAI_SUPPORTED_IMAGE_TYPES.has(block.source.media_type)) {
@@ -1622,7 +1618,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
1622
1618
  } else {
1623
1619
  parts.push({
1624
1620
  type: "text",
1625
- text: this.fileBlockToText(block),
1621
+ text: fileBlockToProviderText(block),
1626
1622
  });
1627
1623
  }
1628
1624
  break;
@@ -1641,16 +1637,4 @@ export class OpenAIChatCompletionsProvider implements Provider {
1641
1637
 
1642
1638
  return { role: "user", content: parts };
1643
1639
  }
1644
-
1645
- private fileBlockToText(
1646
- block: Extract<ContentBlock, { type: "file" }>,
1647
- ): string {
1648
- const header = `<attached_file name="${escapeXmlAttr(
1649
- block.source.filename ?? "",
1650
- )}" type="${escapeXmlAttr(block.source.media_type)}" />`;
1651
- if (block.extracted_text && block.extracted_text.trim().length > 0) {
1652
- return `${header}\n${block.extracted_text}`;
1653
- }
1654
- return `${header}\nNo extracted text available.`;
1655
- }
1656
1640
  }
@@ -5,7 +5,8 @@ import { isAbortReason } from "../../util/abort-reasons.js";
5
5
  import { ProviderError, type ProviderErrorReason } from "../../util/errors.js";
6
6
  import { getLogger } from "../../util/logger.js";
7
7
  import { extractRetryAfterMs } from "../../util/retry.js";
8
- import { escapeXmlAttr } from "../../util/xml.js";
8
+ import { clampProviderString } from "../content-block-size.js";
9
+ import { fileBlockToProviderText } from "../file-block-text.js";
9
10
  import { base64Source, resolveMediaReferences } from "../media-resolve.js";
10
11
  import { PROMPT_CACHE_BREAKPOINT_MODEL_IDS } from "../model-catalog.js";
11
12
  import { recordProviderRequestDiagnostics } from "../request-diagnostics.js";
@@ -229,8 +230,7 @@ export class OpenAIResponsesProvider implements Provider {
229
230
  const effort = configObj?.effort as string | undefined;
230
231
  const verbosity = configObj?.verbosity as string | undefined;
231
232
  const usageAttributionHeaders = configObj?.usageAttributionHeaders as
232
- | Record<string, string>
233
- | undefined;
233
+ Record<string, string> | undefined;
234
234
  const disableCache = configObj?.disableCache === true;
235
235
  const disableTurnStartCache = configObj?.disableTurnStartCache === true;
236
236
  const promptCacheKey =
@@ -1003,7 +1003,9 @@ export class OpenAIResponsesProvider implements Provider {
1003
1003
  return {
1004
1004
  type: "message",
1005
1005
  role: "user",
1006
- content: [{ type: "input_text", text: blocks[0].text }],
1006
+ content: [
1007
+ { type: "input_text", text: clampProviderString(blocks[0].text) },
1008
+ ],
1007
1009
  };
1008
1010
  }
1009
1011
 
@@ -1011,7 +1013,10 @@ export class OpenAIResponsesProvider implements Provider {
1011
1013
  for (const block of blocks) {
1012
1014
  switch (block.type) {
1013
1015
  case "text":
1014
- parts.push({ type: "input_text", text: block.text });
1016
+ parts.push({
1017
+ type: "input_text",
1018
+ text: clampProviderString(block.text),
1019
+ });
1015
1020
  break;
1016
1021
  case "image":
1017
1022
  if (!OPENAI_SUPPORTED_IMAGE_TYPES.has(block.source.media_type)) {
@@ -1030,7 +1035,7 @@ export class OpenAIResponsesProvider implements Provider {
1030
1035
  case "file":
1031
1036
  parts.push({
1032
1037
  type: "input_text",
1033
- text: this.fileBlockToText(block),
1038
+ text: fileBlockToProviderText(block),
1034
1039
  });
1035
1040
  break;
1036
1041
  case "server_tool_use":
@@ -1051,16 +1056,4 @@ export class OpenAIResponsesProvider implements Provider {
1051
1056
  content: parts,
1052
1057
  };
1053
1058
  }
1054
-
1055
- private fileBlockToText(
1056
- block: Extract<ContentBlock, { type: "file" }>,
1057
- ): string {
1058
- const header = `<attached_file name="${escapeXmlAttr(
1059
- block.source.filename ?? "",
1060
- )}" type="${escapeXmlAttr(block.source.media_type)}" />`;
1061
- if (block.extracted_text && block.extracted_text.trim().length > 0) {
1062
- return `${header}\n${block.extracted_text}`;
1063
- }
1064
- return `${header}\nNo extracted text available.`;
1065
- }
1066
1059
  }
@@ -313,16 +313,19 @@ export async function resolveProviderFromConnection(
313
313
  // For every other connection this is `undefined` and the effective provider
314
314
  // is the connection's own — no behavior change.
315
315
  const effectiveProvider = opts.providerOverride ?? connection.provider;
316
- // Routing identities must be translated to a real upstream before this
317
- // point (resolveRoutingIdentity in connection-resolution) — an identity
318
- // reaching adapter construction would silently yield no adapter and the
319
- // retry wire-normalization keys off real adapter provider names.
320
- if (ROUTING_IDENTITY_PROVIDERS.has(effectiveProvider)) {
316
+ const model = opts.model ?? resolveModel(config, effectiveProvider);
317
+ // Routing identities must already be translated to a factory id
318
+ // (`resolveRoutingIdentity`). `chatgpt` has no factory and must not
319
+ // reach adapter construction. `vellum` is a catalog factory, so it is
320
+ // a valid effectiveProvider after that translation.
321
+ if (
322
+ ROUTING_IDENTITY_PROVIDERS.has(effectiveProvider) &&
323
+ !PROVIDER_CATALOG.some((entry) => entry.id === effectiveProvider)
324
+ ) {
321
325
  throw new Error(
322
- `resolveProviderFromConnection received unresolved routing identity "${effectiveProvider}" — translate to a real upstream before adapter construction`,
326
+ `resolveProviderFromConnection received unresolved routing identity "${effectiveProvider}": translate to a real upstream before adapter construction`,
323
327
  );
324
328
  }
325
- const model = opts.model ?? resolveModel(config, effectiveProvider);
326
329
  const cacheKey = getConnectionProviderCacheKey(
327
330
  connection,
328
331
  model,
@@ -39,7 +39,8 @@ export class ConnectionResolutionError extends ConfigError {
39
39
  | "model_incompatible"
40
40
  | "missing_credential"
41
41
  | "platform_unauthenticated"
42
- | "unroutable_managed_model",
42
+ | "unroutable_managed_model"
43
+ | "adapter_unavailable",
43
44
  message: string,
44
45
  options?: { cause?: unknown; model?: string; profileName?: string },
45
46
  ) {
@@ -429,10 +429,7 @@ export interface SendMessageConfig {
429
429
  /**
430
430
  * When true, the TURN-STARTING user message carries content that will not
431
431
  * recur byte-identically on the next turn, so a long-TTL breakpoint placed
432
- * on it could never be read back across turns. The agent loop is the only
433
- * producer: it sets the flag from the history it is about to send, when the
434
- * turn-starting message carries a memory-v3 `<memory_spotlight>` block (the
435
- * one injected block strip-and-replaced from every user message each turn).
432
+ * on it could never be read back across turns.
436
433
  *
437
434
  * The flag describes the turn, not the request, so it holds for every
438
435
  * request the turn makes, including tool-loop iterations, whose trailing
@@ -37,8 +37,9 @@ export const MANAGED_ROUTABLE_PROVIDERS: ReadonlySet<string> = new Set(
37
37
  * connection. Unlike the per-provider `*-managed` connections, this one does
38
38
  * not name an upstream provider on its DB row — the upstream is determined
39
39
  * per-request from the resolving profile. The same id is the catalog owner
40
- * of Vellum-hosted GPU models. Dispatch still substitutes a concrete
41
- * upstream (including `vellum` for those GPU models) before adapter lookup.
40
+ * of Vellum-hosted GPU models. Dispatch translates the identity to a
41
+ * factory id (including `vellum` when that is the catalog owner) before
42
+ * adapter lookup.
42
43
  */
43
44
  export const VELLUM_MANAGED_PROVIDER = "vellum";
44
45
 
@@ -20,8 +20,18 @@ Channel inbound turns (Slack/Telegram/etc. — `processChannelMessageInBackgroun
20
20
 
21
21
  **Invariant:** do NOT "fix" this by routing channel turns through `conversation.enqueueMessage` — the drain has no channel-callback delivery, so the reply would run but never reach the channel. A channel turn that still hits `CONVERSATION_BUSY_MESSAGE` (a non-channel turn raced in after admission) is re-scheduled for the channel-retry sweep via `deferRetryUntilIdle` — never `recordProcessingFailure` (which `classifyError` treats as fatal → dead-letter → silent drop, JARVIS-1346). The sweep is itself busy-aware: it `deferRetryUntilIdle`s a retry whose conversation is mid-turn. Busy deferral must **not** burn the retry budget: `deferRetryUntilIdle` pushes `retryAfter` forward but never increments `processingAttempts` and never dead-letters, so a conversation that stays busy across many sweeps re-defers indefinitely rather than dropping the reply at `RETRY_MAX_ATTEMPTS` (~10 min). Crash durability: while the in-memory admission waits, the inbound row sits `pending`; a crash mid-wait is recovered at startup by `recoverOrphanedChannelEvents` (`monitoring/recovery/`), which promotes boot-fenced orphan `pending` rows onto the sweep's `failed` retry path.
22
22
 
23
+ **A growing reply is channel-generic, and what it leaves behind is not.** `channel-reply-session.ts` owns the shared half: consuming the turn's event stream once, accumulating text across LLM calls, coalescing, tracking the plan from `ui_show`/`ui_update`, and deciding open/append/stop. Everything platform-shaped belongs to the transport. A channel declares it can carry a growing reply by implementing `streamReply` at all; whether THIS conversation can is answered by the transport's own reply to `start` (Slack refuses a turn with no thread, Telegram a chat that is not private), which arrives as an ordinary not-ok and falls back to sending the finished reply. A channel that caps how much one operation may carry declares that as `maxStreamTextChars`; the session does the splitting, because it is what tracks how much the channel has actually taken and that mark may only advance once per confirmed operation. A transport that split a wide delta into several calls of its own would leave the session unable to tell a partial delivery from a whole one, and a failure part-way through would re-send the part that already landed.
24
+
25
+ The load-bearing distinction is `streamPersists`. Slack finalizes its streamed message in place, so the stream IS the reply and durable delivery must not send it again. Telegram's draft is a 30-second preview that evaporates, so the reply is still owed and goes out the ordinary path. Two things follow, and both are easy to get wrong: a preview reports `fallback` at finish even though it really streamed, and a preview's stream id is never handed to `onStreamOpen`, because the crash-recovery breadcrumb exists to reconcile against a message the reader can still see. Recording a preview's id would point recovery at a message that never existed and lose the reply instead of posting it. Omitting `streamPersists` is the safe default: at worst the reply repeats, never disappears.
26
+
27
+ **Delivery has its own crash window, and its own recovery.** A turn is marked `processing_status = 'processed'` as soon as it persists, while its reply delivery finalizes afterwards in memory, so a crash in between strands the row `processed` + `delivery_status = 'pending'`. Neither existing path recovers that: the sweep selects `delivery_status = 'failed'`, and the orphan step above selects `processing_status = 'pending'`. `recoverStrandedDeliveryEvents` (`monitoring/recovery/`) closes it, promoting boot-fenced rows whose stored payload names both a `replyCallbackUrl` and a `replyMessageId` onto the delivery-retry arm. That payload requirement is load-bearing, not a cheap filter: it keeps the step off the intercept-settled rows (reactions, edits, admission denials) which legitimately end `processed` + `pending` with no reply owed. The corollary for new code: any path that finishes a channel turn WITHOUT delivering must settle its own `deliveryStatus` rather than leaving it `pending`, or this step will re-post a reply that was never owed. The deduplicated-ingress skip in `background-dispatch.ts` calls `markDeliveryDelivered` for exactly that reason.
28
+
23
29
  **Faithful replay:** the sweep reconstructs a turn from the stored raw payload, so it must produce the SAME turn the live ingress path would have run — not an impoverished one. Two invariants, each learned from a live-path hardening change that originally skipped the sweep: (1) **content fencing** — non-guardian content is wrapped in `<external_content>` via the shared `prepareChannelInboundContent` (`routes/inbound-stages/inbound-content-prep.ts`), used by BOTH `inbound-message-handler.ts` and the sweep; replaying raw, unwrapped text would drop the untrusted-content boundary the model relies on (regression window: #30785 wrapped only the live path). (2) **idempotency key + slackMeta** — the live turn captures its `slackInbound` onto the stored payload (`storeInboundSlackMetadata`) and the sweep replays that EXACT object (`parseStoredSlackInbound`), so `deriveIngressIdempotencyKey` yields a byte-identical `client_message_id` (a replay of an already-persisted turn dedups the agent loop) and full slackMeta survives the replay. Dedup is not enough on its own: on a dedup hit the sweep gates `finalizeEventDelivery` on `isDeduplicatedDeliveryOwnedBySibling` — skipping delivery when a sibling event already owns it, so it never double-posts — and, when the deduped turn crashed before writing any reply, completes it with a fresh run rather than delivering nothing. A `buildReplaySlackInbound` fallback reconstructs the key-bearing fields for payloads stored before the capture existed (regression window: #38378 added the key to the live path only, though the sweep IS the "Slack retry" path it targeted). Any future hardening applied at channel ingress must be mirrored in the sweep, or a retried/recovered turn silently loses it.
24
30
 
31
+ **Reaction wake turns are the deliberate exception to the sweep's processing lane.** A reaction an admitted actor ADDS to the assistant's own post dispatches a discretion turn through `processChannelMessageInBackground` (`inbound-stages/reaction-intercept.ts`, `buildReactionWakeTurn`): the turn's persisted user row carries the reaction envelope through the ordinary ingress carriers (`channelInbound` for every channel whose lane accepts it, the transitional `slackReactionRowMeta` for Slack), so the row reads as a reaction everywhere, and its content is the same line the reload renderer produces. The reply's destination is resolved from the target row's own thread rather than the reaction event's callback, which names the reacted message as its thread.
32
+
33
+ These turns are at-most-once BY DESIGN: the sweep's processing lane rebuilds a turn from its stored payload as a plain message, which would corrupt a reaction into a fabricated user message. So the event is marked processed up front, and the payload it stores is delivery-only (callback, chat, assistant id): enough for the delivery lane to re-post a reply whose first delivery failed, and carrying no content or channel, so no processing replay can fabricate a turn from it. A post-admission busy loss degrades through the dispatch's `onTurnLostToBusy` hook to the passive transcript row instead of the sweep. Do not "fix" a lost reaction turn by widening that payload into a replayable one - teach the sweep to rebuild reaction turns first.
34
+
25
35
  ### SSE backpressure shedding must be observable
26
36
 
27
37
  SSE handlers built on `ReadableStream` shed slow subscribers when `controller.desiredSize <= 0` to keep daemon memory bounded. Every shed site must emit a log line + Sentry capture so the daemon-side shed can be time-correlated with the client-side idle watchdog (otherwise stalls are invisible from both sides). See [WHATWG Streams — Backpressure](https://streams.spec.whatwg.org/#pipe-chains) and [Node `monitorEventLoopDelay`](https://nodejs.org/api/perf_hooks.html#perf_hooksmonitoreventloopdelayoptions).
@@ -160,7 +170,7 @@ All CDP-backed browser tools (`browser_navigate`, `browser_snapshot`, `browser_s
160
170
 
161
171
  ### Interactive requests on channels (approvals, questions)
162
172
 
163
- **The guardian-request pipeline is the canonical rail for anything interactive on a channel** — cards with buttons, request-code replies, emoji reactions, typed answers. The end-to-end map (promotion → gateway `guardian_requests` row → notification broadcaster → per-channel adapters → reply router → decision primitive → per-kind resolver) lives in [docs/guardian-request-flow.md](../../docs/guardian-request-flow.md). New interactive features extend that pipeline's seams; do NOT add per-feature watchers, callback schemes, or inbound intercepts.
173
+ **The guardian-request pipeline is the canonical rail for anything interactive on a channel**: cards with buttons, request-code replies, typed answers. The end-to-end map (promotion → gateway `guardian_requests` row → notification broadcaster → per-channel adapters → reply router → decision primitive → per-kind resolver) lives in [docs/guardian-request-flow.md](../../docs/guardian-request-flow.md). New interactive features extend that pipeline's seams; do NOT add per-feature watchers, callback schemes, or inbound intercepts.
164
174
 
165
175
  Identifiers and plumbing notes:
166
176
 
@@ -204,22 +214,37 @@ the gateway's Channel Identity Vocabulary, which covers the wire side.
204
214
  channel, including one this repo has no code for, which is why a new
205
215
  channel belongs here rather than in a sixth key of its own.
206
216
 
207
- Every channel except Slack writes it: a reaction row carries the whole
208
- shape (`inbound-stages/reaction-intercept.ts`), an edit or a delete
209
- stamps `editedAt` / `deletedAt` onto whatever the row already said about
210
- itself through `mergeProviderMessageMetadata`
211
- (`inbound-stages/edit-intercept.ts`, `inbound-message-handler.ts`), and an
212
- outbound assistant reply is stamped with a partial envelope at reserve time
213
- (`buildAssistantChannelMetadata`) whose `messageId` (and, for a reply
217
+ Every row the daemon authors writes it on every channel, Slack included:
218
+ an outbound assistant reply is stamped with a partial envelope at reserve
219
+ time (`buildAssistantChannelMetadata`) whose `messageId` (and, for a reply
214
220
  split into several posts, `additionalMessageIds`) the post-send
215
221
  reconciliation in `channel-reply-delivery.ts` back-fills from the
216
- transport's delivery results, which is what lets a later reaction on the
217
- assistant's own post resolve back to its row. Slack
218
- keeps writing `slackMeta`, and `readProviderMetadata` maps that envelope
219
- onto this shape on read, so the channel-agnostic readers in
220
- `persistence/delivery-crud.ts` (thread evidence, and finding the
221
- conversation that holds a given provider message id) serve both without a
222
- per-channel branch.
222
+ transport's delivery results, the assistant's own reaction rows write it
223
+ (`daemon/reaction-record.ts`), and bot-authored Slack backfill rows write
224
+ it. Every channel except Slack writes it for inbound rows too: a reaction
225
+ row carries the whole shape (`inbound-stages/reaction-intercept.ts`), and
226
+ an edit or a delete stamps `editedAt` / `deletedAt` onto whatever the row
227
+ already said about itself through `mergeProviderMessageMetadata`
228
+ (`inbound-stages/edit-intercept.ts`, `inbound-message-handler.ts`). The same reconciliation writes each id into
229
+ the `channel_outbound_posts` index (the outbound counterpart of
230
+ `channel_inbound_events`' provider-id resolution), which is what lets a
231
+ later reaction or delete naming the assistant's own post resolve back to
232
+ its row exactly; the envelope stays the row's self-description, and the
233
+ capped envelope scan in `findMessageByProviderMessageId` survives only as
234
+ the transitional fallback for rows reconciled before the table. A reply
235
+ still pending for the retry sweep that was reserved with Slack's own
236
+ pre-send envelope converges onto this envelope when that reconciliation
237
+ stamps it (`providerMetadataOfPreSendSlackEnvelope`, transitional in the
238
+ same way). Inbound Slack rows still write `slackMeta` (transitional: the
239
+ end state is this
240
+ envelope on every Slack row, with `slackMeta` as the read-compat arm for
241
+ historical rows). The two envelopes map onto each other on read, in both
242
+ directions: `readProviderMetadata` serves a `slackMeta` row as this shape
243
+ to the channel-agnostic readers in `persistence/delivery-crud.ts`, and
244
+ `readSlackMetadataFromMessageMetadata` serves a neutral row as Slack's
245
+ view (`slackViewOfProviderMetadata`, Slack's own fields riding the
246
+ schema's passthrough) to the Slack renderers and backfill readers, so no
247
+ reader on either side carries a per-envelope branch.
223
248
 
224
249
  ### Channel verification: gateway-owned
225
250
 
@@ -40,11 +40,6 @@ export function composeApprovalMessage(
40
40
  return getFallbackMessage(context);
41
41
  }
42
42
 
43
- /** @internal Exported for use by the daemon-injected generator implementation. */
44
- export function escapeRegExp(input: string): string {
45
- return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
46
- }
47
-
48
43
  /** @internal Exported for use by the daemon-injected generator implementation. */
49
44
  export function includesRequiredKeywords(
50
45
  text: string,
@@ -54,7 +49,7 @@ export function includesRequiredKeywords(
54
49
  return true;
55
50
  }
56
51
  return requiredKeywords.every((keyword) => {
57
- const re = new RegExp(`\\b${escapeRegExp(keyword)}\\b`, "i");
52
+ const re = new RegExp(`\\b${RegExp.escape(keyword)}\\b`, "i");
58
53
  return re.test(text);
59
54
  });
60
55
  }
@@ -338,9 +338,7 @@ export class AssistantEventHub {
338
338
  * it receive the event; untargeted events go to all
339
339
  * - if `targetInterfaceId` is set, only client subscribers whose
340
340
  * `interfaceId` matches receive the event; process subscribers and
341
- * non-matching clients are skipped. Used to narrow legacy
342
- * broadcasts (e.g. `conversation_list_invalidated`) to a specific
343
- * client surface during a migration window.
341
+ * non-matching clients are skipped.
344
342
  *
345
343
  * Fanout is isolated: a throwing or rejecting subscriber does not abort
346
344
  * delivery to remaining subscribers.
@@ -744,13 +742,7 @@ export function broadcastMessage(
744
742
  const targetClientId = options?.targetClientId;
745
743
  const targetInterfaceId = options?.targetInterfaceId;
746
744
 
747
- // `conversation_list_invalidated` is a list-level system event — publish
748
- // it unscoped so every subscriber refreshes its sidebar.
749
- const scopedConversationId =
750
- msg.type === "conversation_list_invalidated"
751
- ? undefined
752
- : resolvedConversationId;
753
- const event = buildAssistantEvent(msg, scopedConversationId);
745
+ const event = buildAssistantEvent(msg, resolvedConversationId);
754
746
  const targetCapability = capabilityForMessageType(msg.type);
755
747
  // Self-echo suppression: a `sync_changed` carrying an `originClientId`
756
748
  // means a specific client just mutated the resource. The hub must not
@@ -780,35 +772,6 @@ export function broadcastMessage(
780
772
  stampAndBuffer(event, { targeting: publishOptions });
781
773
  _hubChain = _hubChain
782
774
  .then(() => assistantEventHub.publish(event, publishOptions))
783
- .then(() => {
784
- // When a conversation title changes, also publish a
785
- // `conversation_list_invalidated` so the macOS sidebar refreshes
786
- // its row ordering for the renamed conversation. Web consumes the
787
- // paired `sync_changed` with `conversation:<id>:metadata` tag
788
- // emitted by `publishConversationTitleChanged` and patches the
789
- // single row in place, so the broadcast is scoped to macOS only.
790
- //
791
- // TODO(electron-cutover): remove this emission once macOS migrates
792
- // to the Electron client and consumes `sync_changed` directly. At
793
- // that point `conversation_list_invalidated` has no remaining
794
- // consumers and the message type can be retired.
795
- if (msg.type === "conversation_title_updated") {
796
- return assistantEventHub
797
- .publish(
798
- buildAssistantEvent({
799
- type: "conversation_list_invalidated",
800
- reason: "renamed",
801
- }),
802
- { targetInterfaceId: "macos" },
803
- )
804
- .catch((err: unknown) => {
805
- log.warn(
806
- { err },
807
- "Failed to publish conversation_list_invalidated after title update",
808
- );
809
- });
810
- }
811
- })
812
775
  .catch((err: unknown) => {
813
776
  log.warn({ err }, "assistant-events hub subscriber threw during publish");
814
777
  });
@@ -166,11 +166,7 @@ export interface ChannelApprovalPrompt {
166
166
  * joins (every channel's buttons ride the same `apr:` callback), and no
167
167
  * consumer reads a channel off this field.
168
168
  */
169
- export type ApprovalDecisionSource =
170
- | "button"
171
- | "reaction"
172
- | "vellum_surface"
173
- | "plain_text";
169
+ export type ApprovalDecisionSource = "button" | "vellum_surface" | "plain_text";
174
170
 
175
171
  /** The structured result of a user's approval decision. */
176
172
  export interface ApprovalDecisionResult {