@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
@@ -236,6 +236,55 @@ function seedFailedSlackEvent(opts: {
236
236
  return inbound.eventId;
237
237
  }
238
238
 
239
+ function seedFailedTelegramEvent(opts: {
240
+ content: string;
241
+ externalChatId: string;
242
+ messageId: string;
243
+ threadId?: string;
244
+ /** Sender display name as `storePayload` records it at ingress. */
245
+ senderName?: string;
246
+ /** Stands in for the `channelInbound` the live path captures onto the payload. */
247
+ channelInbound?: Record<string, unknown>;
248
+ }): string {
249
+ const inbound = deliveryCrud.recordInbound(
250
+ "telegram",
251
+ opts.externalChatId,
252
+ opts.messageId,
253
+ );
254
+ deliveryCrud.storePayload(inbound.eventId, {
255
+ content: opts.content,
256
+ sourceChannel: "telegram",
257
+ interface: "telegram",
258
+ externalChatId: opts.externalChatId,
259
+ externalMessageId: opts.messageId,
260
+ sourceMetadata: {
261
+ messageId: opts.messageId,
262
+ ...(opts.threadId ? { threadId: opts.threadId } : {}),
263
+ },
264
+ ...(opts.senderName ? { senderName: opts.senderName } : {}),
265
+ ...(opts.channelInbound ? { channelInbound: opts.channelInbound } : {}),
266
+ trustCtx: {
267
+ trustClass: "guardian",
268
+ sourceChannel: "telegram",
269
+ requesterExternalUserId: "user-1",
270
+ requesterChatId: opts.externalChatId,
271
+ guardianPrincipalId: "principal-1",
272
+ },
273
+ });
274
+
275
+ getDb()
276
+ .update(channelInboundEvents)
277
+ .set({
278
+ processingStatus: "failed",
279
+ processingAttempts: 1,
280
+ retryAfter: Date.now() - 1,
281
+ })
282
+ .where(eq(channelInboundEvents.id, inbound.eventId))
283
+ .run();
284
+
285
+ return inbound.eventId;
286
+ }
287
+
239
288
  describe("channel-retry-sweep", () => {
240
289
  beforeEach(() => {
241
290
  resetTables();
@@ -352,6 +401,37 @@ describe("channel-retry-sweep", () => {
352
401
  expect(eventRow?.processingStatus).toBe("processed");
353
402
  });
354
403
 
404
+ test("restores the triggering message's id and thread on replay", async () => {
405
+ seedFailedEventWithTrustClass("guardian", {
406
+ sourceMessageId: "1700000000.000200",
407
+ sourceThreadId: "1700000000.000100",
408
+ });
409
+
410
+ let capturedTrust: Record<string, unknown> | undefined;
411
+ await sweepFailedEvents(async (conversationId, _content, options) => {
412
+ capturedTrust = (options as { trustContext?: Record<string, unknown> })
413
+ .trustContext;
414
+ const messageId = "message-location";
415
+ getDb()
416
+ .insert(messages)
417
+ .values({
418
+ id: messageId,
419
+ conversationId,
420
+ role: "user",
421
+ content: JSON.stringify([{ type: "text", text: "retry me" }]),
422
+ createdAt: Date.now(),
423
+ })
424
+ .run();
425
+ return { messageId };
426
+ });
427
+
428
+ // The replayed turn names the same message and thread the live turn
429
+ // did, so the turn's chat and thread lines and approval-card source
430
+ // links read the same on a retry as on the first attempt.
431
+ expect(capturedTrust?.sourceMessageId).toBe("1700000000.000200");
432
+ expect(capturedTrust?.sourceThreadId).toBe("1700000000.000100");
433
+ });
434
+
355
435
  test("fences a non-guardian Slack replay in external_content and carries the ingress idempotency key", async () => {
356
436
  seedFailedSlackEvent({
357
437
  trustClass: "unverified_contact",
@@ -442,6 +522,125 @@ describe("channel-retry-sweep", () => {
442
522
  expect(capturedOptions?.slackInbound?.channelTs).toBe("1700000000.000200");
443
523
  });
444
524
 
525
+ test("replays a captured channelInbound envelope verbatim", async () => {
526
+ const captured = {
527
+ source: "telegram",
528
+ conversationExternalId: "chat-500",
529
+ messageId: "tg-msg-1",
530
+ threadId: "topic-3",
531
+ actorExternalId: "user-77",
532
+ displayName: "Alice",
533
+ eventKind: "message",
534
+ };
535
+ seedFailedTelegramEvent({
536
+ content: "retry me",
537
+ externalChatId: "chat-500",
538
+ messageId: "tg-msg-1",
539
+ channelInbound: captured,
540
+ });
541
+
542
+ let capturedOptions:
543
+ | { channelInbound?: Record<string, unknown> }
544
+ | undefined;
545
+ await sweepFailedEvents(async (conversationId, content, options) => {
546
+ capturedOptions = options as {
547
+ channelInbound?: Record<string, unknown>;
548
+ };
549
+ const messageId = "message-tg-captured";
550
+ getDb()
551
+ .insert(messages)
552
+ .values({
553
+ id: messageId,
554
+ conversationId,
555
+ role: "user",
556
+ content: JSON.stringify([{ type: "text", text: content }]),
557
+ createdAt: Date.now(),
558
+ })
559
+ .run();
560
+ return { messageId };
561
+ });
562
+
563
+ // The captured envelope is the EXACT object the live turn used, so the
564
+ // replayed row persists an identical providerMeta.
565
+ expect(capturedOptions?.channelInbound).toEqual(captured);
566
+ });
567
+
568
+ test("reconstructs channelInbound from the stored payload when no capture exists", async () => {
569
+ // The crash window: `storePayload` ran at ingress but the daemon died
570
+ // before `storeInboundChannelMetadata`. Recovery must rebuild the same
571
+ // envelope the live turn would have persisted, sender fields included.
572
+ seedFailedTelegramEvent({
573
+ content: "retry me",
574
+ externalChatId: "chat-501",
575
+ messageId: "tg-msg-2",
576
+ threadId: "topic-9",
577
+ senderName: "Alice",
578
+ });
579
+
580
+ let capturedOptions:
581
+ | { channelInbound?: Record<string, unknown> }
582
+ | undefined;
583
+ await sweepFailedEvents(async (conversationId, content, options) => {
584
+ capturedOptions = options as {
585
+ channelInbound?: Record<string, unknown>;
586
+ };
587
+ const messageId = "message-tg-fallback";
588
+ getDb()
589
+ .insert(messages)
590
+ .values({
591
+ id: messageId,
592
+ conversationId,
593
+ role: "user",
594
+ content: JSON.stringify([{ type: "text", text: content }]),
595
+ createdAt: Date.now(),
596
+ })
597
+ .run();
598
+ return { messageId };
599
+ });
600
+
601
+ expect(capturedOptions?.channelInbound).toEqual({
602
+ source: "telegram",
603
+ conversationExternalId: "chat-501",
604
+ messageId: "tg-msg-2",
605
+ threadId: "topic-9",
606
+ displayName: "Alice",
607
+ actorExternalId: "user-1",
608
+ eventKind: "message",
609
+ });
610
+ });
611
+
612
+ test("a Slack replay carries no channelInbound (Slack keeps slackMeta)", async () => {
613
+ seedFailedSlackEvent({
614
+ trustClass: "guardian",
615
+ content: "hello",
616
+ externalChatId: "C0000001",
617
+ channelTs: "1700000001.000100",
618
+ });
619
+
620
+ let capturedOptions:
621
+ | { channelInbound?: Record<string, unknown> }
622
+ | undefined;
623
+ await sweepFailedEvents(async (conversationId, content, options) => {
624
+ capturedOptions = options as {
625
+ channelInbound?: Record<string, unknown>;
626
+ };
627
+ const messageId = "message-slack-no-neutral";
628
+ getDb()
629
+ .insert(messages)
630
+ .values({
631
+ id: messageId,
632
+ conversationId,
633
+ role: "user",
634
+ content: JSON.stringify([{ type: "text", text: content }]),
635
+ createdAt: Date.now(),
636
+ })
637
+ .run();
638
+ return { messageId };
639
+ });
640
+
641
+ expect(capturedOptions?.channelInbound).toBeUndefined();
642
+ });
643
+
445
644
  test("replays the sender's Slack app context from the captured slackInbound", async () => {
446
645
  seedFailedSlackEvent({
447
646
  trustClass: "guardian",
@@ -84,6 +84,7 @@ function createWebTurnContext(
84
84
  size: () => 0,
85
85
  } as unknown as MessageQueue;
86
86
  let processing = false;
87
+ let owner = 0;
87
88
  return {
88
89
  conversationId: "conv-client-os-test",
89
90
  messages: [],
@@ -91,6 +92,21 @@ function createWebTurnContext(
91
92
  setProcessing: (value: boolean) => {
92
93
  processing = value;
93
94
  },
95
+ acquireProcessingFenced: async () => {
96
+ if (processing) {
97
+ return null;
98
+ }
99
+ processing = true;
100
+ owner += 1;
101
+ return owner;
102
+ },
103
+ releaseProcessing: (claim: number) => {
104
+ if (claim !== owner) {
105
+ return false;
106
+ }
107
+ processing = false;
108
+ return true;
109
+ },
94
110
  abortController: null,
95
111
  queue: queueStub,
96
112
  clientOs,
@@ -111,7 +127,7 @@ describe("client OS surface metadata persistence", () => {
111
127
  addMessageCalls.length = 0;
112
128
  });
113
129
 
114
- test.each(["macos", "windows", "ios", "android", "web"])(
130
+ test.each(["macos", "windows", "linux", "ios", "android", "web"])(
115
131
  "stamps client.os = %s from the conversation's clientOs",
116
132
  async (os) => {
117
133
  const ctx = createWebTurnContext(os);
@@ -137,7 +153,7 @@ describe("client OS surface metadata persistence", () => {
137
153
  });
138
154
 
139
155
  test("omits the client bag for values outside the ClientOs vocabulary", async () => {
140
- const ctx = createWebTurnContext("linux");
156
+ const ctx = createWebTurnContext("plan9");
141
157
  await persistQueuedMessageBody(ctx, {
142
158
  content: "hello",
143
159
  requestId: "req-invalid",
@@ -214,9 +230,22 @@ describe("client OS per-row evidence marker", () => {
214
230
  expect(isDesktopOriginatedUserMessage(lastUserMetadata())).toBe(true);
215
231
  });
216
232
 
233
+ test("recognizes a Linux app turn as desktop-originated", async () => {
234
+ const ctx = createWebTurnContext("linux");
235
+ await persistQueuedMessageBody(ctx, {
236
+ content: "hello",
237
+ requestId: "req-transport-os-linux",
238
+ requestClientOs: "linux",
239
+ });
240
+
241
+ expect(lastUserMetadata().client).toEqual({ os: "linux" });
242
+ expect(lastUserMetadata().clientOsFromRequest).toBe(true);
243
+ expect(isDesktopOriginatedUserMessage(lastUserMetadata())).toBe(true);
244
+ });
245
+
217
246
  // Mirrors the Mac-originated case above: a plain browser tab reports
218
247
  // `client.os: "web"`, while the Electron desktop renderer resolves its own
219
- // host OS first and persists `macos`/`windows` instead.
248
+ // host OS first and persists `macos`/`windows`/`linux` instead.
220
249
  test("marks an OS this row's own transport reported (web)", async () => {
221
250
  const ctx = createWebTurnContext("web");
222
251
  await persistQueuedMessageBody(ctx, {
@@ -1677,6 +1677,21 @@ describe("ConnectionResolutionError classification", () => {
1677
1677
  expect(result.userMessage).toContain("Restart the assistant");
1678
1678
  });
1679
1679
 
1680
+ it("classifies adapter_unavailable for a Vellum-hosted GPU model", () => {
1681
+ const err = new ConnectionResolutionError(
1682
+ "vellum",
1683
+ "adapter_unavailable",
1684
+ "no adapter",
1685
+ { model: "qwen/qwen3-8b", profileName: "steer" },
1686
+ );
1687
+ const result = classifyConversationError(err, errCtx);
1688
+ expect(result.code).toBe("PROVIDER_NOT_CONFIGURED");
1689
+ expect(result.userMessage).toContain("qwen/qwen3-8b");
1690
+ expect(result.userMessage).toContain("Vellum GPU route");
1691
+ expect(result.userMessage).toContain('profile "steer"');
1692
+ expect(result.userMessage).toContain("was not sent through another provider");
1693
+ });
1694
+
1680
1695
  it("classifies missing_credential naming the connection and fix", () => {
1681
1696
  const err = new ConnectionResolutionError(
1682
1697
  "anthropic-personal",
@@ -589,6 +589,53 @@ describe("loadFromDb history repair", () => {
589
589
  expect(allText).not.toContain("hunter2");
590
590
  });
591
591
 
592
+ test("the assistant's own deleted post keeps its content plus a visibility marker", async () => {
593
+ mockConversation = {
594
+ id: "conv-1",
595
+ contextSummary: null,
596
+ contextCompactedMessageCount: 0,
597
+ totalInputTokens: 0,
598
+ totalOutputTokens: 0,
599
+ totalEstimatedCost: 0,
600
+ };
601
+ mockDbMessages = [
602
+ {
603
+ id: "m1",
604
+ role: "user",
605
+ content: [{ type: "text", text: "What time is the meeting?" }],
606
+ },
607
+ {
608
+ id: "m2",
609
+ role: "assistant",
610
+ content: [{ type: "text", text: "The meeting is at 3pm." }],
611
+ metadata: JSON.stringify({
612
+ providerMeta: JSON.stringify({
613
+ source: "discord",
614
+ conversationExternalId: "chan-1",
615
+ messageId: "555.2",
616
+ eventKind: "message",
617
+ deletedAt: 1700000001000,
618
+ }),
619
+ }),
620
+ },
621
+ ];
622
+
623
+ const conversation = makeConversation();
624
+ await conversation.loadFromDb();
625
+ const assistantRow = conversation
626
+ .getMessages()
627
+ .find((m) => m.role === "assistant");
628
+ const texts = (assistantRow?.content ?? [])
629
+ .filter((b) => b.type === "text")
630
+ .map((b) => (b.type === "text" ? b.text : ""));
631
+ // Erasure is for a retracted user message; the assistant's own words
632
+ // stay (it did say them), and the rendered fact is lost visibility.
633
+ expect(texts.join("\n")).toContain("The meeting is at 3pm.");
634
+ expect(texts.join("\n")).toContain(
635
+ "[This message was deleted from the channel and is no longer visible to participants]",
636
+ );
637
+ });
638
+
592
639
  test("a reaction never quotes a target that was later deleted", async () => {
593
640
  mockConversation = {
594
641
  id: "conv-1",
@@ -703,6 +750,77 @@ describe("loadFromDb history repair", () => {
703
750
  expect(allText).not.toContain("an earlier message");
704
751
  });
705
752
 
753
+ test("a partially deleted split reply quotes only its surviving posts", async () => {
754
+ mockConversation = {
755
+ id: "conv-1",
756
+ contextSummary: null,
757
+ contextCompactedMessageCount: 0,
758
+ totalInputTokens: 0,
759
+ totalOutputTokens: 0,
760
+ totalEstimatedCost: 0,
761
+ };
762
+ const reactionRow = (target: string, emoji: string, id: string) => ({
763
+ id,
764
+ role: "user",
765
+ content: [{ type: "text", text: "[reaction]" }],
766
+ metadata: JSON.stringify({
767
+ providerMeta: JSON.stringify({
768
+ source: "discord",
769
+ conversationExternalId: "chan-1",
770
+ eventKind: "reaction",
771
+ reaction: {
772
+ targetMessageId: target,
773
+ emoji,
774
+ op: "added",
775
+ actorDisplayName: "Alice",
776
+ },
777
+ }),
778
+ }),
779
+ });
780
+ mockDbMessages = [
781
+ {
782
+ id: "m1",
783
+ role: "assistant",
784
+ content: [{ type: "text", text: "Deploy finished cleanly." }],
785
+ metadata: JSON.stringify({
786
+ providerMeta: JSON.stringify({
787
+ source: "discord",
788
+ conversationExternalId: "chan-1",
789
+ messageId: "555.1",
790
+ additionalMessageIds: ["555.2"],
791
+ deletedMessageIds: ["555.2"],
792
+ eventKind: "message",
793
+ }),
794
+ }),
795
+ },
796
+ // Reaction on the surviving post: still quotable.
797
+ reactionRow("555.1", "tada", "m2"),
798
+ // Reaction on the deleted post: degrades to the unresolved form
799
+ // rather than quoting content no longer on the channel.
800
+ reactionRow("555.2", "eyes", "m3"),
801
+ ];
802
+
803
+ const conversation = makeConversation();
804
+ await conversation.loadFromDb();
805
+ const allText = conversation
806
+ .getMessages()
807
+ .flatMap((m) => m.content)
808
+ .filter((b) => b.type === "text")
809
+ .map((b) => (b.type === "text" ? b.text : ""))
810
+ .join("\n");
811
+ // The row itself is NOT fully deleted: its content stays.
812
+ expect(allText).toContain("Deploy finished cleanly.");
813
+ // :tada: (surviving target) quotes; :eyes: (deleted target) does not.
814
+ const tadaLine = allText
815
+ .split("\n")
816
+ .find((line) => line.includes(":tada:"));
817
+ const eyesLine = allText
818
+ .split("\n")
819
+ .find((line) => line.includes(":eyes:"));
820
+ expect(tadaLine).toContain("Deploy finished cleanly.");
821
+ expect(eyesLine).toContain("an earlier message");
822
+ });
823
+
706
824
  test("the assistant's own reaction row renders second-person", async () => {
707
825
  mockConversation = {
708
826
  id: "conv-1",
@@ -19,9 +19,17 @@ let addMessageShouldThrow = false;
19
19
  /** Simulated existing conversations for getConversation mock. */
20
20
  let mockExistingConversations: Record<
21
21
  string,
22
- { id: string; source: string; title: string | null }
22
+ {
23
+ id: string;
24
+ source: string;
25
+ title: string | null;
26
+ conversationType?: string;
27
+ }
23
28
  > = {};
24
29
 
30
+ /** Mutable arm for the assistant-initiated-threads gate mock. */
31
+ let assistantInitiatedThreadsEnabled = false;
32
+
25
33
  const createConversationMock = mock((_opts?: unknown) => {
26
34
  if (createConversationShouldThrow) {
27
35
  throw new Error("DB write failed");
@@ -102,6 +110,12 @@ mock.module("../persistence/external-conversation-store.js", () => ({
102
110
  upsertOutboundBinding: upsertOutboundBindingMock,
103
111
  }));
104
112
 
113
+ /* The registry resolves through workspace override files; a mutable arm keeps
114
+ these tests off the filesystem, mirroring the section-split suite. */
115
+ mock.module("../config/assistant-initiated-threads-gate.js", () => ({
116
+ isAssistantInitiatedThreadsEnabled: () => assistantInitiatedThreadsEnabled,
117
+ }));
118
+
105
119
  import { pairDeliveryWithConversation } from "../notifications/conversation-pairing.js";
106
120
  import type { NotificationSignal } from "../notifications/signal.js";
107
121
  import type {
@@ -162,6 +176,7 @@ describe("pairDeliveryWithConversation", () => {
162
176
  addMessageShouldThrow = false;
163
177
  mockExistingConversations = {};
164
178
  mockBindings = {};
179
+ assistantInitiatedThreadsEnabled = false;
165
180
  });
166
181
 
167
182
  // ── start_new_conversation (vellum) ─────────────────────────────────
@@ -1356,4 +1371,129 @@ describe("pairDeliveryWithConversation", () => {
1356
1371
  expect(result.conversationId).toBe("conv-001");
1357
1372
  expect(result.messageId).toBe("msg-001");
1358
1373
  });
1374
+ // ── assistant.share → assistant-initiated thread (flag-gated) ────────
1375
+
1376
+ describe("assistant.share promotion under assistant-initiated-threads", () => {
1377
+ const share = (overrides?: Partial<NotificationSignal>) =>
1378
+ makeSignal({
1379
+ requiresConversation: undefined,
1380
+ sourceEventName: "assistant.share",
1381
+ sourceContextId: "conv-heartbeat",
1382
+ ...overrides,
1383
+ });
1384
+
1385
+ test("flag on: a share from a background run materializes a section thread", async () => {
1386
+ assistantInitiatedThreadsEnabled = true;
1387
+ mockExistingConversations["conv-heartbeat"] = {
1388
+ id: "conv-heartbeat",
1389
+ source: "heartbeat",
1390
+ title: null,
1391
+ conversationType: "background",
1392
+ };
1393
+
1394
+ const result = await pairDeliveryWithConversation(
1395
+ share(),
1396
+ "vellum" as NotificationChannel,
1397
+ makeCopy({ conversationTitle: "Noticed something" }),
1398
+ );
1399
+
1400
+ expect(result.createdNewConversation).toBe(true);
1401
+ const callArgs = createConversationMock.mock.calls[0]![0] as Record<
1402
+ string,
1403
+ unknown
1404
+ >;
1405
+ expect(callArgs.source).toBe("assistant_initiated");
1406
+ expect(callArgs.conversationType).toBe("standard");
1407
+ expect(callArgs.title).toBe("Noticed something");
1408
+ });
1409
+
1410
+ test("flag on: an unresolvable source context still materializes", async () => {
1411
+ assistantInitiatedThreadsEnabled = true;
1412
+
1413
+ const result = await pairDeliveryWithConversation(
1414
+ share({ sourceContextId: "cli-123" }),
1415
+ "vellum" as NotificationChannel,
1416
+ makeCopy(),
1417
+ );
1418
+
1419
+ expect(result.createdNewConversation).toBe(true);
1420
+ const callArgs = createConversationMock.mock.calls[0]![0] as Record<
1421
+ string,
1422
+ unknown
1423
+ >;
1424
+ expect(callArgs.source).toBe("assistant_initiated");
1425
+ });
1426
+
1427
+ test("flag on: a share from a user-facing thread keeps the passive append", async () => {
1428
+ assistantInitiatedThreadsEnabled = true;
1429
+ mockExistingConversations["conv-user"] = {
1430
+ id: "conv-user",
1431
+ source: "user",
1432
+ title: "Ongoing chat",
1433
+ conversationType: "standard",
1434
+ };
1435
+
1436
+ const result = await pairDeliveryWithConversation(
1437
+ share({ sourceContextId: "conv-user" }),
1438
+ "vellum" as NotificationChannel,
1439
+ makeCopy(),
1440
+ );
1441
+
1442
+ expect(result.createdNewConversation).toBe(false);
1443
+ expect(result.conversationId).toBe("conv-user");
1444
+ expect(createConversationMock).not.toHaveBeenCalled();
1445
+ });
1446
+
1447
+ test("flag off: shares keep the passive append even from background runs", async () => {
1448
+ mockExistingConversations["conv-heartbeat"] = {
1449
+ id: "conv-heartbeat",
1450
+ source: "heartbeat",
1451
+ title: null,
1452
+ conversationType: "background",
1453
+ };
1454
+
1455
+ const result = await pairDeliveryWithConversation(
1456
+ share(),
1457
+ "vellum" as NotificationChannel,
1458
+ makeCopy(),
1459
+ );
1460
+
1461
+ expect(result.createdNewConversation).toBe(false);
1462
+ expect(result.conversationId).toBe("conv-heartbeat");
1463
+ expect(createConversationMock).not.toHaveBeenCalled();
1464
+ });
1465
+
1466
+ test("flag on: an explicit conversationMetadata.source wins over promotion", async () => {
1467
+ assistantInitiatedThreadsEnabled = true;
1468
+
1469
+ const result = await pairDeliveryWithConversation(
1470
+ share({ conversationMetadata: { source: "schedule" } }),
1471
+ "vellum" as NotificationChannel,
1472
+ makeCopy(),
1473
+ );
1474
+
1475
+ // The producer declared its own filing, so the passive rule holds.
1476
+ expect(result.createdNewConversation).toBe(false);
1477
+ expect(createConversationMock).not.toHaveBeenCalled();
1478
+ });
1479
+
1480
+ test("flag on: non-share passive events keep the passive append", async () => {
1481
+ assistantInitiatedThreadsEnabled = true;
1482
+ mockExistingConversations["conv-heartbeat"] = {
1483
+ id: "conv-heartbeat",
1484
+ source: "heartbeat",
1485
+ title: null,
1486
+ conversationType: "background",
1487
+ };
1488
+
1489
+ const result = await pairDeliveryWithConversation(
1490
+ share({ sourceEventName: "scheduler.reminder_fired" }),
1491
+ "vellum" as NotificationChannel,
1492
+ makeCopy(),
1493
+ );
1494
+
1495
+ expect(result.createdNewConversation).toBe(false);
1496
+ expect(createConversationMock).not.toHaveBeenCalled();
1497
+ });
1498
+ });
1359
1499
  });
@@ -97,8 +97,35 @@ function createFakeConversation(conversationId: string): Conversation {
97
97
  isProcessing(this: { processing: boolean }) {
98
98
  return this.processing;
99
99
  },
100
- setProcessing(this: { processing: boolean }, value: boolean) {
100
+ setProcessing(
101
+ this: { processing: boolean; owner: number },
102
+ value: boolean,
103
+ ) {
101
104
  this.processing = value;
105
+ this.owner = value ? this.owner + 1 : 0;
106
+ },
107
+ owner: 0,
108
+ async acquireProcessingFenced(this: {
109
+ processing: boolean;
110
+ owner: number;
111
+ }) {
112
+ if (this.processing) {
113
+ return null;
114
+ }
115
+ this.processing = true;
116
+ this.owner += 1;
117
+ return this.owner;
118
+ },
119
+ releaseProcessing(
120
+ this: { processing: boolean; owner: number },
121
+ claim: number,
122
+ ) {
123
+ if (claim !== this.owner) {
124
+ return false;
125
+ }
126
+ this.processing = false;
127
+ this.owner = 0;
128
+ return true;
102
129
  },
103
130
  setChannelCapabilities: () => {},
104
131
  setAssistantId: () => {},