@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
@@ -0,0 +1,487 @@
1
+ /**
2
+ * Tests for POST /v1/conversations/:id/sight-frame, the HTTP door the text
3
+ * chat's camera tile persists a kept frame through.
4
+ *
5
+ * Exercises the guards the handler owns (an id that names no conversation, a
6
+ * body with no attachment), the actor the row is attributed to, and the
7
+ * persist behaviour it inherits from the live-voice path, including the one
8
+ * difference: a keep taken beside the composer is not a voice session turn.
9
+ */
10
+
11
+ import { describe, expect, mock, spyOn, test } from "bun:test";
12
+
13
+ import {
14
+ createMockProvider,
15
+ textResponse,
16
+ } from "../../../__tests__/helpers/mock-provider.js";
17
+ import { setConfig } from "../../../__tests__/helpers/set-config.js";
18
+
19
+ setConfig("memory", { enabled: false });
20
+
21
+ import { Conversation } from "../../../daemon/conversation.js";
22
+ import {
23
+ deleteConversation,
24
+ setConversation,
25
+ } from "../../../daemon/conversation-registry.js";
26
+ import { destroyActiveConversation } from "../../../daemon/conversation-store.js";
27
+ import { _setProcessingWaitMsForTests } from "../../../live-voice/live-voice-photo.js";
28
+ import {
29
+ getAttachmentById,
30
+ uploadAttachment,
31
+ } from "../../../persistence/attachments-store.js";
32
+ import {
33
+ createConversation,
34
+ deleteConversation as deleteConversationRows,
35
+ getConversation,
36
+ getMessages,
37
+ type MessageRow,
38
+ } from "../../../persistence/conversation-crud.js";
39
+ import { sightFrameAttachmentIdsFromMetadata } from "../../../persistence/conversation-types.js";
40
+ import { getDb } from "../../../persistence/db-connection.js";
41
+ import { initializeDb } from "../../../persistence/db-init.js";
42
+ import { conversations } from "../../../persistence/schema/index.js";
43
+ import { BadRequestError, NotFoundError } from "../errors.js";
44
+ import type { RouteDefinition } from "../types.js";
45
+ import * as vellumActorTrust from "../vellum-actor-trust.js";
46
+
47
+ /**
48
+ * Run something in the window the handler opens between accepting the request
49
+ * and persisting the frame.
50
+ *
51
+ * Resolving the request's actor is the only thing the handler awaits in
52
+ * between, so a hook here lands inside that window without a timer to race.
53
+ * Armed per test and cleared as it fires; every other test leaves it null and
54
+ * gets the real resolver.
55
+ */
56
+ let duringTrustResolution: (() => void) | null = null;
57
+ const realVellumActorTrust = { ...vellumActorTrust };
58
+ mock.module("../vellum-actor-trust.js", () => ({
59
+ ...realVellumActorTrust,
60
+ resolveVellumActorTrustContext: (
61
+ ...args: Parameters<
62
+ typeof realVellumActorTrust.resolveVellumActorTrustContext
63
+ >
64
+ ) => {
65
+ const hook = duringTrustResolution;
66
+ duringTrustResolution = null;
67
+ hook?.();
68
+ return realVellumActorTrust.resolveVellumActorTrustContext(...args);
69
+ },
70
+ }));
71
+
72
+ const { ROUTES: SIGHT_FRAME_ROUTES } = await import("../sight-frame-routes.js");
73
+
74
+ await initializeDb();
75
+
76
+ const IMAGE_BASE64 =
77
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk";
78
+
79
+ function findHandler(routes: RouteDefinition[], operationId: string) {
80
+ const route = routes.find((r) => r.operationId === operationId);
81
+ if (!route) {
82
+ throw new Error(`Route ${operationId} not found`);
83
+ }
84
+ return route.handler;
85
+ }
86
+
87
+ const sightFrameHandler = findHandler(
88
+ SIGHT_FRAME_ROUTES,
89
+ "conversationSightFrame",
90
+ );
91
+
92
+ interface SightFrameResponse {
93
+ persisted: boolean;
94
+ messageId?: string;
95
+ }
96
+
97
+ function persist(
98
+ id: string,
99
+ body: Record<string, unknown>,
100
+ headers?: Record<string, string>,
101
+ ): Promise<SightFrameResponse> {
102
+ return sightFrameHandler({
103
+ pathParams: { id },
104
+ body,
105
+ ...(headers ? { headers } : {}),
106
+ }) as Promise<SightFrameResponse>;
107
+ }
108
+
109
+ /** Tear the conversation down the way DELETE /v1/conversations/:id does. */
110
+ function deleteEverywhere(id: string): void {
111
+ destroyActiveConversation(id, { keepSubagentRecords: true });
112
+ deleteConversationRows(id);
113
+ }
114
+
115
+ /** A registered conversation the persist path can reach, plus its teardown. */
116
+ function liveConversation(title: string) {
117
+ const row = createConversation(title);
118
+ const { provider } = createMockProvider([textResponse("")]);
119
+ const activeConversation = new Conversation(
120
+ row.id,
121
+ provider,
122
+ "system prompt",
123
+ () => {},
124
+ "/tmp",
125
+ { maxTokens: 4096 },
126
+ );
127
+ activeConversation.setTrustContext({
128
+ trustClass: "guardian",
129
+ sourceChannel: "vellum",
130
+ });
131
+ setConversation(row.id, activeConversation);
132
+ return {
133
+ id: row.id,
134
+ activeConversation,
135
+ dispose: () => {
136
+ deleteConversation(row.id);
137
+ activeConversation.dispose();
138
+ },
139
+ };
140
+ }
141
+
142
+ function metadataOf(row: MessageRow): Record<string, unknown> {
143
+ return JSON.parse(row.metadata ?? "{}") as Record<string, unknown>;
144
+ }
145
+
146
+ function conversationCount(): number {
147
+ return getDb().select({ id: conversations.id }).from(conversations).all()
148
+ .length;
149
+ }
150
+
151
+ function uploadFrame(name: string): Promise<string> {
152
+ return uploadAttachment(name, "image/png", IMAGE_BASE64).then((a) => a.id);
153
+ }
154
+
155
+ /** True while the attachment's row is still in the store. */
156
+ function frameStored(attachmentId: string): boolean {
157
+ return getAttachmentById(attachmentId) !== null;
158
+ }
159
+
160
+ function sleep(ms: number): Promise<void> {
161
+ return new Promise((resolve) => setTimeout(resolve, ms));
162
+ }
163
+
164
+ describe("POST /v1/conversations/:id/sight-frame", () => {
165
+ test("404s for an unknown id without minting a conversation for it", async () => {
166
+ // The persist calls `getOrCreateConversation`, so an id the handler lets
167
+ // through becomes a conversation holding nothing but camera frames.
168
+ const before = conversationCount();
169
+
170
+ await expect(
171
+ persist(crypto.randomUUID(), { attachmentId: "att-1" }),
172
+ ).rejects.toThrow(NotFoundError);
173
+
174
+ expect(conversationCount()).toBe(before);
175
+ });
176
+
177
+ test("400s when the body carries no attachment", async () => {
178
+ const live = liveConversation("Chat keep bad body");
179
+ try {
180
+ await expect(persist(live.id, {})).rejects.toThrow(BadRequestError);
181
+ await expect(persist(live.id, { attachmentId: "" })).rejects.toThrow(
182
+ BadRequestError,
183
+ );
184
+ await expect(persist(live.id, { attachmentId: 7 })).rejects.toThrow(
185
+ BadRequestError,
186
+ );
187
+ expect(getMessages(live.id)).toHaveLength(0);
188
+ } finally {
189
+ live.dispose();
190
+ }
191
+ });
192
+
193
+ test("persists the frame as its own tagged row and names it", async () => {
194
+ const live = liveConversation("Chat keep happy path");
195
+ try {
196
+ const attachmentId = await uploadFrame("frame.png");
197
+
198
+ const response = await persist(live.id, { attachmentId });
199
+
200
+ const [row] = getMessages(live.id);
201
+ expect(response).toEqual({ persisted: true, messageId: row.id });
202
+ expect(row.content.filter((block) => block.type === "text")).toEqual([
203
+ { type: "text", text: "(camera frame)" },
204
+ ]);
205
+ // Retention reads the tag and the memory-privacy guard reads the pair of
206
+ // `scripted` and the tag, so a chat keep has to carry both.
207
+ expect(metadataOf(row).scripted).toBe(true);
208
+ expect(sightFrameAttachmentIdsFromMetadata(metadataOf(row))).toEqual([
209
+ attachmentId,
210
+ ]);
211
+ } finally {
212
+ live.dispose();
213
+ }
214
+ });
215
+
216
+ test("attributes the row to the verified actor, not the conversation's trust", async () => {
217
+ // The conversation rests on the trust of whoever used it last, so a row
218
+ // stamped from the slot names that actor rather than the one this request
219
+ // was verified as. Actor-scoped history reads the stamp, so the wrong one
220
+ // either hides the frame from its owner or lends guardian standing to
221
+ // content that has none.
222
+ const live = liveConversation("Chat keep actor attribution");
223
+ try {
224
+ const attachmentId = await uploadFrame("frame.png");
225
+ // A principal no guardian binding names, which the vellum-channel
226
+ // resolver fails closed to `unknown`.
227
+ const headers = { "x-vellum-actor-principal-id": "actor-not-bound" };
228
+ expect(live.activeConversation.trustContext?.trustClass).toBe("guardian");
229
+
230
+ expect(await persist(live.id, { attachmentId }, headers)).toMatchObject({
231
+ persisted: true,
232
+ });
233
+
234
+ const metadata = metadataOf(getMessages(live.id)[0]);
235
+ expect(metadata.provenanceTrustClass).toBe("unknown");
236
+ expect(metadata.provenanceSourceChannel).toBe("vellum");
237
+ } finally {
238
+ live.dispose();
239
+ }
240
+ });
241
+
242
+ test("attributes a caller with no actor header to the guardian", async () => {
243
+ // A local or IPC caller carries no actor header and is the guardian by
244
+ // construction, the same reading canonical ingress gives it.
245
+ const live = liveConversation("Chat keep local caller");
246
+ try {
247
+ const attachmentId = await uploadFrame("frame.png");
248
+
249
+ expect(await persist(live.id, { attachmentId })).toMatchObject({
250
+ persisted: true,
251
+ });
252
+
253
+ const metadata = metadataOf(getMessages(live.id)[0]);
254
+ expect(metadata.provenanceTrustClass).toBe("guardian");
255
+ expect(metadata.provenanceSourceChannel).toBe("vellum");
256
+ } finally {
257
+ live.dispose();
258
+ }
259
+ });
260
+
261
+ test("does not mark a composer keep as a voice session turn", async () => {
262
+ // The mark says a reply reaches the user over a session that is still
263
+ // open. Nothing is open here, so a reply that follows this row has to be
264
+ // pushed like any other.
265
+ const live = liveConversation("Chat keep voice mark");
266
+ try {
267
+ const attachmentId = await uploadFrame("frame.png");
268
+
269
+ expect(await persist(live.id, { attachmentId })).toMatchObject({
270
+ persisted: true,
271
+ });
272
+
273
+ expect(
274
+ metadataOf(getMessages(live.id)[0]).voiceSessionTurn,
275
+ ).toBeUndefined();
276
+ } finally {
277
+ live.dispose();
278
+ }
279
+ });
280
+
281
+ test("a frame from another actor makes the resident history reload", async () => {
282
+ // The persist stamps the ROW for the actor that sent it, but the message
283
+ // it appends to the resident history carries no scope of its own. A
284
+ // conversation resident under one actor would go on sending that array to
285
+ // the model, image included, while a reload would have filtered it out.
286
+ const live = liveConversation("Chat keep cross trust");
287
+ // Scope the resident history for the conversation's own actor.
288
+ await live.activeConversation.ensureActorScopedHistory();
289
+ const reload = spyOn(Conversation.prototype, "loadFromDb");
290
+ try {
291
+ const attachmentId = await uploadFrame("cross-trust.png");
292
+ // A principal no guardian binding names, which the vellum-channel
293
+ // resolver fails closed to `unknown`: not the class this history holds.
294
+ expect(
295
+ await persist(
296
+ live.id,
297
+ { attachmentId },
298
+ { "x-vellum-actor-principal-id": "actor-not-bound" },
299
+ ),
300
+ ).toMatchObject({ persisted: true });
301
+
302
+ await live.activeConversation.ensureActorScopedHistory();
303
+
304
+ // Reloaded and re-filtered rather than reusing the array the frame was
305
+ // appended to.
306
+ expect(reload).toHaveBeenCalled();
307
+ } finally {
308
+ reload.mockRestore();
309
+ live.dispose();
310
+ }
311
+ });
312
+
313
+ test("a frame from the resident actor does not force a reload", async () => {
314
+ // The control on the check above. Frames arrive on a camera's cadence, so
315
+ // a reload apiece is a real cost and only a differing scope earns one.
316
+ const live = liveConversation("Chat keep same trust");
317
+ await live.activeConversation.ensureActorScopedHistory();
318
+ const reload = spyOn(Conversation.prototype, "loadFromDb");
319
+ try {
320
+ const attachmentId = await uploadFrame("same-trust.png");
321
+ // No actor header, which the resolver reads as the guardian: the class
322
+ // the resident history is already scoped for.
323
+ expect(await persist(live.id, { attachmentId })).toMatchObject({
324
+ persisted: true,
325
+ });
326
+
327
+ await live.activeConversation.ensureActorScopedHistory();
328
+
329
+ expect(reload).not.toHaveBeenCalled();
330
+ } finally {
331
+ reload.mockRestore();
332
+ live.dispose();
333
+ }
334
+ });
335
+
336
+ test("reports a frame whose attachment does not resolve", async () => {
337
+ const live = liveConversation("Chat keep missing attachment");
338
+ try {
339
+ expect(await persist(live.id, { attachmentId: "att-missing" })).toEqual({
340
+ persisted: false,
341
+ });
342
+ expect(getMessages(live.id)).toHaveLength(0);
343
+ } finally {
344
+ live.dispose();
345
+ }
346
+ });
347
+
348
+ test("two overlapping keeps write one at a time", async () => {
349
+ // The processing flag is a boolean rather than a counted lock, so two
350
+ // persists in flight at once would both read it free and both take it.
351
+ const live = liveConversation("Chat keep serialized");
352
+ try {
353
+ const first = await uploadFrame("first.png");
354
+ const second = await uploadFrame("second.png");
355
+
356
+ live.activeConversation.setProcessing(true);
357
+ const firstKeep = persist(live.id, { attachmentId: first });
358
+ // Long enough for the first keep to reach the idle wait, so the second
359
+ // queues behind a job that has already begun rather than replacing it.
360
+ await sleep(30);
361
+ const secondKeep = persist(live.id, { attachmentId: second });
362
+ await sleep(30);
363
+ live.activeConversation.setProcessing(false);
364
+
365
+ expect(await firstKeep).toMatchObject({ persisted: true });
366
+ expect(await secondKeep).toMatchObject({ persisted: true });
367
+ expect(getMessages(live.id)).toHaveLength(2);
368
+ } finally {
369
+ live.dispose();
370
+ }
371
+ });
372
+
373
+ test("a newer keep replaces one still waiting, and its upload is reclaimed", async () => {
374
+ // What bounds the chain: keeps arrive every few seconds while each job can
375
+ // wait out a turn for far longer. The displaced upload is the daemon's to
376
+ // collect, because the client's own abandon-delete fires on a refused
377
+ // send and this send was accepted.
378
+ const live = liveConversation("Chat keep superseded");
379
+ try {
380
+ const running = await uploadFrame("running.png");
381
+ const stale = await uploadFrame("stale.png");
382
+ const newest = await uploadFrame("newest.png");
383
+
384
+ live.activeConversation.setProcessing(true);
385
+ const runningKeep = persist(live.id, { attachmentId: running });
386
+ await sleep(30);
387
+ const staleKeep = persist(live.id, { attachmentId: stale });
388
+ const newestKeep = persist(live.id, { attachmentId: newest });
389
+ await sleep(30);
390
+ live.activeConversation.setProcessing(false);
391
+
392
+ expect(await runningKeep).toMatchObject({ persisted: true });
393
+ expect(await staleKeep).toEqual({ persisted: false });
394
+ expect(await newestKeep).toMatchObject({ persisted: true });
395
+
396
+ expect(getMessages(live.id)).toHaveLength(2);
397
+ expect(frameStored(stale)).toBe(false);
398
+ expect(frameStored(running)).toBe(true);
399
+ expect(frameStored(newest)).toBe(true);
400
+ } finally {
401
+ live.dispose();
402
+ }
403
+ });
404
+
405
+ test("a deletion is final for a keep still queued behind another", async () => {
406
+ // The route's 404 speaks only for the moment it ran. A keep queued behind
407
+ // another one starts later, so the job re-reads the conversation before it
408
+ // touches anything: the persist reaches it through
409
+ // `getOrCreateConversation`, which writes the row back for an id it finds
410
+ // missing, and a deleted conversation must not return carrying nothing but
411
+ // camera frames.
412
+ const restoreWait = _setProcessingWaitMsForTests(150);
413
+ const live = liveConversation("Chat keep deleted mid-queue");
414
+ const holder = await uploadFrame("holder.png");
415
+ const queued = await uploadFrame("queued.png");
416
+ try {
417
+ // The first keep holds the chain by waiting out a turn that never ends,
418
+ // so the second is still queued and has touched nothing.
419
+ live.activeConversation.setProcessing(true);
420
+ const holderKeep = persist(live.id, { attachmentId: holder });
421
+ await sleep(30);
422
+ const queuedKeep = persist(live.id, { attachmentId: queued });
423
+
424
+ deleteEverywhere(live.id);
425
+ const countAfterDelete = conversationCount();
426
+
427
+ expect(await holderKeep).toEqual({ persisted: false });
428
+ expect(await queuedKeep).toEqual({ persisted: false });
429
+
430
+ expect(getConversation(live.id)).toBeNull();
431
+ expect(conversationCount()).toBe(countAfterDelete);
432
+ expect(getMessages(live.id)).toHaveLength(0);
433
+ expect(frameStored(queued)).toBe(false);
434
+ } finally {
435
+ _setProcessingWaitMsForTests(restoreWait);
436
+ }
437
+ });
438
+
439
+ test("a frame does not land in a conversation replaced while the actor resolves", async () => {
440
+ // The 404 gate is where the frame is accepted, and resolving the request's
441
+ // actor is awaited after it. A delete and recreate under this id inside
442
+ // that await leaves a row the gate never saw, and a persist that read the
443
+ // id for itself afterwards would take the replacement for the conversation
444
+ // the client addressed.
445
+ const live = liveConversation("Chat keep replaced mid-trust");
446
+ try {
447
+ const frame = await uploadFrame("mid-trust.png");
448
+
449
+ duringTrustResolution = () => {
450
+ deleteConversationRows(live.id);
451
+ createConversation({ id: live.id, title: "Recreated" });
452
+ };
453
+
454
+ expect(await persist(live.id, { attachmentId: frame })).toEqual({
455
+ persisted: false,
456
+ });
457
+
458
+ expect(getMessages(live.id)).toHaveLength(0);
459
+ // Nothing else would ever collect it: the client's abandon-delete fires
460
+ // on a refused send, and this send was accepted.
461
+ expect(frameStored(frame)).toBe(false);
462
+ } finally {
463
+ duringTrustResolution = null;
464
+ live.dispose();
465
+ }
466
+ });
467
+
468
+ test("reports a keep that outwaited the turn ahead of it", async () => {
469
+ const restoreWait = _setProcessingWaitMsForTests(120);
470
+ const live = liveConversation("Chat keep wait timeout");
471
+ try {
472
+ const frame = await uploadFrame("timed-out.png");
473
+
474
+ live.activeConversation.setProcessing(true);
475
+ expect(await persist(live.id, { attachmentId: frame })).toEqual({
476
+ persisted: false,
477
+ });
478
+
479
+ expect(frameStored(frame)).toBe(false);
480
+ expect(getMessages(live.id)).toHaveLength(0);
481
+ } finally {
482
+ live.activeConversation.setProcessing(false);
483
+ live.dispose();
484
+ _setProcessingWaitMsForTests(restoreWait);
485
+ }
486
+ });
487
+ });
@@ -204,7 +204,7 @@ function awaitRouteApproval(args: {
204
204
  broadcastMessage(confirmationMsg, conversationId);
205
205
 
206
206
  // Promote the confirmation to a guardian request so channel
207
- // guardian decisions (reactions, buttons, text) can resolve it.
207
+ // guardian decisions (buttons, text) can resolve it.
208
208
  void createGuardianRequestForConfirmation(confirmationMsg, conversationId);
209
209
  });
