@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
@@ -686,28 +686,6 @@ describe("recordApprovalCardDelivery", () => {
686
686
  expect(delivery?.destinationChatId).toBeNull();
687
687
  expect(delivery?.status).toBe("pending");
688
688
  });
689
-
690
- test("lets a Slack reaction resolve back to its request (LUM-2502)", async () => {
691
- // A delivered Slack approval card must be addressable by (channel, chat, ts)
692
- // so an emoji reaction on it resolves to the right request rather than
693
- // silently falling through to transcript persistence.
694
- const req = makeRequest();
695
- await recordApprovalCardDelivery({
696
- requestId: req.id,
697
- channel: "slack",
698
- chatId: "C-guardian",
699
- messageId: "1700000000.5678",
700
- status: "sent",
701
- });
702
-
703
- const resolved =
704
- await bridgeState.module.getPendingRequestByDestinationMessageOrNull(
705
- "slack",
706
- "C-guardian",
707
- "1700000000.5678",
708
- );
709
- expect(resolved?.id).toBe(req.id);
710
- });
711
689
  });
712
690
 
713
691
  describe("recordGuardianRequestDeliveries", () => {
@@ -454,25 +454,6 @@ export function createGuardianGatewaySim() {
454
454
  .map((d) => ({ ...d }));
455
455
  }
456
456
 
457
- async function getPendingRequestByDestinationMessage(
458
- channel: string,
459
- chatId: string,
460
- messageId: string,
461
- ): Promise<SimGuardianRequest | null> {
462
- throwIfReadError();
463
- const delivery = deliveries.find(
464
- (d) =>
465
- d.destinationChannel === channel &&
466
- d.destinationChatId === chatId &&
467
- d.destinationMessageId === messageId,
468
- );
469
- if (!delivery) {
470
- return null;
471
- }
472
- const request = requests.get(delivery.requestId);
473
- return request?.status === "pending" ? { ...request } : null;
474
- }
475
-
476
457
  async function listPendingRequestsByDestination(params: {
477
458
  channel?: string;
478
459
  chatId?: string;
@@ -624,10 +605,6 @@ export function createGuardianGatewaySim() {
624
605
  listGuardianRequestDeliveries,
625
606
  [],
626
607
  ),
627
- getPendingRequestByDestinationMessageOrNull: degrade(
628
- getPendingRequestByDestinationMessage,
629
- null,
630
- ),
631
608
  listPendingRequestsByDestinationOrEmpty: degrade(
632
609
  listPendingRequestsByDestination,
633
610
  [],
@@ -1,12 +1,14 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import {
4
+ applyGuardianReplyMechanics,
4
5
  buildGuardianCodeOnlyClarification,
5
6
  buildGuardianDisambiguationExample,
6
7
  buildGuardianDisambiguationLabel,
7
8
  buildGuardianInvalidActionReply,
8
9
  buildGuardianReplyDirective,
9
10
  buildGuardianRequestCodeInstruction,
11
+ guardianCopyCarriesReplyMechanics,
10
12
  hasGuardianRequestCodeInstruction,
11
13
  parseGuardianQuestionPayload,
12
14
  parseInteractiveApprovalPayload,
@@ -286,6 +288,112 @@ describe("guardian-question-mode", () => {
286
288
  ).not.toBeNull();
287
289
  });
288
290
 
291
+ test("guardianCopyCarriesReplyMechanics: chat channels carry them, card surfaces do not", () => {
292
+ const approval = {
293
+ requestId: "req-1",
294
+ requestCode: "A1B2C3",
295
+ requestKind: "tool_grant_request",
296
+ questionText: "Allow bash?",
297
+ toolName: "bash",
298
+ };
299
+ const question = {
300
+ requestId: "req-2",
301
+ requestCode: "A1B2C3",
302
+ requestKind: "pending_question",
303
+ questionText: "Which one?",
304
+ };
305
+ // The bell, the banner, and the push act through the card whatever the mode.
306
+ expect(guardianCopyCarriesReplyMechanics("vellum", approval)).toBe(false);
307
+ expect(guardianCopyCarriesReplyMechanics("vellum", question)).toBe(false);
308
+ expect(guardianCopyCarriesReplyMechanics("platform", question)).toBe(false);
309
+ // Slack draws buttons for approvals only; a question is answered by text.
310
+ expect(guardianCopyCarriesReplyMechanics("slack", approval)).toBe(false);
311
+ expect(guardianCopyCarriesReplyMechanics("slack", question)).toBe(true);
312
+ expect(guardianCopyCarriesReplyMechanics("telegram", approval)).toBe(true);
313
+ expect(guardianCopyCarriesReplyMechanics("telegram", question)).toBe(true);
314
+ });
315
+
316
+ test("applyGuardianReplyMechanics enforces into chat copy, strips from card-surface copy, and is idempotent", () => {
317
+ const signal = {
318
+ sourceEventName: "guardian.question",
319
+ contextPayload: {
320
+ requestId: "req-2",
321
+ requestCode: "a1b2c3",
322
+ requestKind: "pending_question",
323
+ questionText: "Which one?",
324
+ },
325
+ } as const;
326
+ const copy = {
327
+ title: "Question",
328
+ body: "Which one?\n\n1. Left\n2. Right",
329
+ conversationSeedMessage: "Which one?\n\n1. Left\n2. Right",
330
+ };
331
+ const instruction = 'Reference code: A1B2C3. Reply "A1B2C3 <your answer>".';
332
+
333
+ const telegram = applyGuardianReplyMechanics(copy, "telegram", signal);
334
+ expect(telegram.body).toBe(`${copy.body}\n\n${instruction}`);
335
+ expect(telegram.conversationSeedMessage).toBe(
336
+ `${copy.body}\n\n${instruction}`,
337
+ );
338
+ expect(applyGuardianReplyMechanics(telegram, "telegram", signal)).toEqual(
339
+ telegram,
340
+ );
341
+
342
+ const vellum = applyGuardianReplyMechanics(telegram, "vellum", signal);
343
+ expect(vellum).toEqual(copy);
344
+ expect(applyGuardianReplyMechanics(copy, "vellum", signal)).toEqual(copy);
345
+
346
+ // Any other signal passes through untouched.
347
+ expect(
348
+ applyGuardianReplyMechanics(copy, "telegram", {
349
+ ...signal,
350
+ sourceEventName: "schedule.notify",
351
+ }),
352
+ ).toEqual(copy);
353
+ });
354
+
355
+ test("applyGuardianReplyMechanics replaces an instruction-only field with the question text on a card surface", () => {
356
+ const signal = {
357
+ sourceEventName: "guardian.question",
358
+ contextPayload: {
359
+ requestId: "req-3",
360
+ requestCode: "A1B2C3",
361
+ requestKind: "tool_grant_request",
362
+ questionText: "Allow bash to run ls /tmp?",
363
+ toolName: "bash",
364
+ },
365
+ } as const;
366
+ const instructionOnly = {
367
+ title: "Tool Grant Request. Approval code: A1B2C3",
368
+ body: 'Reference code: A1B2C3. Reply "A1B2C3 approve" or "A1B2C3 reject".',
369
+ deliveryText: "Approval code: A1B2C3",
370
+ };
371
+ const vellum = applyGuardianReplyMechanics(
372
+ instructionOnly,
373
+ "vellum",
374
+ signal,
375
+ );
376
+ expect(vellum.title).toBe("Tool Grant Request.");
377
+ expect(vellum.body).toBe("Allow bash to run ls /tmp?");
378
+ expect(vellum.deliveryText).toBe("Allow bash to run ls /tmp?");
379
+ // A title is a headline, so a code-only one keeps its text.
380
+ expect(
381
+ applyGuardianReplyMechanics(
382
+ { ...instructionOnly, title: "Approval code: A1B2C3" },
383
+ "vellum",
384
+ signal,
385
+ ).title,
386
+ ).toBe("Approval code: A1B2C3");
387
+
388
+ // With no question text to fall back on, the field keeps its text rather
389
+ // than becoming empty copy the broadcaster would skip.
390
+ const bare = applyGuardianReplyMechanics(instructionOnly, "vellum", {
391
+ ...signal,
392
+ contextPayload: { ...signal.contextPayload, questionText: "" },
393
+ });
394
+ expect(bare.body).toBe(instructionOnly.body);
395
+ });
396
+
289
397
  test("parseInteractiveApprovalPayload rejects answer-mode and unparseable payloads", () => {
290
398
  // Answer mode: free-text questions get no Approve/Reject buttons.
291
399
  expect(
@@ -44,7 +44,6 @@ mock.module("../channels/gateway-guardian-requests.js", () => ({
44
44
  getGuardianRequestOrNull: (id: string) =>
45
45
  Promise.resolve(requestsById.get(id) ?? null),
46
46
  getGuardianRequestByCodeOrNull: () => Promise.resolve(null),
47
- getPendingRequestByDestinationMessageOrNull: () => Promise.resolve(null),
48
47
  listGuardianRequestsOrEmpty: () => Promise.resolve(pendingList),
49
48
  }));
50
49
 
@@ -91,7 +90,6 @@ function makeRequest(
91
90
  function makeContext(overrides: Record<string, unknown> = {}) {
92
91
  return {
93
92
  messageText: "",
94
- channel: "telegram",
95
93
  actor: {
96
94
  actorPrincipalId: "prin-guardian",
97
95
  actorExternalUserId: "tg-guardian",
@@ -106,7 +106,6 @@ function replyCtx(
106
106
  ): GuardianReplyContext {
107
107
  return {
108
108
  messageText: "",
109
- channel: "telegram",
110
109
  actor: guardianActor(),
111
110
  conversationId: "conv-test",
112
111
  ...overrides,
@@ -1509,7 +1508,6 @@ describe("routing invariant: destination hints do not bypass tool_approval princ
1509
1508
  const result = await routeGuardianReply(
1510
1509
  replyCtx({
1511
1510
  messageText: "approve",
1512
- channel: "telegram",
1513
1511
  actor: guardianActor({ guardianPrincipalId: "different-principal" }),
1514
1512
  conversationId: "conv-guardian-chat",
1515
1513
  pendingScope: { mode: "scoped", requestIds: [req.id] },
@@ -1544,7 +1542,6 @@ describe("routing invariant: destination hints do not bypass tool_approval princ
1544
1542
  const result = await routeGuardianReply(
1545
1543
  replyCtx({
1546
1544
  messageText: "approve",
1547
- channel: "telegram",
1548
1545
  actor: guardianActor({ actorExternalUserId: "guardian-tg-user" }),
1549
1546
  conversationId: "conv-guardian-chat",
1550
1547
  // pendingScope omitted — no delivery hints
@@ -1704,7 +1701,6 @@ describe("routing invariant: invite handoff bypass for access requests", () => {
1704
1701
 
1705
1702
  const result = await routeGuardianReply({
1706
1703
  messageText: "C0D3A5 approve",
1707
- channel: "vellum",
1708
1704
  actor: trustedActor({ channel: "vellum" }),
1709
1705
  conversationId: "conv-guardian-conversation",
1710
1706
  pendingScope: { mode: "scoped", requestIds: [req.id] },
@@ -1743,7 +1739,6 @@ describe("routing invariant: invite handoff bypass for access requests", () => {
1743
1739
 
1744
1740
  const result = await routeGuardianReply({
1745
1741
  messageText: "please approve this request",
1746
- channel: "vellum",
1747
1742
  actor: trustedActor({ channel: "vellum" }),
1748
1743
  conversationId: "conv-guardian-conversation",
1749
1744
  pendingScope: { mode: "scoped", requestIds: [req.id] },
@@ -1907,16 +1902,35 @@ describe("routing invariant: kind-specific action sets in prompt mapping", () =>
1907
1902
  expect(actions[1].action).toBe("reject");
1908
1903
  });
1909
1904
 
1910
- test("source-code invariant: guardian-action-routes.ts contains kind guard", () => {
1911
- const srcRoot = resolve(__dirname, "..");
1912
- const fullPath = join(srcRoot, "runtime/routes/guardian-action-routes.ts");
1913
- const source = readFileSync(fullPath, "utf-8");
1914
- expect(source).toContain('req.kind === "access_request"');
1915
- });
1916
-
1917
1905
  // Integration tests: verify listGuardianDecisionPrompts returns correct
1918
1906
  // action sets for each guardian request kind.
1919
1907
 
1908
+ test("access_request prompt text carries the invite-flow sentence and no decision directive", async () => {
1909
+ const convId = "conv-kind-access-request";
1910
+ sim.seedRequest({
1911
+ kind: "access_request",
1912
+ sourceType: "channel",
1913
+ sourceConversationId: convId,
1914
+ guardianExternalUserId: "guardian-1",
1915
+ guardianPrincipalId: TEST_PRINCIPAL_ID,
1916
+ toolName: "ingress_access_request",
1917
+ questionText: "Alice is requesting access to the assistant.",
1918
+ requestCode: "A1B2C3",
1919
+ expiresAt: Date.now() + 60_000,
1920
+ });
1921
+
1922
+ const prompts = await listGuardianDecisionPrompts({
1923
+ conversationId: convId,
1924
+ });
1925
+ expect(prompts).toHaveLength(1);
1926
+ // The actions carry the decision; the invite flow has no action anywhere,
1927
+ // so its sentence rides in the text.
1928
+ expect(prompts[0]!.questionText).toBe(
1929
+ 'Alice is requesting access to the assistant.\nReply "open invite flow" to start Trusted Contacts invite flow.',
1930
+ );
1931
+ expect(prompts[0]!.questionText).not.toContain("A1B2C3");
1932
+ });
1933
+
1920
1934
  test("tool_approval prompt uses approve_once + reject only (one-time decision pattern)", async () => {
1921
1935
  const convId = "conv-kind-tool-approval";
1922
1936
  sim.seedRequest({
@@ -17,6 +17,7 @@ describe("supportsHostProxy (runtime)", () => {
17
17
  test("no-arg form returns true for host-proxy interfaces", () => {
18
18
  expect(supportsHostProxy("macos")).toBe(true);
19
19
  expect(supportsHostProxy("windows")).toBe(true);
20
+ expect(supportsHostProxy("linux")).toBe(true);
20
21
  });
21
22
 
22
23
  test("no-arg form returns false for interfaces without host-proxy support", () => {
@@ -59,6 +60,15 @@ describe("supportsHostProxy (runtime)", () => {
59
60
  expect(supportsHostProxy("windows", "host_app_control")).toBe(false);
60
61
  });
61
62
 
63
+ test("capability form withholds unimplemented Linux app control", () => {
64
+ expect(supportsHostProxy("linux", "host_bash")).toBe(true);
65
+ expect(supportsHostProxy("linux", "host_file")).toBe(true);
66
+ expect(supportsHostProxy("linux", "host_cu")).toBe(true);
67
+ expect(supportsHostProxy("linux", "host_browser")).toBe(true);
68
+ expect(supportsHostProxy("linux", "host_ui_snapshot")).toBe(true);
69
+ expect(supportsHostProxy("linux", "host_app_control")).toBe(false);
70
+ });
71
+
62
72
  test("capability form rejects everything for non-host-proxy interfaces", () => {
63
73
  expect(supportsHostProxy("ios", "host_bash")).toBe(false);
64
74
  expect(supportsHostProxy("cli", "host_file")).toBe(false);
@@ -103,6 +103,7 @@ function createTestContext(
103
103
  : null;
104
104
 
105
105
  let processing = false;
106
+ let owner = 0;
106
107
  return {
107
108
  conversationId: "conv-test",
108
109
  messages: [],
@@ -110,6 +111,21 @@ function createTestContext(
110
111
  setProcessing: (value: boolean) => {
111
112
  processing = value;
112
113
  },
114
+ acquireProcessingFenced: async () => {
115
+ if (processing) {
116
+ return null;
117
+ }
118
+ processing = true;
119
+ owner += 1;
120
+ return owner;
121
+ },
122
+ releaseProcessing: (claim: number) => {
123
+ if (claim !== owner) {
124
+ return false;
125
+ }
126
+ processing = false;
127
+ return true;
128
+ },
113
129
  abortController: null,
114
130
  queue: queueStub,
115
131
  getTurnChannelContext: () => turnChannel,
@@ -10,10 +10,11 @@
10
10
  * pre-cutover v2 blocks both ride the cached prefix); the old whole-layer
11
11
  * strip is gone.
12
12
  *
13
- * The ephemeral `<memory_spotlight>` block is strip-and-replaced every turn:
14
- * stale spotlights are removed from every user message unconditionally (a
15
- * scoped, single-id strip), and the spotlight injector's `append-user-tail`
16
- * block lands at the tail.
13
+ * Leftover `<memory_spotlight>` blocks are stripped from the TAIL user
14
+ * message only (mid-turn re-entry / post-compact must not double-stack).
15
+ * Historical user messages keep the spotlight they were sent with. The
16
+ * spotlight injector's `after-memory-prefix` block is spliced onto the
17
+ * current tail and captured on `blocks`.
17
18
  *
18
19
  * v2 suppression stays keyed off whether v3 produced a block, NOT off the
19
20
  * gate alone: a v3 failure (`produce()` → null) leaves v2's block intact
@@ -128,7 +129,7 @@ function v3Injector(inner: string | null, commit?: () => void): Injector {
128
129
  };
129
130
  }
130
131
 
131
- /** A fake v3 spotlight injector mirroring the real id + tail placement. */
132
+ /** A fake v3 spotlight injector mirroring the real id + placement. */
132
133
  function spotlightInjector(inner: string): Injector {
133
134
  return {
134
135
  name: "memory-v3-spotlight",
@@ -137,7 +138,7 @@ function spotlightInjector(inner: string): Injector {
137
138
  return {
138
139
  id: "memory-v3-spotlight",
139
140
  text: wrapMemorySpotlightBlock(inner),
140
- placement: "append-user-tail",
141
+ placement: "after-memory-prefix",
141
142
  };
142
143
  },
143
144
  };
@@ -242,7 +243,7 @@ describe("memory-v3-live v2 suppression", () => {
242
243
  expect(result.blocks.memoryV3Active).toBe(true);
243
244
  });
244
245
 
245
- test("stale spotlight blocks are stripped from EVERY user message; the new spotlight lands at the tail", async () => {
246
+ test("historical leftover spotlight stays; the fresh spotlight splices onto the tail", async () => {
246
247
  memoryV3LiveSlot = true;
247
248
  injectorChainSlot.push(v3Injector(""), spotlightInjector("fresh sections"));
248
249
 
@@ -266,16 +267,49 @@ describe("memory-v3-live v2 suppression", () => {
266
267
  ...makeTurnContext(),
267
268
  });
268
269
 
269
- // The stale spotlight is gone from the historical turn…
270
+ // Historical turn keeps the spotlight it was sent with.
270
271
  expect(result.messages[0].content).toEqual([
271
272
  { type: "text", text: "earlier question" },
273
+ staleSpotlight,
272
274
  ]);
273
- // …and exactly one fresh spotlight sits at the tail.
275
+ // Fresh spotlight splices onto the tail (no memory prefix, so index 0)
276
+ // and is captured for metadata persist.
274
277
  const texts = tailTexts(result.messages);
275
278
  expect(texts).toEqual([
279
+ wrapMemorySpotlightBlock("fresh sections"),
276
280
  "current question",
281
+ ]);
282
+ expect(result.blocks.memoryV3SpotlightBlock).toBe(
277
283
  wrapMemorySpotlightBlock("fresh sections"),
284
+ );
285
+ });
286
+
287
+ test("tail leftover spotlight is stripped before the fresh spotlight splices", async () => {
288
+ memoryV3LiveSlot = true;
289
+ injectorChainSlot.push(v3Injector(""), spotlightInjector("fresh sections"));
290
+
291
+ const staleOnTail = wrapMemorySpotlightBlock("stale sections");
292
+ const runMessages: Message[] = [
293
+ {
294
+ role: "user",
295
+ content: [
296
+ { type: "text", text: staleOnTail },
297
+ { type: "text", text: "current question" },
298
+ ],
299
+ },
300
+ ];
301
+
302
+ const result = await applyRuntimeInjections(runMessages, {
303
+ ...makeTurnContext(),
304
+ });
305
+
306
+ expect(tailTexts(result.messages)).toEqual([
307
+ wrapMemorySpotlightBlock("fresh sections"),
308
+ "current question",
278
309
  ]);
310
+ expect(result.blocks.memoryV3SpotlightBlock).toBe(
311
+ wrapMemorySpotlightBlock("fresh sections"),
312
+ );
279
313
  });
280
314
 
281
315
  test("convergence re-entry: a tail leading with this turn's frozen v3 cards (and <info>) is NOT stripped", async () => {
@@ -491,6 +491,133 @@ describe("handleListMessages page=latest", () => {
491
491
  });
492
492
  });
493
493
 
494
+ test("an assistant row still carrying the partial pre-send envelope projects no slackMessage", async () => {
495
+ // The row an outbound Slack reply is reserved with names no id of its own:
496
+ // `channelTs` is back-filled by the post-send reconciliation. Until that
497
+ // lands, the strict reader rejects the envelope and the row projects
498
+ // nothing, so a client cannot attribute it to Slack. This is the silent
499
+ // half of the reconciliation gap, pinned so a reader that starts
500
+ // tolerating the partial form has to justify it here.
501
+ const conv = createConversation();
502
+ const db = getDb();
503
+ db.insert(messages)
504
+ .values({
505
+ id: "msg-slack-partial",
506
+ conversationId: conv.id,
507
+ role: "assistant",
508
+ content: JSON.stringify([{ type: "text", text: "Posted to Slack" }]),
509
+ metadata: JSON.stringify({
510
+ userMessageChannel: "slack",
511
+ assistantMessageChannel: "slack",
512
+ slackMeta: JSON.stringify({
513
+ source: "slack",
514
+ eventKind: "message",
515
+ channelId: "C123ABCDEF",
516
+ threadTs: "1710000000.000100",
517
+ timestampTimezone: "America/New_York",
518
+ timestampTimezoneLabel: "ET",
519
+ }),
520
+ }),
521
+ createdAt: 1,
522
+ })
523
+ .run();
524
+
525
+ const body = await callList({ conversationId: conv.id, page: "latest" });
526
+
527
+ expect(body.messages[0].slackMessage).toBeUndefined();
528
+ });
529
+
530
+ test("a reconciled assistant row on the neutral envelope projects the same slackMessage", async () => {
531
+ // The row a Slack reply is written with today: the neutral envelope,
532
+ // Slack's own fields on its passthrough, `messageId` back-filled by the
533
+ // post-send reconciliation. The wire projection reads it through the
534
+ // envelope's Slack view, so the client sees exactly what a legacy
535
+ // `slackMeta` row projects.
536
+ const conv = createConversation();
537
+ const db = getDb();
538
+ db.insert(messages)
539
+ .values({
540
+ id: "msg-slack-neutral",
541
+ conversationId: conv.id,
542
+ role: "assistant",
543
+ content: JSON.stringify([{ type: "text", text: "Posted to Slack" }]),
544
+ metadata: JSON.stringify({
545
+ userMessageChannel: "slack",
546
+ assistantMessageChannel: "slack",
547
+ providerMeta: JSON.stringify({
548
+ source: "slack",
549
+ conversationExternalId: "C123ABCDEF",
550
+ messageId: "1710000000.000200",
551
+ threadId: "1710000000.000100",
552
+ eventKind: "message",
553
+ timestampTimezone: "America/New_York",
554
+ timestampTimezoneLabel: "ET",
555
+ }),
556
+ }),
557
+ createdAt: 1,
558
+ })
559
+ .run();
560
+
561
+ const body = await callList({ conversationId: conv.id, page: "latest" });
562
+
563
+ const slackMessage = body.messages[0].slackMessage;
564
+ expect(slackMessage).toBeDefined();
565
+ expect(slackMessage?.channelId).toBe("C123ABCDEF");
566
+ expect(slackMessage?.channelTs).toBe("1710000000.000200");
567
+ expect(slackMessage?.threadTs).toBe("1710000000.000100");
568
+ expect(slackMessage?.eventKind).toBe("message");
569
+ expect(slackMessage?.messageLink).toBeDefined();
570
+ });
571
+
572
+ test("the reconciled assistant row projects slackMessage with the post's own ts", async () => {
573
+ // The other half of the same chain: once reconciliation stamps the ts the
574
+ // transport returned, the identical row projects a full envelope. Built by
575
+ // running the production reconciliation merge over the partial form rather
576
+ // than hand-writing the result, so the two stay in step.
577
+ const conv = createConversation();
578
+ const partial = {
579
+ source: "slack",
580
+ eventKind: "message" as const,
581
+ channelId: "C123ABCDEF",
582
+ threadTs: "1710000000.000100",
583
+ timestampTimezone: "America/New_York",
584
+ timestampTimezoneLabel: "ET",
585
+ };
586
+ const reconciled = JSON.stringify({
587
+ ...partial,
588
+ channelTs: "1710000000.000200",
589
+ source: "slack",
590
+ });
591
+
592
+ const db = getDb();
593
+ db.insert(messages)
594
+ .values({
595
+ id: "msg-slack-reconciled",
596
+ conversationId: conv.id,
597
+ role: "assistant",
598
+ content: JSON.stringify([{ type: "text", text: "Posted to Slack" }]),
599
+ metadata: JSON.stringify({
600
+ userMessageChannel: "slack",
601
+ assistantMessageChannel: "slack",
602
+ slackMeta: reconciled,
603
+ }),
604
+ createdAt: 1,
605
+ })
606
+ .run();
607
+
608
+ const body = await callList({ conversationId: conv.id, page: "latest" });
609
+
610
+ const slackMessage = body.messages[0].slackMessage;
611
+ expect(slackMessage).toBeDefined();
612
+ expect(slackMessage?.channelId).toBe("C123ABCDEF");
613
+ expect(slackMessage?.channelTs).toBe("1710000000.000200");
614
+ expect(slackMessage?.threadTs).toBe("1710000000.000100");
615
+ expect(slackMessage?.eventKind).toBe("message");
616
+ // The deep link is what the drawer's "open the source" action needs, and
617
+ // it is derivable only from the reconciled ts.
618
+ expect(slackMessage?.messageLink).toBeDefined();
619
+ });
620
+
494
621
  test("top-level Slack messages with matching threadTs use plain permalinks", async () => {
495
622
  const conv = createConversation();
496
623
  const db = getDb();