@vellumai/assistant 0.11.8 → 0.11.9-staging.1

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 (318) 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__/guardian-card-withdrawal.test.ts +0 -22
  64. package/src/__tests__/guardian-gateway-sim.ts +0 -23
  65. package/src/__tests__/guardian-question-mode.test.ts +108 -0
  66. package/src/__tests__/guardian-reply-router-answer-mode.test.ts +0 -2
  67. package/src/__tests__/guardian-routing-invariants.test.ts +26 -12
  68. package/src/__tests__/host-proxy-interface.test.ts +10 -0
  69. package/src/__tests__/inbound-slack-persistence.test.ts +16 -0
  70. package/src/__tests__/injector-v3-suppression.test.ts +43 -9
  71. package/src/__tests__/list-messages-page-latest.test.ts +127 -0
  72. package/src/__tests__/list-messages-system-card.test.ts +103 -0
  73. package/src/__tests__/media-resolve-image-validation.test.ts +77 -0
  74. package/src/__tests__/notification-decision-fallback.test.ts +199 -77
  75. package/src/__tests__/notification-decision-strategy.test.ts +198 -213
  76. package/src/__tests__/notification-discord-adapter.test.ts +25 -0
  77. package/src/__tests__/notification-slack-adapter.test.ts +133 -0
  78. package/src/__tests__/notification-telegram-adapter.test.ts +36 -0
  79. package/src/__tests__/openai-provider.test.ts +5 -5
  80. package/src/__tests__/outbound-slack-persistence.test.ts +85 -72
  81. package/src/__tests__/persist-user-message-set-processing-failure.test.ts +83 -65
  82. package/src/__tests__/platform-client-verify-credential.test.ts +100 -0
  83. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -1
  84. package/src/__tests__/process-message-display-content.test.ts +20 -9
  85. package/src/__tests__/processing-acquire-fenced-guard.test.ts +56 -0
  86. package/src/__tests__/provider-meta-persistence.test.ts +67 -0
  87. package/src/__tests__/reaction-persistence.test.ts +22 -198
  88. package/src/__tests__/run-conversation-turn-persistence.test.ts +5 -2
  89. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +16 -0
  90. package/src/__tests__/skill-load-tool.test.ts +27 -0
  91. package/src/__tests__/skills.test.ts +34 -1
  92. package/src/__tests__/strip-memory-injections.test.ts +3 -4
  93. package/src/__tests__/terminal-tools.test.ts +9 -0
  94. package/src/__tests__/thread-backfill.test.ts +5 -3
  95. package/src/__tests__/unified-turn-context-location.test.ts +76 -0
  96. package/src/__tests__/voice-session-bridge.test.ts +18 -0
  97. package/src/__tests__/watch-retro-report-payload.test.ts +185 -0
  98. package/src/__tests__/watch-retro-tool-availability.test.ts +82 -0
  99. package/src/__tests__/workspace-migration-151-repair-renamed-fireworks-deepseek-pro-model-id.test.ts +235 -0
  100. package/src/__tests__/workspace-migration-152-repair-retired-fireworks-minimax-m2p7-model-id.test.ts +233 -0
  101. package/src/agent/attachments.ts +13 -2
  102. package/src/agent/loop.ts +3 -19
  103. package/src/api/README.md +9 -5
  104. package/src/api/index.ts +9 -8
  105. package/src/api/package.json +1 -0
  106. package/src/api/responses/conversation-message.ts +8 -0
  107. package/src/api/responses/home.ts +7 -18
  108. package/src/api/surfaces.ts +114 -7
  109. package/src/approvals/AGENTS.md +1 -1
  110. package/src/channels/__tests__/gateway-guardian-requests.test.ts +1 -23
  111. package/src/channels/__tests__/types.test.ts +22 -1
  112. package/src/channels/gateway-guardian-requests.ts +0 -22
  113. package/src/channels/types.ts +8 -6
  114. package/src/cli/__tests__/catalog-search-help.test.ts +12 -0
  115. package/src/cli/commands/channels/__tests__/channels.test.ts +26 -0
  116. package/src/cli/commands/channels/index.help.ts +15 -1
  117. package/src/cli/commands/channels/index.ts +6 -2
  118. package/src/cli/commands/db/__tests__/status.test.ts +22 -0
  119. package/src/cli/commands/db/index.help.ts +1 -1
  120. package/src/cli/commands/db/status.ts +172 -1
  121. package/src/cli/commands/platform/__tests__/connect.test.ts +29 -0
  122. package/src/cli/commands/platform/connect.ts +14 -5
  123. package/src/cli/commands/plugins.help.ts +6 -5
  124. package/src/cli/lib/__tests__/install-from-github.test.ts +0 -8
  125. package/src/cli/lib/__tests__/install-from-platform.test.ts +72 -0
  126. package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +33 -0
  127. package/src/cli/lib/bundled-marketplace.json +14 -0
  128. package/src/cli/lib/install-from-github.ts +9 -5
  129. package/src/cli/lib/install-from-platform.ts +12 -1
  130. package/src/config/__tests__/assistant-initiated-threads-gate.test.ts +61 -0
  131. package/src/config/assistant-initiated-threads-gate.ts +50 -0
  132. package/src/config/bundled-skills/acp/SKILL.md +10 -3
  133. package/src/config/bundled-skills/schedule/SKILL.md +25 -11
  134. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +3 -1
  135. package/src/config/call-site-defaults.ts +4 -1
  136. package/src/config/feature-flag-registry.json +21 -4
  137. package/src/config/schemas/memory-v3.ts +4 -3
  138. package/src/context/strip-injections.ts +17 -56
  139. package/src/conversations/__tests__/message-consolidation.test.ts +39 -0
  140. package/src/conversations/message-consolidation.ts +4 -3
  141. package/src/credential-health/credential-health-service.ts +130 -0
  142. package/src/daemon/__tests__/conversation-tool-setup.test.ts +31 -0
  143. package/src/daemon/conversation-agent-loop-handlers.ts +55 -59
  144. package/src/daemon/conversation-error.ts +2 -0
  145. package/src/daemon/conversation-messaging.ts +195 -47
  146. package/src/daemon/conversation-process.ts +20 -5
  147. package/src/daemon/conversation-runtime-assembly.ts +48 -39
  148. package/src/daemon/conversation-store.ts +159 -11
  149. package/src/daemon/conversation-tool-setup.ts +9 -2
  150. package/src/daemon/conversation.ts +290 -19
  151. package/src/daemon/dictation-text-processing.ts +2 -7
  152. package/src/daemon/handlers/config-channels.ts +33 -3
  153. package/src/daemon/handlers/shared.ts +9 -0
  154. package/src/daemon/port-oversized-content.test.ts +117 -0
  155. package/src/daemon/port-oversized-content.ts +110 -0
  156. package/src/daemon/process-message.ts +87 -16
  157. package/src/daemon/reaction-record.test.ts +23 -11
  158. package/src/daemon/reaction-record.ts +6 -9
  159. package/src/documents/document-store.ts +1 -5
  160. package/src/home/conversation-starter-validation.ts +1 -5
  161. package/src/live-voice/__tests__/live-voice-photo.test.ts +910 -50
  162. package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +31 -25
  163. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +70 -1
  164. package/src/live-voice/live-voice-photo.ts +544 -61
  165. package/src/live-voice/live-voice-session.ts +6 -2
  166. package/src/live-voice/protocol.ts +20 -1
  167. package/src/messaging/provider-message-metadata.ts +40 -1
  168. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +10 -2
  169. package/src/messaging/providers/channel-transport.ts +28 -0
  170. package/src/messaging/providers/discord/send.ts +3 -2
  171. package/src/messaging/providers/slack/api.ts +11 -1
  172. package/src/messaging/providers/slack/message-metadata.test.ts +133 -0
  173. package/src/messaging/providers/slack/message-metadata.ts +141 -1
  174. package/src/messaging/providers/slack/send.test.ts +58 -0
  175. package/src/messaging/providers/slack/send.ts +4 -4
  176. package/src/messaging/providers/slack/transport.ts +28 -2
  177. package/src/messaging/providers/telegram-bot/send.test.ts +159 -0
  178. package/src/messaging/providers/telegram-bot/send.ts +93 -0
  179. package/src/messaging/providers/telegram-bot/transport.ts +71 -0
  180. package/src/messaging/reaction-envelopes.test.ts +73 -0
  181. package/src/messaging/reaction-envelopes.ts +33 -5
  182. package/src/messaging/read-provider-metadata.ts +4 -3
  183. package/src/monitoring/recovery/__tests__/stranded-delivery-events.test.ts +208 -0
  184. package/src/monitoring/recovery/db.ts +35 -0
  185. package/src/monitoring/recovery/orphaned-channel-events.ts +3 -22
  186. package/src/monitoring/recovery/run-recovery.ts +6 -2
  187. package/src/monitoring/recovery/stale-processing.ts +3 -20
  188. package/src/monitoring/recovery/stranded-delivery-events.ts +68 -0
  189. package/src/notifications/AGENTS.md +2 -2
  190. package/src/notifications/README.md +2 -2
  191. package/src/notifications/__tests__/assistant-reply-producer.test.ts +11 -9
  192. package/src/notifications/__tests__/broadcaster.test.ts +35 -4
  193. package/src/notifications/__tests__/notification-utils.test.ts +31 -0
  194. package/src/notifications/access-request-copy.ts +126 -114
  195. package/src/notifications/adapters/discord.ts +9 -5
  196. package/src/notifications/adapters/shared.ts +17 -4
  197. package/src/notifications/adapters/slack.ts +14 -4
  198. package/src/notifications/adapters/telegram.ts +8 -4
  199. package/src/notifications/approval-card-data.ts +5 -2
  200. package/src/notifications/assistant-reply-producer.ts +7 -7
  201. package/src/notifications/broadcaster.ts +57 -25
  202. package/src/notifications/conversation-pairing.ts +68 -2
  203. package/src/notifications/copy-composer.ts +13 -32
  204. package/src/notifications/decision-engine.ts +56 -237
  205. package/src/notifications/guardian-delivery-recorder.ts +5 -9
  206. package/src/notifications/guardian-feed-projection.ts +0 -15
  207. package/src/notifications/guardian-question-mode.ts +147 -6
  208. package/src/notifications/notification-utils.ts +117 -1
  209. package/src/permissions/confirmation-guardian-request.ts +2 -2
  210. package/src/permissions/prompter.ts +1 -1
  211. package/src/persistence/conversation-crud.ts +93 -14
  212. package/src/persistence/conversation-queries.ts +144 -17
  213. package/src/persistence/conversation-types.ts +39 -5
  214. package/src/persistence/delivery-crud.ts +224 -28
  215. package/src/persistence/delivery-status.ts +21 -2
  216. package/src/persistence/migrations/374-channel-inbound-message-id-index.ts +26 -0
  217. package/src/persistence/migrations/375-create-channel-outbound-posts.ts +52 -0
  218. package/src/persistence/migrations/__tests__/375-create-channel-outbound-posts.test.ts +84 -0
  219. package/src/persistence/schema/conversations.ts +71 -2
  220. package/src/persistence/schema-contract.test.ts +78 -0
  221. package/src/persistence/schema-contract.ts +96 -0
  222. package/src/persistence/steps.ts +4 -0
  223. package/src/platform/client.ts +55 -0
  224. package/src/plugins/__tests__/mcp-servers.test.ts +46 -0
  225. package/src/plugins/defaults/injector-order.ts +2 -1
  226. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +2 -2
  227. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +13 -15
  228. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +10 -0
  229. package/src/plugins/defaults/memory/injectors.ts +1 -1
  230. package/src/plugins/defaults/memory/memory-marker.ts +17 -7
  231. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +1 -5
  232. package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +54 -41
  233. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +3 -3
  234. package/src/plugins/defaults/memory/v3/injector.ts +14 -15
  235. package/src/plugins/defaults/memory/v3/types.ts +15 -4
  236. package/src/plugins/defaults/turn-context/injectors.ts +3 -0
  237. package/src/plugins/defaults/turn-context/unified-turn-context.ts +24 -0
  238. package/src/plugins/mcp-servers.ts +14 -2
  239. package/src/providers/__tests__/dispatch-connection-routing.test.ts +50 -0
  240. package/src/providers/__tests__/registry-native-web-search.test.ts +49 -2
  241. package/src/providers/anthropic/client.ts +21 -24
  242. package/src/providers/call-site-routing.ts +5 -4
  243. package/src/providers/connection-resolution.ts +29 -1
  244. package/src/providers/content-block-size.test.ts +82 -0
  245. package/src/providers/content-block-size.ts +99 -0
  246. package/src/providers/file-block-text.test.ts +64 -0
  247. package/src/providers/file-block-text.ts +28 -0
  248. package/src/providers/gemini/client.ts +12 -7
  249. package/src/providers/inference/auth.ts +6 -6
  250. package/src/providers/media-resolve.ts +14 -0
  251. package/src/providers/model-catalog.ts +31 -34
  252. package/src/providers/openai/chat-completions-provider.ts +9 -25
  253. package/src/providers/openai/responses-provider.ts +11 -18
  254. package/src/providers/registry.ts +10 -7
  255. package/src/providers/routing-identity.ts +2 -1
  256. package/src/providers/types.ts +1 -4
  257. package/src/providers/vellum-model-routing.ts +3 -2
  258. package/src/runtime/AGENTS.md +40 -15
  259. package/src/runtime/approval-message-composer.ts +1 -6
  260. package/src/runtime/assistant-event-hub.ts +2 -39
  261. package/src/runtime/channel-approval-types.ts +1 -5
  262. package/src/runtime/channel-reply-delivery.ts +182 -115
  263. package/src/runtime/{slack-reply-session.test.ts → channel-reply-session.test.ts} +294 -156
  264. package/src/runtime/{slack-reply-session.ts → channel-reply-session.ts} +107 -86
  265. package/src/runtime/channel-retry-sweep.ts +102 -1
  266. package/src/runtime/finalize-event-delivery.ts +4 -4
  267. package/src/runtime/guardian-action-message-composer.ts +1 -4
  268. package/src/runtime/guardian-reply-router.ts +4 -75
  269. package/src/runtime/http-router.ts +1 -5
  270. package/src/runtime/question-request-guardian-bridge.ts +2 -3
  271. package/src/runtime/routes/__tests__/conversation-list-assistant-section.test.ts +359 -0
  272. package/src/runtime/routes/__tests__/dictation-command-mode.test.ts +120 -0
  273. package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +487 -0
  274. package/src/runtime/routes/acp-routes.ts +1 -1
  275. package/src/runtime/routes/canned-reply-release.ts +19 -9
  276. package/src/runtime/routes/channel-route-shared.ts +0 -39
  277. package/src/runtime/routes/channel-verification-routes.ts +4 -1
  278. package/src/runtime/routes/conversation-list-routes.ts +44 -3
  279. package/src/runtime/routes/conversation-management-routes.ts +2 -1
  280. package/src/runtime/routes/conversation-routes.ts +138 -59
  281. package/src/runtime/routes/diagnostics-routes.ts +111 -60
  282. package/src/runtime/routes/guardian-action-routes.ts +10 -14
  283. package/src/runtime/routes/guardian-approval-interception.ts +0 -5
  284. package/src/runtime/routes/inbound-message-handler.ts +170 -48
  285. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +4 -3
  286. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +169 -8
  287. package/src/runtime/routes/inbound-stages/background-dispatch.ts +82 -14
  288. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +23 -48
  289. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +454 -94
  290. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +309 -102
  291. package/src/runtime/routes/index.ts +2 -0
  292. package/src/runtime/routes/platform-routes.ts +99 -10
  293. package/src/runtime/routes/sight-frame-routes.ts +136 -0
  294. package/src/runtime/sync/resource-sync-events.ts +14 -37
  295. package/src/runtime/sync/sync-publisher.test.ts +5 -4
  296. package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
  297. package/src/tools/client-os.ts +11 -1
  298. package/src/tools/host-filesystem/edit.ts +2 -2
  299. package/src/tools/host-filesystem/read.ts +2 -2
  300. package/src/tools/host-filesystem/transfer.ts +2 -2
  301. package/src/tools/host-filesystem/write.ts +2 -2
  302. package/src/tools/host-terminal/host-shell.ts +2 -2
  303. package/src/tools/skills/load.ts +1 -1
  304. package/src/tools/terminal/safe-env.ts +4 -0
  305. package/src/tools/tool-input-schemas.ts +2 -0
  306. package/src/tools/tool-manifest.ts +2 -0
  307. package/src/tools/ui-surface/definitions.ts +4 -3
  308. package/src/tools/watch/watch-retro-report.ts +205 -0
  309. package/src/watch/__tests__/watch-retro.test.ts +268 -40
  310. package/src/watch/watch-retro.ts +190 -77
  311. package/src/workspace/migrations/151-repair-renamed-fireworks-deepseek-pro-model-id.ts +195 -0
  312. package/src/workspace/migrations/152-repair-retired-fireworks-minimax-m2p7-model-id.ts +198 -0
  313. package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +0 -10
  314. package/src/workspace/migrations/registry.ts +4 -0
  315. package/docker-kata-pip-chroot.sh +0 -22
  316. package/src/__tests__/slack-reaction-approvals.test.ts +0 -97
  317. package/src/__tests__/slack-reaction-guardian-approval.test.ts +0 -307
  318. package/src/api/events/conversation-list-invalidated.ts +0 -38