210
210
  }
@@ -9,10 +9,17 @@
9
9
  * they release only after that burst so the next queued message cannot start
10
10
  * processing ahead of the reply the client is still being told about.
11
11
  *
12
- * The release therefore has to be unconditional: a throw from any broadcast in
13
- * the burst would otherwise latch the conversation "processing" with no agent
14
- * loop, no `finally`, and no later path left to clear it, so every subsequent
15
- * send is queued behind an idle conversation for the life of the daemon.
12
+ * The release runs from a `finally`, because a throw from any broadcast in the
13
+ * burst would otherwise latch the conversation "processing" with no agent
14
+ * loop and no later path left to clear it, so every subsequent send is queued
15
+ * behind an idle conversation for the life of the daemon.
16
+ *
17
+ * It names the claim its scheduler took. The timer fires a tick after the
18
+ * route returned, and a Stop in between force-clears the flag and lets the
19
+ * next request acquire, so a bare clear here would release a turn that is
20
+ * running and kick the queue into it. Naming the claim makes that the no-op
21
+ * the ownership design already provides: the newer hold is somebody else's to
22
+ * release, and the follow-up work waits for whoever holds it.
16
23
  */
17
24
 
18
25
  import type { Conversation } from "../../daemon/conversation.js";
@@ -23,11 +30,13 @@ const log = getLogger("canned-reply-release");
23
30
  /** The conversation surface {@link scheduleCannedReplyRelease} touches. */