@@ -13,8 +13,8 @@
13
13
  * validation, expiry checks, the atomic gateway CAS+outcome commit,
14
14
  * kind-specific resolver dispatch, and grant minting.
15
15
  *
16
- * Routing reads (code lookup, pending discovery, reaction addressing) use the
17
- * degrading gateway-client variants: an unreachable gateway resolves to "no
16
+ * Routing reads (code lookup, pending discovery) use the degrading
17
+ * gateway-client variants: an unreachable gateway resolves to "no
18
18
  * pending requests", so the message falls through to the normal pipeline
19
19
  * instead of failing the inbound turn — decisions themselves still fail
20
20
  * loudly inside the primitive.
@@ -23,7 +23,6 @@
23
23
  * to allow for incremental migration and independent testability.
24
24
  */
25
25
 
26
- import type { InboundReactionPayload } from "@vellumai/gateway-client";
27
26
  import { isGuardianRequestExpired } from "@vellumai/gateway-client";
28
27
 
29
28
  import {
@@ -38,7 +37,6 @@ import type {
38
37
  import {
39
38
  getGuardianRequestByCodeOrNull,
40
39
  getGuardianRequestOrNull,
41
- getPendingRequestByDestinationMessageOrNull,
42
40
  type GuardianRequestWire,
43
41
  listGuardianRequestsOrEmpty,
44
42
  } from "../channels/gateway-guardian-requests.js";
@@ -62,7 +60,6 @@ import type {
62
60
  ApprovalConversationGenerator,
63
61
  } from "./http-types.js";
64
62
  import * as pendingInteractions from "./pending-interactions.js";
65
- import { reactionDecisionForEmoji } from "./routes/channel-route-shared.js";
66
63
 
67
64
  const log = getLogger("guardian-reply-router");
68
65
 
@@ -94,29 +91,16 @@ export type GuardianPendingScope =
94
91
  export interface GuardianReplyContext {
95
92
  /** The raw message text (trimmed). */
96
93
  messageText: string;
97
- /** Source channel (telegram, whatsapp, etc.). */
98
- channel: string;
99
94
  /** Actor identity context for the sender. */
100
95
  actor: ActorContext;
101
96
  /**
102
97
  * Conversation the message arrived in (may be the guardian's own). Only the
103
- * text and NL paths read it; reactions and `apr:` callbacks resolve their
104
- * target by card address or embedded request id and may pass none.
98
+ * text and NL paths read it; `apr:` callbacks resolve their target by the
99
+ * embedded request id and may pass none.
105
100
  */
106
101
  conversationId?: string;
107
102
  /** Callback data from button presses (e.g. `apr:<requestId>:<action>`). */
108
103
  callbackData?: string;
109
- /**
110
- * Structured reaction payload when the inbound event is a reaction; the
111
- * reaction branch keys on it and reads nothing off callbackData.
112
- */
113
- reaction?: InboundReactionPayload;
114
- /**
115
- * For emoji-reaction decisions: the
116
- * channel-native id (e.g. Slack `ts`) of the message the reaction was
117
- * attached to. Used to recover the target request from its delivery record.
118
- */
119
- reactedMessageTs?: string;
120
104
  /**
121
105
  * How to scope this guardian's pending requests (see {@link
122
106
  * GuardianPendingScope}). Omitted is equivalent to
@@ -357,69 +341,14 @@ export async function routeGuardianReply(
357
341
  ): Promise<GuardianReplyResult> {
358
342
  const {
359
343
  messageText,
360
- channel,
361
344
  actor,
362
345
  conversationId,
363
346
  callbackData,
364
- reactedMessageTs,
365
347
  approvalConversationGenerator,
366
348
  channelDeliveryContext,
367
349
  emissionContext,
368
350
  } = ctx;
369
351
 
370
- // ── 0. Reaction decisions (emoji on a delivered approval card) ──
371
- // A reaction carries an emoji plus the message it is attached to. Map the
372
- // emoji to an action and recover the target request from that card's
373
- // delivery record. Addressing by the reacted message disambiguates precisely
374
- // even when several cards are pending in the same chat, so — unlike the
375
- // text/NL paths — no clarification prompt is ever needed. `reaction_removed`
376
- // never expresses intent and is filtered out before reaching the router.
377
- if (ctx.reaction?.op === "added") {
378
- const decision = reactionDecisionForEmoji(ctx.reaction.emoji);
379
- const guardianChatId = channelDeliveryContext?.guardianChatId;
380
- if (!decision || !reactedMessageTs || !guardianChatId) {
381
- // Unknown emoji, or missing addressing context — not an actionable
382
- // approval reaction. Leave it for the caller to persist as a transcript
383
- // signal (it must not trigger an agent turn).
384
- return notConsumed();
385
- }
386
- // A delivery row keys its card by the destination the adapter delivered
387
- // to, and channels address that destination in one of two modalities: a
388
- // chat (the conversation the reaction arrives from) or a person (the
389
- // guardian's own user id, which person-addressed adapters store so a
390
- // stale room id can never become a delivery target). The reactor on an
391
- // approval card is the person it was delivered to, so their id is the
392
- // second probe; identity validation in applyGuardianDecision still
393
- // guards the decision itself.
394
- const request =
395
- (await getPendingRequestByDestinationMessageOrNull(
396
- channel,
397
- guardianChatId,
398
- reactedMessageTs,
399
- )) ??
400
- (actor.actorExternalUserId
401
- ? await getPendingRequestByDestinationMessageOrNull(
402
- channel,
403
- actor.actorExternalUserId,
404
- reactedMessageTs,
405
- )
406
- : null);
407
- if (!request) {
408
- // The reacted message is not a known pending approval card (a stray
409
- // reaction, or one whose request was already resolved). Never approve
410
- // off an unrecognized message.
411
- return notConsumed();
412
- }
413
- return applyDecision(
414
- request.id,
415
- decision.action,
416
- actor,
417
- undefined,
418
- channelDeliveryContext,
419
- emissionContext,
420
- );
421
- }
422
-
423
352
  const pendingScope: GuardianPendingScope = ctx.pendingScope ?? {
424
353
  mode: "identity-fallback",
425
354
  };
@@ -202,7 +202,7 @@ function compileRoute(def: HTTPRouteDefinition): CompiledRoute {
202
202
  const typePattern = PARAM_TYPE_PATTERNS[paramTypeMap.get(name) ?? ""];
203
203
  return typePattern ? `(${typePattern})` : "([^/]+)";
204
204
  }
205
- return escapeRegex(segment);
205
+ return RegExp.escape(segment);
206
206
  })
207
207
  .join("\\/");
208
208
 
@@ -210,7 +210,3 @@ function compileRoute(def: HTTPRouteDefinition): CompiledRoute {
210
210
 
211
211
  return { def, regex, paramNames };
212
212
  }
213
-
214
- function escapeRegex(str: string): string {
215
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
216
- }
@@ -4,9 +4,8 @@
4
4
  *
5
5
  * Mirrors `confirmation-request-guardian-bridge.ts` for the question kind:
6
6
  * emits the notification signal (the broadcaster renders the options as card
7
- * actions — see `resolveQuestionOptionsContext`) and records the resulting
8
- * card deliveries so reply routing can address taps and reactions back to the
9
- * request.
7
+ * actions — see `resolveQuestionContext`) and records the resulting
8
+ * card deliveries so reply routing can address taps back to the request.
10
9
  *
11
10
  * Unlike the confirmation bridge — which escalates a NON-guardian's request TO
12
11
  * the guardian — the caller (`question-guardian-request.ts`) has already
@@ -0,0 +1,359 @@
1
+ /**
2
+ * The `assistant-initiated-threads` split: the threads the assistant started
3
+ * on its own (`source = 'assistant_initiated'`, opt-in at creation) become
4
+ * their own sidebar section instead of sitting in Chats.
5
+ *
6
+ * The flag is the whole point of these tests. It is a read-side gate over rows
7
+ * that exist either way, so the off arm must be byte-identical to the behavior
8
+ * that shipped before the section existed — that is what makes the feature
9
+ * reversible without a migration. Both arms are asserted for every claim.
10
+ *
11
+ * The gate module is mocked rather than the flag registry: the registry
12
+ * resolves through workspace override files, and a test that wrote those would
13
+ * be asserting the resolver's behavior rather than this route's.
14
+ */
15
+
16
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
17
+
18
+ // Avoid spinning up the real event hub for the pinned/groups branches.
19
+ mock.module("../../assistant-event-hub.js", () => ({
20
+ assistantEventHub: {
21
+ publish: async () => {},
22
+ subscribe: () => () => {},
23
+ },
24
+ broadcastMessage: () => {},
25
+ }));
26
+
27
+ let flagEnabled = false;
28
+ mock.module("../../../config/assistant-initiated-threads-gate.js", () => ({
29
+ isAssistantInitiatedThreadsEnabled: () => flagEnabled,
30
+ }));
31
+
32
+ import { projectAssistantMessage } from "../../../persistence/conversation-attention-store.js";
33
+ import { createConversation } from "../../../persistence/conversation-crud.js";
34
+ import { getDb } from "../../../persistence/db-connection.js";
35
+ import { initializeDb } from "../../../persistence/db-init.js";
36
+ import { createGroup } from "../../../persistence/group-crud.js";
37
+ import { rawRun } from "../../../persistence/raw-query.js";
38
+ import { conversations } from "../../../persistence/schema/index.js";
39
+ import { ROUTES as CONVERSATION_LIST_ROUTES } from "../conversation-list-routes.js";
40
+ import type { RouteDefinition } from "../types.js";
41
+
42
+ await initializeDb();
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Helpers
46
+ // ---------------------------------------------------------------------------
47
+
48
+ function findHandler(routes: RouteDefinition[], operationId: string) {
49
+ const route = routes.find((r) => r.operationId === operationId);
50
+ if (!route) {
51
+ throw new Error(`Route ${operationId} not found`);
52
+ }
53
+ return route.handler;
54
+ }
55
+
56
+ const sectionsHandler = findHandler(
57
+ CONVERSATION_LIST_ROUTES,
58
+ "getConversationSections",
59
+ );
60
+ const listHandler = findHandler(CONVERSATION_LIST_ROUTES, "listConversations");
61
+
62
+ interface SectionRow {
63
+ kind: "pinned" | "group" | "channel" | "chats" | "assistant";
64
+ channelId?: string;
65
+ groupId?: string;
66
+ total: number;
67
+ unread: number;
68
+ }
69
+
70
+ function invokeSections(): SectionRow[] {
71
+ return (sectionsHandler({}) as { sections: SectionRow[] }).sections;
72
+ }
73
+
74
+ interface ListResponse {
75
+ conversations: Array<{ id: string; title: string }>;
76
+ }
77
+
78
+ function invokeList(queryParams: Record<string, string> = {}): ListResponse {
79
+ return listHandler({ queryParams }) as ListResponse;
80
+ }
81
+
82
+ function titlesFrom(response: ListResponse): string[] {
83
+ return response.conversations.map((c) => c.title).sort();
84
+ }
85
+
86
+ /**
87
+ * A thread a producer opted into the assistant-initiated section: a
88
+ * standard-type conversation, filed nowhere, stamped
89
+ * `source: 'assistant_initiated'` at creation (ASSISTANT_INITIATED_SOURCE).
90
+ */
91
+ function seedAssistantInitiated(title: string): string {
92
+ return createConversation({ title, source: "assistant_initiated" }).id;
93
+ }
94
+
95
+ /**
96
+ * A transactional notification trail - what the guardian request flows and
97
+ * channel deliveries materialize (`source: 'notification'`, see
98
+ * `notifications/conversation-pairing.ts`). Never a member of the section:
99
+ * these belong to the bell and to Chats.
100
+ */
101
+ function seedNotificationTrail(title: string): string {
102
+ return createConversation({ title, source: "notification" }).id;
103
+ }
104
+
105
+ function seedUnseen(conversationId: string): void {
106
+ projectAssistantMessage({
107
+ conversationId,
108
+ messageId: `msg-${conversationId}`,
109
+ messageAt: Date.now(),
110
+ });
111
+ }
112
+
113
+ function stampChannel(conversationId: string, channel: string): void {
114
+ rawRun(
115
+ "test:stampChannel",
116
+ "UPDATE conversations SET origin_channel = ? WHERE id = ?",
117
+ channel,
118
+ conversationId,
119
+ );
120
+ }
121
+
122
+ function pin(conversationId: string): void {
123
+ rawRun(
124
+ "test:pinConversation",
125
+ "UPDATE conversations SET is_pinned = 1, group_id = 'system:pinned' WHERE id = ?",
126
+ conversationId,
127
+ );
128
+ }
129
+
130
+ function fileIntoGroup(conversationId: string, groupId: string): void {
131
+ rawRun(
132
+ "test:fileIntoGroup",
133
+ "UPDATE conversations SET group_id = ? WHERE id = ?",
134
+ groupId,
135
+ conversationId,
136
+ );
137
+ }
138
+
139
+ beforeEach(() => {
140
+ flagEnabled = false;
141
+ getDb().delete(conversations).run();
142
+ });
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // Flag off — the shipped behavior, unchanged
146
+ // ---------------------------------------------------------------------------
147
+
148
+ describe("assistant-initiated threads — flag off", () => {
149
+ test("emits no assistant section, and the threads stay counted in Chats", () => {
150
+ seedAssistantInitiated("overnight-realization");
151
+ createConversation({ title: "user-thread" });
152
+
153
+ const sections = invokeSections();
154
+
155
+ expect(sections.some((s) => s.kind === "assistant")).toBe(false);
156
+ expect(sections).toContainEqual({ kind: "chats", total: 2, unread: 0 });
157
+ });
158
+
159
+ test("the threads stay listed in the standard list", () => {
160
+ seedAssistantInitiated("overnight-realization");
161
+ createConversation({ title: "user-thread" });
162
+
163
+ expect(titlesFrom(invokeList())).toEqual([
164
+ "overnight-realization",
165
+ "user-thread",
166
+ ]);
167
+ });
168
+
169
+ test("their unread still counts toward Chats", () => {
170
+ seedUnseen(seedAssistantInitiated("noticed-something"));
171
+ createConversation({ title: "user-thread" });
172
+
173
+ expect(invokeSections()).toContainEqual({
174
+ kind: "chats",
175
+ total: 2,
176
+ unread: 1,
177
+ });
178
+ });
179
+ });
180
+
181
+ // ---------------------------------------------------------------------------
182
+ // Flag on — the split
183
+ // ---------------------------------------------------------------------------
184
+
185
+ describe("assistant-initiated threads — flag on", () => {
186
+ beforeEach(() => {
187
+ flagEnabled = true;
188
+ });
189
+
190
+ test("the threads move out of Chats into their own section", () => {
191
+ seedAssistantInitiated("overnight-realization");
192
+ seedAssistantInitiated("noticed-something");
193
+ createConversation({ title: "user-thread" });
194
+
195
+ const sections = invokeSections();
196
+
197
+ expect(sections).toContainEqual({ kind: "assistant", total: 2, unread: 0 });
198
+ expect(sections).toContainEqual({ kind: "chats", total: 1, unread: 0 });
199
+ });
200
+
201
+ test("the section renders at zero, the way Chats does", () => {
202
+ createConversation({ title: "user-thread" });
203
+
204
+ expect(invokeSections()).toContainEqual({
205
+ kind: "assistant",
206
+ total: 0,
207
+ unread: 0,
208
+ });
209
+ });
210
+
211
+ test("unread follows the section, and is not double-counted in Chats", () => {
212
+ seedUnseen(seedAssistantInitiated("noticed-something"));
213
+ seedAssistantInitiated("read-already");
214
+ seedUnseen(createConversation({ title: "user-thread" }).id);
215
+
216
+ const sections = invokeSections();
217
+
218
+ expect(sections).toContainEqual({ kind: "assistant", total: 2, unread: 1 });
219
+ expect(sections).toContainEqual({ kind: "chats", total: 1, unread: 1 });
220
+ });
221
+
222
+ test("the split leaves channel sections alone", () => {
223
+ seedAssistantInitiated("overnight-realization");
224
+ stampChannel(createConversation({ title: "slack-thread" }).id, "slack");
225
+
226
+ const sections = invokeSections();
227
+
228
+ expect(sections).toContainEqual({ kind: "assistant", total: 1, unread: 0 });
229
+ expect(sections).toContainEqual({
230
+ kind: "channel",
231
+ channelId: "slack",
232
+ total: 1,
233
+ unread: 0,
234
+ });
235
+ });
236
+
237
+ test("groupId=system:assistant lists exactly those threads", () => {
238
+ seedAssistantInitiated("overnight-realization");
239
+ seedAssistantInitiated("noticed-something");
240
+ createConversation({ title: "user-thread" });
241
+
242
+ expect(titlesFrom(invokeList({ groupId: "system:assistant" }))).toEqual([
243
+ "noticed-something",
244
+ "overnight-realization",
245
+ ]);
246
+ });
247
+
248
+ test("the standard list withholds them, and keeps every NULL-source row", () => {
249
+ // The regression this guards: `NOT (source = '...')` is NULL for the
250
+ // NULL-source rows that make up most of the list, so a naive negation
251
+ // would return an empty list rather than one section fewer.
252
+ seedAssistantInitiated("overnight-realization");
253
+ createConversation({ title: "user-thread" });
254
+ createConversation({ title: "another-user-thread" });
255
+
256
+ expect(titlesFrom(invokeList())).toEqual([
257
+ "another-user-thread",
258
+ "user-thread",
259
+ ]);
260
+ });
261
+
262
+ test("a list already scoped to the section is not narrowed to nothing", () => {
263
+ // `groupId=system:assistant` selects on the same column the exclusion
264
+ // negates, so applying both would return an empty page for the one caller
265
+ // that actually wants these rows.
266
+ seedAssistantInitiated("overnight-realization");
267
+
268
+ expect(titlesFrom(invokeList({ groupId: "system:assistant" }))).toEqual([
269
+ "overnight-realization",
270
+ ]);
271
+ });
272
+
273
+ test("a pinned thread belongs to Pinned, not to both", () => {
274
+ // Section membership is single-valued everywhere else in the sidebar, and
275
+ // this section is carved out of the same ungrouped set Chats is, so
276
+ // pinning has to move a thread rather than duplicate it.
277
+ pin(seedAssistantInitiated("pinned-realization"));
278
+ seedAssistantInitiated("ordinary-realization");
279
+
280
+ const sections = invokeSections();
281
+
282
+ expect(sections).toContainEqual({ kind: "assistant", total: 1, unread: 0 });
283
+ expect(sections).toContainEqual({ kind: "pinned", total: 1, unread: 0 });
284
+ expect(titlesFrom(invokeList({ groupId: "system:assistant" }))).toEqual([
285
+ "ordinary-realization",
286
+ ]);
287
+ });
288
+
289
+ test("transactional notification trails are not members, and stay in Chats", () => {
290
+ // The membership source is opt-in precisely so the flag turning on cannot
291
+ // sweep the historical guardian-request and channel-delivery trails
292
+ // (`source = 'notification'`) into the section: only threads written for
293
+ // the section, from the moment producers start stamping its source, are
294
+ // members.
295
+ seedNotificationTrail("approve-this-call");
296
+ seedAssistantInitiated("overnight-realization");
297
+
298
+ const sections = invokeSections();
299
+
300
+ expect(sections).toContainEqual({ kind: "assistant", total: 1, unread: 0 });
301
+ expect(sections).toContainEqual({ kind: "chats", total: 1, unread: 0 });
302
+ expect(titlesFrom(invokeList())).toEqual(["approve-this-call"]);
303
+ expect(titlesFrom(invokeList({ groupId: "system:assistant" }))).toEqual([
304
+ "overnight-realization",
305
+ ]);
306
+ });
307
+
308
+ test("a thread filed into a custom group leaves the section and stays listable", () => {
309
+ // The exclusion is the complement of the section, so a thread that moved
310
+ // out of the section must not stay excluded from the list of the group it
311
+ // moved into — otherwise filing one makes it unreachable.
312
+ const group = createGroup("Reading List");
313
+ const filed = seedAssistantInitiated("filed-realization");
314
+ fileIntoGroup(filed, group.id);
315
+
316
+ expect(invokeSections()).toContainEqual({
317
+ kind: "assistant",
318
+ total: 0,
319
+ unread: 0,
320
+ });
321
+ expect(titlesFrom(invokeList({ groupId: group.id }))).toEqual([
322
+ "filed-realization",
323
+ ]);
324
+ });
325
+
326
+ test("the Archive view keeps archived section threads", () => {
327
+ // Archive asks archiveStatus=archived with no groupId, and the section
328
+ // only ever lists active rows, so the split's exclusion must not reach
329
+ // archived reads or an archived section thread is visible nowhere.
330
+ const archived = seedAssistantInitiated("archived-realization");
331
+ rawRun(
332
+ "test:archive",
333
+ "UPDATE conversations SET archived_at = ? WHERE id = ?",
334
+ Date.now(),
335
+ archived,
336
+ );
337
+
338
+ expect(titlesFrom(invokeList({ archiveStatus: "archived" }))).toEqual([
339
+ "archived-realization",
340
+ ]);
341
+ });
342
+
343
+ test("the background umbrella is left unnarrowed", () => {
344
+ // A background row carrying the section's own source is addressed by
345
+ // conversation type, and narrowing that bucket by source would drop rows
346
+ // its callers still page through.
347
+ const background = createConversation({
348
+ title: "background-realization",
349
+ conversationType: "background",
350
+ source: "assistant_initiated",
351
+ });
352
+
353
+ expect(
354
+ invokeList({ conversationType: "background" }).conversations.map(
355
+ (c) => c.id,
356
+ ),
357
+ ).toContain(background.id);
358
+ });
359
+ });
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Command mode of `POST /v1/dictation`: selected text with the words spoken
3
+ * over it. The model is asked, through one forced tool call, whether the
4
+ * words wanted the selection changed or asked about it, and the route answers
5
+ * `command` with the edit or `question` with the words as they were.
6
+ */
7
+
8
+ import { afterEach, describe, expect, mock, test } from "bun:test";
9
+
10
+ type ToolInput = { kind?: string; text?: string };
11
+ let nextToolInput: ToolInput | null = null;
12
+ let forcedTool: string | undefined;
13
+
14
+ mock.module("../../../providers/provider-send-message.js", () => ({
15
+ getConfiguredProvider: async () => ({
16
+ sendMessage: async (
17
+ _messages: unknown,
18
+ options: { config?: { tool_choice?: { name?: string } } },
19
+ ) => {
20
+ forcedTool = options.config?.tool_choice?.name;
21
+ return {
22
+ content: nextToolInput
23
+ ? [
24
+ {
25
+ type: "tool_use",
26
+ id: "tool-1",
27
+ name: "transform_selection",
28
+ input: nextToolInput,
29
+ },
30
+ ]
31
+ : [{ type: "text", text: "Sure, here it is." }],
32
+ };
33
+ },
34
+ }),
35
+ extractToolUse: (response: { content: Array<{ type: string }> }) =>
36
+ response.content.find((block) => block.type === "tool_use"),
37
+ userMessage: (text: string) => ({ role: "user", content: text }),
38
+ createTimeout: () => ({ signal: undefined, cleanup: () => undefined }),
39
+ }));
40
+
41
+ mock.module("../../../daemon/dictation-profile-store.js", () => ({
42
+ resolveProfile: () => ({
43
+ profile: { id: "default", stylePrompt: "", dictionary: [], snippets: [] },
44
+ source: "default",
45
+ }),
46
+ }));
47
+
48
+ mock.module("../../../daemon/dictation-text-processing.js", () => ({
49
+ applyDictionary: (text: string) => text,
50
+ expandSnippets: (text: string) => text,
51
+ }));
52
+
53
+ const { ROUTES } = await import("../diagnostics-routes.js");
54
+ const route = ROUTES.find((r) => r.operationId === "dictation_post")!;
55
+
56
+ const SELECTION = "Please send me the files.";
57
+
58
+ async function speakOverSelection(transcription: string) {
59
+ return (await route.handler({
60
+ body: {
61
+ transcription,
62
+ context: {
63
+ bundleIdentifier: "com.example.mail",
64
+ appName: "Example Mail",
65
+ windowTitle: "New Message",
66
+ selectedText: SELECTION,
67
+ cursorInTextField: true,
68
+ },
69
+ },
70
+ } as unknown as Parameters<typeof route.handler>[0])) as {
71
+ text: string;
72
+ mode: string;
73
+ };
74
+ }
75
+
76
+ afterEach(() => {
77
+ nextToolInput = null;
78
+ forcedTool = undefined;
79
+ });
80
+
81
+ describe("dictation command mode", () => {
82
+ test("answers an instruction with the edit", async () => {
83
+ nextToolInput = { kind: "edit", text: "Could you send the files over?" };
84
+
85
+ const result = await speakOverSelection("make this friendlier");
86
+
87
+ expect(forcedTool).toBe("transform_selection");
88
+ expect(result).toMatchObject({
89
+ mode: "command",
90
+ text: "Could you send the files over?",
91
+ });
92
+ });
93
+
94
+ test("answers a question with the words as they were", async () => {
95
+ nextToolInput = { kind: "answer", text: "" };
96
+
97
+ const result = await speakOverSelection("what does this mean");
98
+
99
+ expect(result).toMatchObject({
100
+ mode: "question",
101
+ text: "what does this mean",
102
+ });
103
+ });
104
+
105
+ test("an edit with nothing in it is a question", async () => {
106
+ nextToolInput = { kind: "edit", text: " " };
107
+
108
+ const result = await speakOverSelection("is this right");
109
+
110
+ expect(result.mode).toBe("question");
111
+ });
112
+
113
+ test("a model that answers in prose hands the selection back unchanged", async () => {
114
+ nextToolInput = null;
115
+
116
+ const result = await speakOverSelection("make this friendlier");
117
+
118
+ expect(result).toMatchObject({ mode: "command", text: SELECTION });
119
+ });
120
+ });