24
31
  export type CannedReplyReleaseTarget = Pick<
25
32
  Conversation,
26
- "setProcessing" | "kickDrainQueue"
33
+ "releaseProcessing" | "kickDrainQueue"
27
34
  >;
28
35
 
29
36
  export function scheduleCannedReplyRelease(params: {
30
37
  conversation: CannedReplyReleaseTarget;
38
+ /** The claim the scheduling route holds, and the only one this releases. */
39
+ owner: number;
31
40
  /** Names the release site in drain logs (e.g. `"canned_greeting"`). */
32
41
  origin: string;
33
42
  /** The deferred client event burst. Runs before the release. */
@@ -35,7 +44,7 @@ export function scheduleCannedReplyRelease(params: {
35
44
  /** Optional work that only makes sense once the lock is free. */
36
45
  afterRelease?: () => void;
37
46
  }): void {
38
- const { conversation, origin, emit, afterRelease } = params;
47
+ const { conversation, owner, origin, emit, afterRelease } = params;
39
48
  setTimeout(() => {
40
49
  try {
41
50
  emit();
@@ -46,9 +55,10 @@ export function scheduleCannedReplyRelease(params: {
46
55
  // reconcile a missed event burst on their next fetch.
47
56
  log.error({ err, origin }, "Canned reply event burst failed");
48
57
  } finally {
49
- conversation.setProcessing(false);
50
- void conversation.kickDrainQueue("loop_complete", origin);
58
+ if (conversation.releaseProcessing(owner)) {
59
+ void conversation.kickDrainQueue("loop_complete", origin);
60
+ afterRelease?.();
61
+ }
51
62
  }
52
- afterRelease?.();
53
63
  }, 0);
54
64
  }
@@ -3,7 +3,6 @@
3
3
  */
4
4
  import type { ChannelId } from "../../channels/types.js";
5
5
  import {
6
- type ApprovalAction,
7
6
  type ApprovalDecisionResult,
8
7
  type ApprovalUIMetadata,
9
8
  isApprovalAction,
@@ -58,44 +57,6 @@ export function parseCallbackData(
58
57
  return { action, source, requestId };
59
58
  }
60
59
 
61
- // ---------------------------------------------------------------------------
62
- // Reaction decision vocabulary
63
- // ---------------------------------------------------------------------------
64
-
65
- /**
66
- * Map of reaction emoji to approval actions, in each channel's own
67
- * vocabulary: Slack reactions carry colon names (with aliasing, e.g. `+1`
68
- * and `thumbsup` are both the thumbs-up emoji), Telegram and Discord carry
69
- * the unicode character itself. Telegram's fixed reaction set offers the
70
- * thumbs but not the check mark or alarm clock; Discord offers all four.
71
- */
72
- const REACTION_EMOJI_MAP: ReadonlyMap<string, ApprovalAction> = new Map([
73
- ["+1", "approve_once"],
74
- ["thumbsup", "approve_once"],
75
- ["white_check_mark", "approve_once"],
76
- ["alarm_clock", "approve_once"],
77
- ["-1", "reject"],
78
- ["thumbsdown", "reject"],
79
- ["\u{1F44D}", "approve_once"], // thumbs up
80
- ["\u2705", "approve_once"], // check mark button
81
- ["\u23F0", "approve_once"], // alarm clock
82
- ["\u{1F44E}", "reject"], // thumbs down
83
- ]);
84
-
85
- /**
86
- * Map a reaction emoji, in the source channel's own vocabulary, to an
87
- * approval decision. Returns null if the emoji is not mapped to any action.
88
- */
89
- export function reactionDecisionForEmoji(
90
- emoji: string,
91
- ): ApprovalDecisionResult | null {
92
- const action = REACTION_EMOJI_MAP.get(emoji);
93
- if (!action) {
94
- return null;
95
- }
96
- return { action, source: "reaction" };
97
- }
98
-
99
60
  // ---------------------------------------------------------------------------
100
61
  // Context builders
101
62
  // ---------------------------------------------------------------------------
@@ -28,6 +28,8 @@ import {
28
28
  createInboundChallenge,
29
29
  getVerificationStatus,
30
30
  revokeVerificationForChannel,
31
+ type VerificationStatusResponse,
32
+ verificationStatusResponseSchema,
31
33
  verifyTrustedContact,
32
34
  } from "../../daemon/handlers/config-channels.js";
33
35
  import { normalizePhoneNumber } from "../../util/phone.js";
@@ -182,7 +184,7 @@ export async function handleCreateVerificationSession({
182
184
  async function handleGetVerificationStatus({
183
185
  queryParams = {},
184
186
  body = {},
185
- }: RouteHandlerArgs) {
187
+ }: RouteHandlerArgs): Promise<VerificationStatusResponse> {
186
188
  const channel = (queryParams.channel ??
187
189
  (body as Record<string, unknown>).channel) as ChannelId | undefined;
188
190
  return await getVerificationStatus(channel);
@@ -356,6 +358,7 @@ export const ROUTES: RouteDefinition[] = [
356
358
  description: "Optional channel ID filter",
357
359
  },
358
360
  ],
361
+ responseBody: verificationStatusResponseSchema,
359
362
  handler: handleGetVerificationStatus,
360
363
  },
361
364
  ];