@vellumai/assistant 0.3.4 → 0.3.6

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 (506) hide show
  1. package/Dockerfile +2 -0
  2. package/README.md +88 -2
  3. package/eslint.config.mjs +31 -0
  4. package/package.json +1 -1
  5. package/scripts/ipc/check-swift-decoder-drift.ts +4 -1
  6. package/scripts/ipc/generate-swift.ts +31 -2
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +438 -1
  8. package/src/__tests__/approval-conversation-turn.test.ts +214 -0
  9. package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
  10. package/src/__tests__/approval-message-composer.test.ts +253 -0
  11. package/src/__tests__/browser-manager.test.ts +1 -0
  12. package/src/__tests__/call-conversation-messages.test.ts +130 -0
  13. package/src/__tests__/call-domain.test.ts +12 -2
  14. package/src/__tests__/call-orchestrator.test.ts +799 -249
  15. package/src/__tests__/call-pointer-messages.test.ts +148 -0
  16. package/src/__tests__/call-recovery.test.ts +3 -0
  17. package/src/__tests__/call-routes-http.test.ts +32 -2
  18. package/src/__tests__/call-store.test.ts +3 -0
  19. package/src/__tests__/channel-approval-routes.test.ts +1277 -98
  20. package/src/__tests__/channel-approval.test.ts +37 -0
  21. package/src/__tests__/channel-approvals.test.ts +36 -50
  22. package/src/__tests__/channel-guardian.test.ts +630 -22
  23. package/src/__tests__/channel-readiness-service.test.ts +324 -0
  24. package/src/__tests__/checker.test.ts +14 -7
  25. package/src/__tests__/clarification-resolver.test.ts +44 -24
  26. package/src/__tests__/commit-message-enrichment-service.test.ts +9 -4
  27. package/src/__tests__/computer-use-session-working-dir.test.ts +8 -0
  28. package/src/__tests__/config-schema.test.ts +14 -8
  29. package/src/__tests__/context-window-manager.test.ts +30 -2
  30. package/src/__tests__/contradiction-checker.test.ts +20 -5
  31. package/src/__tests__/credential-security-invariants.test.ts +7 -2
  32. package/src/__tests__/daemon-lifecycle.test.ts +13 -12
  33. package/src/__tests__/db-migration-rollback.test.ts +752 -0
  34. package/src/__tests__/dictation-mode-detection.test.ts +63 -0
  35. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +2 -0
  36. package/src/__tests__/entity-search.test.ts +615 -0
  37. package/src/__tests__/fuzzy-match-property.test.ts +5 -5
  38. package/src/__tests__/guardian-action-store.test.ts +123 -0
  39. package/src/__tests__/guardian-action-sweep.test.ts +277 -0
  40. package/src/__tests__/guardian-dispatch.test.ts +389 -0
  41. package/src/__tests__/guardian-question-copy.test.ts +47 -0
  42. package/src/__tests__/handlers-telegram-config.test.ts +4 -2
  43. package/src/__tests__/handlers-twilio-config.test.ts +533 -0
  44. package/src/__tests__/intent-routing.test.ts +2 -0
  45. package/src/__tests__/ipc-snapshot.test.ts +291 -1
  46. package/src/__tests__/memory-upsert-concurrency.test.ts +828 -0
  47. package/src/__tests__/messaging-send-tool.test.ts +65 -0
  48. package/src/__tests__/model-intents.test.ts +96 -0
  49. package/src/__tests__/no-direct-anthropic-sdk-imports.test.ts +42 -0
  50. package/src/__tests__/oauth2-gateway-transport.test.ts +130 -0
  51. package/src/__tests__/onboarding-starter-tasks.test.ts +2 -0
  52. package/src/__tests__/provider-commit-message-generator.test.ts +89 -13
  53. package/src/__tests__/provider-error-scenarios.test.ts +621 -0
  54. package/src/__tests__/provider-fail-open-selection.test.ts +119 -0
  55. package/src/__tests__/qdrant-manager.test.ts +27 -20
  56. package/src/__tests__/relay-server.test.ts +779 -40
  57. package/src/__tests__/run-orchestrator-assistant-events.test.ts +6 -0
  58. package/src/__tests__/run-orchestrator.test.ts +42 -4
  59. package/src/__tests__/runtime-runs-http.test.ts +17 -1
  60. package/src/__tests__/runtime-runs.test.ts +16 -0
  61. package/src/__tests__/schedule-store.test.ts +18 -4
  62. package/src/__tests__/scheduler-recurrence.test.ts +13 -4
  63. package/src/__tests__/session-abort-tool-results.test.ts +6 -0
  64. package/src/__tests__/session-agent-loop.test.ts +857 -0
  65. package/src/__tests__/session-conflict-gate.test.ts +6 -0
  66. package/src/__tests__/session-pre-run-repair.test.ts +6 -0
  67. package/src/__tests__/session-profile-injection.test.ts +6 -0
  68. package/src/__tests__/session-provider-retry-repair.test.ts +6 -0
  69. package/src/__tests__/session-queue.test.ts +6 -0
  70. package/src/__tests__/session-runtime-assembly.test.ts +321 -13
  71. package/src/__tests__/session-slash-known.test.ts +6 -0
  72. package/src/__tests__/session-slash-queue.test.ts +6 -0
  73. package/src/__tests__/session-slash-unknown.test.ts +6 -0
  74. package/src/__tests__/session-surfaces-task-progress.test.ts +2 -0
  75. package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
  76. package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
  77. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
  78. package/src/__tests__/session-workspace-injection.test.ts +6 -0
  79. package/src/__tests__/session-workspace-tool-tracking.test.ts +6 -0
  80. package/src/__tests__/skills.test.ts +2 -0
  81. package/src/__tests__/sms-messaging-provider.test.ts +126 -0
  82. package/src/__tests__/starter-task-flow.test.ts +2 -0
  83. package/src/__tests__/swarm-dag-pathological.test.ts +535 -0
  84. package/src/__tests__/system-prompt.test.ts +2 -0
  85. package/src/__tests__/task-management-tools.test.ts +2 -2
  86. package/src/__tests__/task-runner.test.ts +14 -4
  87. package/src/__tests__/terminal-tools.test.ts +25 -19
  88. package/src/__tests__/tool-execution-abort-cleanup.test.ts +545 -0
  89. package/src/__tests__/tool-executor-shell-integration.test.ts +11 -11
  90. package/src/__tests__/tool-executor.test.ts +23 -24
  91. package/src/__tests__/trust-store.test.ts +3 -3
  92. package/src/__tests__/twilio-rest.test.ts +29 -0
  93. package/src/__tests__/twilio-routes-elevenlabs.test.ts +3 -0
  94. package/src/__tests__/twilio-routes-twiml.test.ts +11 -0
  95. package/src/__tests__/twilio-routes.test.ts +167 -11
  96. package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
  97. package/src/__tests__/user-reference.test.ts +2 -0
  98. package/src/__tests__/voice-quality.test.ts +222 -0
  99. package/src/__tests__/web-search.test.ts +46 -30
  100. package/src/__tests__/work-item-output.test.ts +110 -0
  101. package/src/agent/loop.ts +1 -1
  102. package/src/agent-heartbeat/agent-heartbeat-service.ts +2 -10
  103. package/src/amazon/client.ts +1418 -0
  104. package/src/amazon/request-extractor.ts +135 -0
  105. package/src/amazon/session.ts +109 -0
  106. package/src/autonomy/autonomy-store.ts +5 -5
  107. package/src/browser-extension-relay/client.ts +124 -0
  108. package/src/browser-extension-relay/protocol.ts +63 -0
  109. package/src/browser-extension-relay/server.ts +177 -0
  110. package/src/bundler/app-bundler.ts +3 -3
  111. package/src/bundler/bundle-signer.ts +1 -1
  112. package/src/bundler/signature-verifier.ts +1 -1
  113. package/src/calls/call-conversation-messages.ts +33 -0
  114. package/src/calls/call-domain.ts +114 -10
  115. package/src/calls/call-orchestrator.ts +268 -59
  116. package/src/calls/call-pointer-messages.ts +53 -0
  117. package/src/calls/call-recovery.ts +3 -8
  118. package/src/calls/call-store.ts +69 -87
  119. package/src/calls/elevenlabs-config.ts +3 -2
  120. package/src/calls/guardian-action-sweep.ts +105 -0
  121. package/src/calls/guardian-dispatch.ts +203 -0
  122. package/src/calls/guardian-question-copy.ts +133 -0
  123. package/src/calls/relay-server.ts +466 -8
  124. package/src/calls/speaker-identification.ts +1 -1
  125. package/src/calls/twilio-config.ts +22 -14
  126. package/src/calls/twilio-provider.ts +6 -4
  127. package/src/calls/twilio-rest.ts +308 -7
  128. package/src/calls/twilio-routes.ts +65 -12
  129. package/src/calls/types.ts +3 -1
  130. package/src/channels/types.ts +25 -0
  131. package/src/cli/amazon.ts +815 -0
  132. package/src/cli/config-commands.ts +2 -2
  133. package/src/cli/core-commands.ts +4 -3
  134. package/src/cli/influencer.ts +244 -0
  135. package/src/cli/map.ts +89 -6
  136. package/src/cli.ts +1 -1
  137. package/src/config/agent-schema.ts +171 -0
  138. package/src/config/bundled-skills/amazon/SKILL.md +127 -0
  139. package/src/config/bundled-skills/amazon/icon.svg +13 -0
  140. package/src/config/bundled-skills/api-mapping/SKILL.md +78 -0
  141. package/src/config/bundled-skills/browser/SKILL.md +1 -0
  142. package/src/config/bundled-skills/browser/TOOLS.json +17 -0
  143. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +25 -0
  144. package/src/config/bundled-skills/doordash/SKILL.md +51 -51
  145. package/src/config/bundled-skills/email-setup/SKILL.md +14 -5
  146. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +183 -0
  147. package/src/config/bundled-skills/influencer/SKILL.md +144 -0
  148. package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
  149. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
  150. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
  151. package/src/config/bundled-skills/macos-automation/icon.svg +12 -0
  152. package/src/config/bundled-skills/media-processing/SKILL.md +176 -0
  153. package/src/config/bundled-skills/media-processing/TOOLS.json +230 -0
  154. package/src/config/bundled-skills/media-processing/__tests__/concurrency-pool.test.ts +77 -0
  155. package/src/config/bundled-skills/media-processing/__tests__/cost-tracker.test.ts +69 -0
  156. package/src/config/bundled-skills/media-processing/__tests__/preprocess.test.ts +303 -0
  157. package/src/config/bundled-skills/media-processing/services/concurrency-pool.ts +55 -0
  158. package/src/config/bundled-skills/media-processing/services/cost-tracker.ts +86 -0
  159. package/src/config/bundled-skills/media-processing/services/gemini-map.ts +339 -0
  160. package/src/config/bundled-skills/media-processing/services/preprocess.ts +551 -0
  161. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +259 -0
  162. package/src/config/bundled-skills/media-processing/services/reduce.ts +197 -0
  163. package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +136 -0
  164. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +59 -0
  165. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
  166. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
  167. package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +143 -0
  168. package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
  169. package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +65 -0
  170. package/src/config/bundled-skills/messaging/SKILL.md +33 -8
  171. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -7
  172. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +2 -1
  173. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
  174. package/src/config/bundled-skills/phone-calls/SKILL.md +88 -23
  175. package/src/config/bundled-skills/twitter/SKILL.md +19 -3
  176. package/src/config/bundled-skills/twitter/icon.svg +14 -0
  177. package/src/config/bundled-tool-registry.ts +310 -0
  178. package/src/config/calls-schema.ts +181 -0
  179. package/src/config/core-schema.ts +309 -0
  180. package/src/config/defaults.ts +28 -3
  181. package/src/config/env-registry.ts +162 -0
  182. package/src/config/env.ts +175 -0
  183. package/src/config/loader.ts +6 -6
  184. package/src/config/memory-schema.ts +528 -0
  185. package/src/config/sandbox-schema.ts +55 -0
  186. package/src/config/schema.ts +158 -1133
  187. package/src/config/skill-state.ts +1 -1
  188. package/src/config/skills-schema.ts +32 -0
  189. package/src/config/skills.ts +35 -24
  190. package/src/config/system-prompt.ts +131 -56
  191. package/src/config/templates/IDENTITY.md +2 -2
  192. package/src/config/templates/SOUL.md +1 -1
  193. package/src/config/types.ts +1 -0
  194. package/src/config/user-reference.ts +4 -9
  195. package/src/config/vellum-skills/catalog.json +6 -7
  196. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +5 -1
  197. package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +4 -3
  198. package/src/config/vellum-skills/sms-setup/SKILL.md +216 -0
  199. package/src/config/vellum-skills/twilio-setup/SKILL.md +40 -8
  200. package/src/context/window-manager.ts +27 -7
  201. package/src/daemon/approval-generators.ts +186 -0
  202. package/src/daemon/approved-devices-store.ts +140 -0
  203. package/src/daemon/assistant-attachments.ts +1 -1
  204. package/src/daemon/classifier.ts +35 -32
  205. package/src/daemon/config-watcher.ts +1 -1
  206. package/src/daemon/daemon-control.ts +217 -0
  207. package/src/daemon/handlers/apps.ts +2 -3
  208. package/src/daemon/handlers/config-channels.ts +158 -0
  209. package/src/daemon/handlers/config-inbox.ts +540 -0
  210. package/src/daemon/handlers/config-ingress.ts +231 -0
  211. package/src/daemon/handlers/config-integrations.ts +258 -0
  212. package/src/daemon/handlers/config-model.ts +143 -0
  213. package/src/daemon/handlers/config-parental.ts +163 -0
  214. package/src/daemon/handlers/config-scheduling.ts +172 -0
  215. package/src/daemon/handlers/config-slack.ts +92 -0
  216. package/src/daemon/handlers/config-telegram.ts +301 -0
  217. package/src/daemon/handlers/config-tools.ts +177 -0
  218. package/src/daemon/handlers/config-trust.ts +104 -0
  219. package/src/daemon/handlers/config-twilio.ts +1080 -0
  220. package/src/daemon/handlers/config.ts +53 -1689
  221. package/src/daemon/handlers/diagnostics.ts +1 -1
  222. package/src/daemon/handlers/dictation.ts +180 -0
  223. package/src/daemon/handlers/documents.ts +18 -32
  224. package/src/daemon/handlers/identity.ts +14 -23
  225. package/src/daemon/handlers/index.ts +11 -0
  226. package/src/daemon/handlers/misc.ts +3 -5
  227. package/src/daemon/handlers/pairing.ts +98 -0
  228. package/src/daemon/handlers/sessions.ts +56 -5
  229. package/src/daemon/handlers/shared.ts +6 -1
  230. package/src/daemon/handlers/skills.ts +1 -1
  231. package/src/daemon/handlers/twitter-auth.ts +2 -0
  232. package/src/daemon/handlers/work-items.ts +17 -9
  233. package/src/daemon/handlers/workspace-files.ts +4 -3
  234. package/src/daemon/install-cli-launchers.ts +113 -0
  235. package/src/daemon/ipc-contract/apps.ts +356 -0
  236. package/src/daemon/ipc-contract/browser.ts +74 -0
  237. package/src/daemon/ipc-contract/computer-use.ts +151 -0
  238. package/src/daemon/ipc-contract/diagnostics.ts +56 -0
  239. package/src/daemon/ipc-contract/documents.ts +74 -0
  240. package/src/daemon/ipc-contract/inbox.ts +209 -0
  241. package/src/daemon/ipc-contract/integrations.ts +284 -0
  242. package/src/daemon/ipc-contract/memory.ts +48 -0
  243. package/src/daemon/ipc-contract/messages.ts +211 -0
  244. package/src/daemon/ipc-contract/pairing.ts +45 -0
  245. package/src/daemon/ipc-contract/parental-control.ts +95 -0
  246. package/src/daemon/ipc-contract/schedules.ts +97 -0
  247. package/src/daemon/ipc-contract/sessions.ts +315 -0
  248. package/src/daemon/ipc-contract/shared.ts +42 -0
  249. package/src/daemon/ipc-contract/skills.ts +120 -0
  250. package/src/daemon/ipc-contract/subagents.ts +58 -0
  251. package/src/daemon/ipc-contract/surfaces.ts +250 -0
  252. package/src/daemon/ipc-contract/trust.ts +60 -0
  253. package/src/daemon/ipc-contract/work-items.ts +225 -0
  254. package/src/daemon/ipc-contract/workspace.ts +113 -0
  255. package/src/daemon/ipc-contract-inventory.json +70 -0
  256. package/src/daemon/ipc-contract-inventory.ts +55 -29
  257. package/src/daemon/ipc-contract.ts +229 -2426
  258. package/src/daemon/ipc-protocol.ts +1 -1
  259. package/src/daemon/ipc-validate.ts +7 -0
  260. package/src/daemon/lifecycle.ts +97 -377
  261. package/src/daemon/pairing-store.ts +177 -0
  262. package/src/daemon/providers-setup.ts +43 -0
  263. package/src/daemon/ride-shotgun-handler.ts +68 -3
  264. package/src/daemon/server.ts +66 -46
  265. package/src/daemon/session-agent-loop-handlers.ts +421 -0
  266. package/src/daemon/session-agent-loop.ts +117 -275
  267. package/src/daemon/session-dynamic-profile.ts +1 -1
  268. package/src/daemon/session-history.ts +1 -1
  269. package/src/daemon/session-media-retry.ts +1 -1
  270. package/src/daemon/session-messaging.ts +37 -2
  271. package/src/daemon/session-notifiers.ts +5 -25
  272. package/src/daemon/session-process.ts +99 -59
  273. package/src/daemon/session-queue-manager.ts +96 -4
  274. package/src/daemon/session-runtime-assembly.ts +199 -10
  275. package/src/daemon/session-surfaces.ts +19 -4
  276. package/src/daemon/session-tool-setup.ts +30 -30
  277. package/src/daemon/session-workspace.ts +1 -1
  278. package/src/daemon/session.ts +35 -2
  279. package/src/daemon/shutdown-handlers.ts +122 -0
  280. package/src/daemon/trace-emitter.ts +1 -1
  281. package/src/daemon/watch-handler.ts +36 -33
  282. package/src/doordash/cart-queries.ts +787 -0
  283. package/src/doordash/client.ts +144 -127
  284. package/src/doordash/order-queries.ts +85 -0
  285. package/src/doordash/queries.ts +10 -1308
  286. package/src/doordash/search-queries.ts +203 -0
  287. package/src/doordash/session.ts +3 -2
  288. package/src/doordash/store-queries.ts +246 -0
  289. package/src/doordash/types.ts +367 -0
  290. package/src/email/providers/agentmail.ts +2 -1
  291. package/src/email/providers/index.ts +3 -2
  292. package/src/email/service.ts +3 -2
  293. package/src/errors.ts +43 -0
  294. package/src/home-base/prebuilt/seed.ts +1 -1
  295. package/src/hooks/cli.ts +6 -5
  296. package/src/hooks/config.ts +6 -8
  297. package/src/hooks/discovery.ts +6 -5
  298. package/src/hooks/manager.ts +4 -3
  299. package/src/hooks/runner.ts +2 -2
  300. package/src/hooks/templates.ts +5 -5
  301. package/src/inbound/public-ingress-urls.ts +6 -4
  302. package/src/index.ts +4 -2
  303. package/src/influencer/client.ts +1104 -0
  304. package/src/instrument.ts +4 -3
  305. package/src/logfire.ts +4 -3
  306. package/src/memory/admin.ts +25 -35
  307. package/src/memory/attachments-store.ts +4 -7
  308. package/src/memory/channel-delivery-store.ts +30 -1
  309. package/src/memory/channel-guardian-store.ts +202 -2
  310. package/src/memory/clarification-resolver.ts +37 -33
  311. package/src/memory/conflict-store.ts +67 -61
  312. package/src/memory/contradiction-checker.ts +141 -117
  313. package/src/memory/conversation-store.ts +335 -51
  314. package/src/memory/db-connection.ts +27 -4
  315. package/src/memory/db-init.ts +265 -4
  316. package/src/memory/db.ts +14 -1
  317. package/src/memory/embedding-backend.ts +27 -5
  318. package/src/memory/embedding-ollama.ts +2 -1
  319. package/src/memory/entity-extractor.ts +38 -35
  320. package/src/memory/guardian-action-store.ts +430 -0
  321. package/src/memory/inbox-escalation-projection.ts +59 -0
  322. package/src/memory/inbox-thread-store.ts +218 -0
  323. package/src/memory/ingress-invite-store.ts +338 -0
  324. package/src/memory/ingress-member-store.ts +350 -0
  325. package/src/memory/items-extractor.ts +91 -97
  326. package/src/memory/job-handlers/index-maintenance.ts +3 -3
  327. package/src/memory/job-handlers/media-processing.ts +69 -0
  328. package/src/memory/job-handlers/summarization.ts +32 -26
  329. package/src/memory/job-utils.ts +3 -10
  330. package/src/memory/jobs-store.ts +8 -10
  331. package/src/memory/jobs-worker.ts +55 -36
  332. package/src/memory/media-store.ts +759 -0
  333. package/src/memory/migrations/001-job-deferrals.ts +45 -0
  334. package/src/memory/migrations/002-tool-invocations-fk.ts +43 -0
  335. package/src/memory/migrations/003-memory-fts-backfill.ts +24 -0
  336. package/src/memory/migrations/004-entity-relation-dedup.ts +87 -0
  337. package/src/memory/migrations/005-fingerprint-scope-unique.ts +80 -0
  338. package/src/memory/migrations/006-scope-salted-fingerprints.ts +62 -0
  339. package/src/memory/migrations/007-assistant-id-to-self.ts +254 -0
  340. package/src/memory/migrations/008-remove-assistant-id-columns.ts +208 -0
  341. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +83 -0
  342. package/src/memory/migrations/010-ext-conv-bindings-channel-chat-unique.ts +56 -0
  343. package/src/memory/migrations/011-call-sessions-provider-sid-dedup.ts +63 -0
  344. package/src/memory/migrations/012-call-sessions-add-initiated-from.ts +19 -0
  345. package/src/memory/migrations/013-guardian-action-tables.ts +68 -0
  346. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +76 -0
  347. package/src/memory/migrations/015-drop-active-search-index.ts +27 -0
  348. package/src/memory/migrations/016-memory-segments-indexes.ts +11 -0
  349. package/src/memory/migrations/017-memory-items-indexes.ts +10 -0
  350. package/src/memory/migrations/018-remaining-table-indexes.ts +13 -0
  351. package/src/memory/migrations/index.ts +24 -0
  352. package/src/memory/migrations/registry.ts +79 -0
  353. package/src/memory/migrations/validate-migration-state.ts +69 -0
  354. package/src/memory/qdrant-manager.ts +49 -8
  355. package/src/memory/query-builder.ts +1 -1
  356. package/src/memory/raw-query.ts +119 -0
  357. package/src/memory/recall-cache.ts +4 -1
  358. package/src/memory/retriever.ts +165 -47
  359. package/src/memory/schema-migration.ts +25 -984
  360. package/src/memory/schema.ts +228 -7
  361. package/src/memory/search/entity.ts +205 -31
  362. package/src/memory/search/lexical.ts +81 -52
  363. package/src/memory/search/ranking.ts +27 -23
  364. package/src/memory/search/semantic.ts +157 -19
  365. package/src/memory/search/types.ts +24 -0
  366. package/src/memory/shared-app-links-store.ts +4 -5
  367. package/src/memory/validation.ts +19 -0
  368. package/src/messaging/draft-store.ts +5 -6
  369. package/src/messaging/provider-types.ts +2 -0
  370. package/src/messaging/providers/sms/adapter.ts +201 -0
  371. package/src/messaging/providers/sms/client.ts +93 -0
  372. package/src/messaging/providers/sms/types.ts +7 -0
  373. package/src/messaging/providers/telegram-bot/adapter.ts +2 -5
  374. package/src/messaging/providers/whatsapp/adapter.ts +136 -0
  375. package/src/messaging/providers/whatsapp/client.ts +67 -0
  376. package/src/messaging/style-analyzer.ts +5 -4
  377. package/src/messaging/thread-summarizer.ts +61 -69
  378. package/src/messaging/triage-engine.ts +62 -71
  379. package/src/migrations/config-merge.ts +53 -0
  380. package/src/migrations/data-layout.ts +68 -0
  381. package/src/migrations/data-merge.ts +33 -0
  382. package/src/migrations/hooks-merge.ts +90 -0
  383. package/src/migrations/index.ts +6 -0
  384. package/src/migrations/log.ts +23 -0
  385. package/src/migrations/skills-merge.ts +33 -0
  386. package/src/migrations/workspace-layout.ts +79 -0
  387. package/src/permissions/checker.ts +133 -11
  388. package/src/permissions/prompter.ts +14 -0
  389. package/src/permissions/shell-identity.ts +31 -1
  390. package/src/permissions/trust-store.ts +21 -1
  391. package/src/providers/anthropic/client.ts +4 -4
  392. package/src/providers/failover.ts +2 -2
  393. package/src/providers/model-intents.ts +70 -0
  394. package/src/providers/ollama/client.ts +2 -1
  395. package/src/providers/provider-send-message.ts +176 -0
  396. package/src/providers/registry.ts +71 -30
  397. package/src/providers/retry.ts +35 -1
  398. package/src/providers/types.ts +12 -1
  399. package/src/runtime/approval-conversation-turn.ts +97 -0
  400. package/src/runtime/approval-message-composer.ts +253 -0
  401. package/src/runtime/channel-approval-parser.ts +36 -2
  402. package/src/runtime/channel-approvals.ts +11 -24
  403. package/src/runtime/channel-guardian-service.ts +88 -21
  404. package/src/runtime/channel-readiness-service.ts +418 -0
  405. package/src/runtime/channel-readiness-types.ts +35 -0
  406. package/src/runtime/channel-retry-sweep.ts +184 -0
  407. package/src/runtime/guardian-context-resolver.ts +108 -0
  408. package/src/runtime/http-server.ts +275 -717
  409. package/src/runtime/http-types.ts +59 -3
  410. package/src/runtime/middleware/auth.ts +116 -0
  411. package/src/runtime/middleware/error-handler.ts +33 -0
  412. package/src/runtime/middleware/twilio-validation.ts +127 -0
  413. package/src/runtime/routes/app-routes.ts +1 -1
  414. package/src/runtime/routes/call-routes.ts +51 -7
  415. package/src/runtime/routes/channel-delivery-routes.ts +170 -0
  416. package/src/runtime/routes/channel-guardian-routes.ts +1191 -0
  417. package/src/runtime/routes/channel-inbound-routes.ts +1152 -0
  418. package/src/runtime/routes/channel-route-shared.ts +144 -0
  419. package/src/runtime/routes/channel-routes.ts +32 -1588
  420. package/src/runtime/routes/conversation-routes.ts +50 -7
  421. package/src/runtime/routes/events-routes.ts +2 -2
  422. package/src/runtime/routes/identity-routes.ts +126 -0
  423. package/src/runtime/routes/pairing-routes.ts +143 -0
  424. package/src/runtime/routes/run-routes.ts +15 -1
  425. package/src/runtime/run-orchestrator.ts +86 -35
  426. package/src/schedule/schedule-store.ts +36 -32
  427. package/src/schedule/scheduler.ts +3 -3
  428. package/src/security/encrypted-store.ts +5 -7
  429. package/src/security/oauth2.ts +45 -15
  430. package/src/security/parental-control-store.ts +183 -0
  431. package/src/security/secret-allowlist.ts +4 -3
  432. package/src/security/secret-scanner.ts +5 -5
  433. package/src/security/secure-keys.ts +1 -1
  434. package/src/security/token-manager.ts +3 -2
  435. package/src/services/vercel-deploy.ts +6 -2
  436. package/src/skills/tool-manifest.ts +3 -3
  437. package/src/skills/vellum-catalog-remote.ts +75 -16
  438. package/src/slack/slack-webhook.ts +2 -1
  439. package/src/swarm/orchestrator.ts +92 -1
  440. package/src/swarm/router-planner.ts +6 -9
  441. package/src/swarm/worker-prompts.ts +9 -12
  442. package/src/tasks/task-compiler.ts +19 -28
  443. package/src/tasks/task-runner.ts +1 -1
  444. package/src/tools/assets/materialize.ts +2 -2
  445. package/src/tools/assets/search.ts +15 -14
  446. package/src/tools/browser/__tests__/auth-detector.test.ts +1 -0
  447. package/src/tools/browser/auto-navigate.ts +1 -0
  448. package/src/tools/browser/browser-execution.ts +10 -1
  449. package/src/tools/browser/browser-manager.ts +119 -4
  450. package/src/tools/browser/network-recorder.ts +5 -0
  451. package/src/tools/calls/call-start.ts +1 -0
  452. package/src/tools/credentials/broker.ts +11 -2
  453. package/src/tools/credentials/metadata-store.ts +18 -14
  454. package/src/tools/credentials/post-connect-hooks.ts +61 -0
  455. package/src/tools/credentials/vault.ts +49 -23
  456. package/src/tools/execution-target.ts +11 -1
  457. package/src/tools/executor.ts +68 -9
  458. package/src/tools/host-terminal/cli-discover.ts +1 -1
  459. package/src/tools/network/script-proxy/http-forwarder.ts +1 -1
  460. package/src/tools/network/script-proxy/mitm-handler.ts +1 -1
  461. package/src/tools/network/script-proxy/server.ts +1 -1
  462. package/src/tools/network/script-proxy/session-manager.ts +6 -5
  463. package/src/tools/network/web-fetch.ts +18 -2
  464. package/src/tools/network/web-search.ts +8 -4
  465. package/src/tools/reminder/reminder-store.ts +14 -15
  466. package/src/tools/schedule/create.ts +1 -0
  467. package/src/tools/schedule/list.ts +2 -1
  468. package/src/tools/shared/filesystem/file-ops-service.ts +5 -7
  469. package/src/tools/skills/skill-script-runner.ts +24 -9
  470. package/src/tools/skills/skill-tool-factory.ts +1 -0
  471. package/src/tools/tasks/work-item-enqueue.ts +2 -2
  472. package/src/tools/terminal/evaluate-typescript.ts +21 -12
  473. package/src/tools/terminal/parser.ts +50 -0
  474. package/src/tools/types.ts +2 -0
  475. package/src/tools/watcher/delete.ts +6 -0
  476. package/src/tools/weather/service.ts +1 -1
  477. package/src/twitter/client.ts +190 -24
  478. package/src/twitter/router.ts +1 -1
  479. package/src/twitter/session.ts +4 -3
  480. package/src/util/clipboard.ts +1 -1
  481. package/src/util/errors.ts +65 -8
  482. package/src/util/fs.ts +40 -0
  483. package/src/util/json.ts +10 -0
  484. package/src/util/log-redact.ts +189 -0
  485. package/src/util/logger.ts +19 -17
  486. package/src/util/object.ts +3 -0
  487. package/src/util/platform.ts +105 -363
  488. package/src/util/pricing.ts +1 -1
  489. package/src/util/promise-guard.ts +1 -1
  490. package/src/util/retry.ts +19 -0
  491. package/src/util/row-mapper.ts +79 -0
  492. package/src/util/silently.ts +21 -0
  493. package/src/watcher/engine.ts +5 -1
  494. package/src/watcher/provider-types.ts +20 -0
  495. package/src/watcher/providers/github.ts +156 -0
  496. package/src/watcher/providers/gmail.ts +1 -0
  497. package/src/watcher/providers/google-calendar.ts +1 -0
  498. package/src/watcher/providers/linear.ts +460 -0
  499. package/src/watcher/providers/slack.ts +1 -0
  500. package/src/work-items/work-item-runner.ts +1 -1
  501. package/src/workspace/git-service.ts +1 -1
  502. package/src/workspace/provider-commit-message-generator.ts +51 -22
  503. package/src/__tests__/call-bridge.test.ts +0 -517
  504. package/src/__tests__/session-process-bridge.test.ts +0 -244
  505. package/src/calls/call-bridge.ts +0 -168
  506. package/src/config/vellum-skills/google-oauth-setup/SKILL.md +0 -199
@@ -1,1590 +1,34 @@
1
1
  /**
2
- * Route handlers for channel inbound messages, delivery acks, and
3
- * conversation deletion.
4
- */
5
- import { timingSafeEqual } from 'node:crypto';
6
- import { deleteConversationKey } from '../../memory/conversation-key-store.js';
7
- import * as conversationStore from '../../memory/conversation-store.js';
8
- import * as attachmentsStore from '../../memory/attachments-store.js';
9
- import * as channelDeliveryStore from '../../memory/channel-delivery-store.js';
10
- import * as externalConversationStore from '../../memory/external-conversation-store.js';
11
- import { getPendingConfirmationsByConversation } from '../../memory/runs-store.js';
12
- import { renderHistoryContent } from '../../daemon/handlers.js';
13
- import { checkIngressForSecrets } from '../../security/secret-ingress.js';
14
- import { IngressBlockedError } from '../../util/errors.js';
15
- import { getLogger } from '../../util/logger.js';
16
- import {
17
- getGuardianBinding,
18
- isGuardian,
19
- validateAndConsumeChallenge,
20
- } from '../channel-guardian-service.js';
21
- import {
22
- createApprovalRequest,
23
- getPendingApprovalByGuardianChat,
24
- getPendingApprovalByRunAndGuardianChat,
25
- getAllPendingApprovalsByGuardianChat,
26
- getPendingApprovalForRun,
27
- getUnresolvedApprovalForRun,
28
- getExpiredPendingApprovals,
29
- updateApprovalDecision,
30
- } from '../../memory/channel-guardian-store.js';
31
- import { deliverChannelReply, deliverApprovalPrompt } from '../gateway-client.js';
32
- import { parseApprovalDecision } from '../channel-approval-parser.js';
33
- import {
34
- getChannelApprovalPrompt,
35
- buildApprovalUIMetadata,
36
- buildGuardianApprovalPrompt,
37
- handleChannelDecision,
38
- buildReminderPrompt,
39
- channelSupportsRichApprovalUI,
40
- } from '../channel-approvals.js';
41
- import type { ApprovalAction, ApprovalDecisionResult } from '../channel-approval-types.js';
42
- import type { RunOrchestrator } from '../run-orchestrator.js';
43
- import type {
44
- MessageProcessor,
45
- RuntimeAttachmentMetadata,
46
- } from '../http-types.js';
47
-
48
- const log = getLogger('runtime-http');
49
-
50
- // ---------------------------------------------------------------------------
51
- // Gateway-origin proof
52
- // ---------------------------------------------------------------------------
53
-
54
- /**
55
- * Header name used by the gateway to prove a request originated from it.
56
- * The gateway sends a dedicated gateway-origin secret (or the bearer token
57
- * as fallback). The runtime validates it using constant-time comparison.
58
- * Requests to `/channels/inbound` that lack a valid proof are rejected with 403.
59
- */
60
- export const GATEWAY_ORIGIN_HEADER = 'X-Gateway-Origin';
61
-
62
- /**
63
- * Validate that the request carries a valid gateway-origin proof.
64
- * Uses constant-time comparison to prevent timing attacks.
65
- *
66
- * The `gatewayOriginSecret` parameter is the dedicated secret configured
67
- * via `RUNTIME_GATEWAY_ORIGIN_SECRET`. When set, only this value is
68
- * accepted. When not set, the function falls back to `bearerToken` for
69
- * backward compatibility. When neither is configured (local dev), validation
70
- * is skipped entirely.
71
- */
72
- export function verifyGatewayOrigin(
73
- req: Request,
74
- bearerToken?: string,
75
- gatewayOriginSecret?: string,
76
- ): boolean {
77
- // Determine the expected secret: prefer dedicated secret, fall back to bearer token
78
- const expectedSecret = gatewayOriginSecret ?? bearerToken;
79
- if (!expectedSecret) return true; // No shared secret configured — skip validation
80
- const provided = req.headers.get(GATEWAY_ORIGIN_HEADER);
81
- if (!provided) return false;
82
- const a = Buffer.from(provided);
83
- const b = Buffer.from(expectedSecret);
84
- if (a.length !== b.length) return false;
85
- return timingSafeEqual(a, b);
86
- }
87
-
88
- // ---------------------------------------------------------------------------
89
- // Actor role
90
- // ---------------------------------------------------------------------------
91
-
92
- export type ActorRole = 'guardian' | 'non-guardian' | 'unverified_channel';
93
-
94
- /** Sub-reason for `unverified_channel` denials. */
95
- export type DenialReason = 'no_binding' | 'no_identity';
96
-
97
- export interface GuardianContext {
98
- actorRole: ActorRole;
99
- /** The guardian's delivery chat ID (from the guardian binding). */
100
- guardianChatId?: string;
101
- /** The guardian's external user ID. */
102
- guardianExternalUserId?: string;
103
- /** Display identifier for the requester (username or external user ID). */
104
- requesterIdentifier?: string;
105
- /** The requester's external user ID. */
106
- requesterExternalUserId?: string;
107
- /** The requester's chat ID. */
108
- requesterChatId?: string;
109
- /** Sub-reason when actorRole is 'unverified_channel'. */
110
- denialReason?: DenialReason;
111
- }
112
-
113
- /** Guardian approval request expiry (30 minutes). */
114
- const GUARDIAN_APPROVAL_TTL_MS = 30 * 60 * 1000;
115
-
116
- /**
117
- * Return the effective prompt text for an approval prompt, appending the
118
- * plainTextFallback instructions when the channel does not support rich
119
- * inline approval UI (e.g. Telegram inline keyboards).
120
- */
121
- function effectivePromptText(
122
- promptText: string,
123
- plainTextFallback: string,
124
- channel: string,
125
- ): string {
126
- if (channelSupportsRichApprovalUI(channel)) return promptText;
127
- return plainTextFallback;
128
- }
129
-
130
- /**
131
- * Build contextual deny guidance for guardian-gated auto-deny paths.
132
- * This is passed through the confirmation pipeline so the assistant can
133
- * produce a single, user-facing message with next steps.
134
- */
135
- function buildGuardianDenyContext(
136
- toolName: string,
137
- denialReason: DenialReason,
138
- sourceChannel: string,
139
- ): string {
140
- if (denialReason === 'no_identity') {
141
- return `Permission denied: the action "${toolName}" requires guardian approval, but your identity could not be verified on ${sourceChannel}. Do not retry yet. Explain this clearly, ask the user to message from a verifiable direct account/chat, and then retry after identity is available.`;
142
- }
143
-
144
- return `Permission denied: the action "${toolName}" requires guardian approval, but no guardian is configured for this ${sourceChannel} channel. Do not retry yet. Explain that a guardian must be set up first. The guardian/admin should open the Channels section in Settings and click "Verify Guardian", or ask the assistant to set up guardian verification. The setup flow will provide a verification token to send as /guardian_verify <token> in the ${sourceChannel} chat.`;
145
- }
146
-
147
- // ---------------------------------------------------------------------------
148
- // Callback data parser — format: "apr:<runId>:<action>"
149
- // ---------------------------------------------------------------------------
150
-
151
- const VALID_ACTIONS: ReadonlySet<string> = new Set<string>([
152
- 'approve_once',
153
- 'approve_always',
154
- 'reject',
155
- ]);
156
-
157
- function parseCallbackData(data: string): ApprovalDecisionResult | null {
158
- const parts = data.split(':');
159
- if (parts.length < 3 || parts[0] !== 'apr') return null;
160
- const runId = parts[1];
161
- const action = parts.slice(2).join(':');
162
- if (!runId || !VALID_ACTIONS.has(action)) return null;
163
- return { action: action as ApprovalAction, source: 'telegram_button', runId };
164
- }
165
-
166
- export async function handleDeleteConversation(req: Request, assistantId: string = 'self'): Promise<Response> {
167
- const body = await req.json() as {
168
- sourceChannel?: string;
169
- externalChatId?: string;
170
- };
171
-
172
- const { sourceChannel, externalChatId } = body;
173
-
174
- if (!sourceChannel || typeof sourceChannel !== 'string') {
175
- return Response.json({ error: 'sourceChannel is required' }, { status: 400 });
176
- }
177
- if (!externalChatId || typeof externalChatId !== 'string') {
178
- return Response.json({ error: 'externalChatId is required' }, { status: 400 });
179
- }
180
-
181
- // Delete the assistant-scoped key unconditionally. The legacy key is
182
- // canonical for the self assistant and must not be deleted from non-self
183
- // routes, otherwise a non-self reset can accidentally reset self state.
184
- const legacyKey = `${sourceChannel}:${externalChatId}`;
185
- const scopedKey = `asst:${assistantId}:${sourceChannel}:${externalChatId}`;
186
- deleteConversationKey(scopedKey);
187
- if (assistantId === 'self') {
188
- deleteConversationKey(legacyKey);
189
- }
190
- // external_conversation_bindings is currently assistant-agnostic
191
- // (unique by sourceChannel + externalChatId). Restrict mutations to the
192
- // canonical self-assistant route so multi-assistant legacy routes do not
193
- // clobber each other's bindings.
194
- if (assistantId === 'self') {
195
- externalConversationStore.deleteBindingByChannelChat(sourceChannel, externalChatId);
196
- }
197
-
198
- return Response.json({ ok: true });
199
- }
200
-
201
- export async function handleChannelInbound(
202
- req: Request,
203
- processMessage?: MessageProcessor,
204
- bearerToken?: string,
205
- runOrchestrator?: RunOrchestrator,
206
- assistantId: string = 'self',
207
- gatewayOriginSecret?: string,
208
- ): Promise<Response> {
209
- // Reject requests that lack valid gateway-origin proof. This ensures
210
- // channel inbound messages can only arrive via the gateway (which
211
- // performs webhook-level verification) and not via direct HTTP calls.
212
- if (!verifyGatewayOrigin(req, bearerToken, gatewayOriginSecret)) {
213
- log.warn('Rejected channel inbound request: missing or invalid gateway-origin proof');
214
- return Response.json(
215
- { error: 'Forbidden: missing gateway-origin proof', code: 'GATEWAY_ORIGIN_REQUIRED' },
216
- { status: 403 },
217
- );
218
- }
219
-
220
- const body = await req.json() as {
221
- sourceChannel?: string;
222
- externalChatId?: string;
223
- externalMessageId?: string;
224
- content?: string;
225
- isEdit?: boolean;
226
- senderName?: string;
227
- attachmentIds?: string[];
228
- senderExternalUserId?: string;
229
- senderUsername?: string;
230
- sourceMetadata?: Record<string, unknown>;
231
- replyCallbackUrl?: string;
232
- callbackQueryId?: string;
233
- callbackData?: string;
234
- };
235
-
236
- const {
237
- sourceChannel,
238
- externalChatId,
239
- externalMessageId,
240
- content,
241
- isEdit,
242
- attachmentIds,
243
- sourceMetadata,
244
- } = body;
245
-
246
- if (!sourceChannel || typeof sourceChannel !== 'string') {
247
- return Response.json({ error: 'sourceChannel is required' }, { status: 400 });
248
- }
249
- if (!externalChatId || typeof externalChatId !== 'string') {
250
- return Response.json({ error: 'externalChatId is required' }, { status: 400 });
251
- }
252
- if (!externalMessageId || typeof externalMessageId !== 'string') {
253
- return Response.json({ error: 'externalMessageId is required' }, { status: 400 });
254
- }
255
-
256
- // Reject non-string content regardless of whether attachments are present.
257
- if (content !== undefined && content !== null && typeof content !== 'string') {
258
- return Response.json({ error: 'content must be a string' }, { status: 400 });
259
- }
260
-
261
- const trimmedContent = typeof content === 'string' ? content.trim() : '';
262
- const hasAttachments = Array.isArray(attachmentIds) && attachmentIds.length > 0;
263
-
264
- const hasCallbackData = typeof body.callbackData === 'string' && body.callbackData.length > 0;
265
-
266
- if (trimmedContent.length === 0 && !hasAttachments && !isEdit && !hasCallbackData) {
267
- return Response.json({ error: 'content or attachmentIds is required' }, { status: 400 });
268
- }
269
-
270
- if (hasAttachments) {
271
- const resolved = attachmentsStore.getAttachmentsByIds(attachmentIds);
272
- if (resolved.length !== attachmentIds.length) {
273
- const resolvedIds = new Set(resolved.map((a) => a.id));
274
- const missing = attachmentIds.filter((id) => !resolvedIds.has(id));
275
- return Response.json(
276
- { error: `Attachment IDs not found: ${missing.join(', ')}` },
277
- { status: 400 },
278
- );
279
- }
280
- }
281
-
282
- const sourceMessageId = typeof sourceMetadata?.messageId === 'string'
283
- ? sourceMetadata.messageId
284
- : undefined;
285
-
286
- if (isEdit && !sourceMessageId) {
287
- return Response.json({ error: 'sourceMetadata.messageId is required for edits' }, { status: 400 });
288
- }
289
-
290
- // ── Edit path: update existing message content, no new agent loop ──
291
- if (isEdit && sourceMessageId) {
292
- // Dedup the edit event itself (retried edited_message webhooks)
293
- const editResult = channelDeliveryStore.recordInbound(
294
- sourceChannel,
295
- externalChatId,
296
- externalMessageId,
297
- { sourceMessageId, assistantId },
298
- );
299
-
300
- if (editResult.duplicate) {
301
- return Response.json({
302
- accepted: true,
303
- duplicate: true,
304
- eventId: editResult.eventId,
305
- });
306
- }
307
-
308
- // Retry lookup a few times — the original message may still be processing
309
- // (linkMessage hasn't been called yet). Short backoff avoids losing edits
310
- // that arrive while the original agent loop is in progress.
311
- const EDIT_LOOKUP_RETRIES = 5;
312
- const EDIT_LOOKUP_DELAY_MS = 2000;
313
-
314
- let original: { messageId: string; conversationId: string } | null = null;
315
- for (let attempt = 0; attempt <= EDIT_LOOKUP_RETRIES; attempt++) {
316
- original = channelDeliveryStore.findMessageBySourceId(
317
- sourceChannel,
318
- externalChatId,
319
- sourceMessageId,
320
- );
321
- if (original) break;
322
- if (attempt < EDIT_LOOKUP_RETRIES) {
323
- log.info(
324
- { assistantId, sourceMessageId, attempt: attempt + 1, maxAttempts: EDIT_LOOKUP_RETRIES },
325
- 'Original message not linked yet, retrying edit lookup',
326
- );
327
- await new Promise((resolve) => setTimeout(resolve, EDIT_LOOKUP_DELAY_MS));
328
- }
329
- }
330
-
331
- if (original) {
332
- conversationStore.updateMessageContent(original.messageId, content ?? '');
333
- log.info(
334
- { assistantId, sourceMessageId, messageId: original.messageId },
335
- 'Updated message content from edited_message',
336
- );
337
- } else {
338
- log.warn(
339
- { assistantId, sourceChannel, externalChatId, sourceMessageId },
340
- 'Could not find original message for edit after retries, ignoring',
341
- );
342
- }
343
-
344
- return Response.json({
345
- accepted: true,
346
- duplicate: false,
347
- eventId: editResult.eventId,
348
- });
349
- }
350
-
351
- // ── New message path ──
352
- const result = channelDeliveryStore.recordInbound(
353
- sourceChannel,
354
- externalChatId,
355
- externalMessageId,
356
- { sourceMessageId, assistantId },
357
- );
358
-
359
- // external_conversation_bindings is assistant-agnostic. Restrict writes to
360
- // self so assistant-scoped legacy routes do not overwrite each other's
361
- // channel binding metadata for the same chat.
362
- if (assistantId === 'self') {
363
- externalConversationStore.upsertBinding({
364
- conversationId: result.conversationId,
365
- sourceChannel,
366
- externalChatId,
367
- externalUserId: body.senderExternalUserId ?? null,
368
- displayName: body.senderName ?? null,
369
- username: body.senderUsername ?? null,
370
- });
371
- }
372
-
373
- const metadataHintsRaw = sourceMetadata?.hints;
374
- const metadataHints = Array.isArray(metadataHintsRaw)
375
- ? metadataHintsRaw.filter((hint): hint is string => typeof hint === 'string' && hint.trim().length > 0)
376
- : [];
377
- const metadataUxBrief = typeof sourceMetadata?.uxBrief === 'string' && sourceMetadata.uxBrief.trim().length > 0
378
- ? sourceMetadata.uxBrief.trim()
379
- : undefined;
380
-
381
- const replyCallbackUrl = body.replyCallbackUrl;
382
-
383
- // ── Guardian verification command intercept ──
384
- // Handled before normal message processing so it never enters the agent loop.
385
- if (
386
- !result.duplicate &&
387
- trimmedContent.startsWith('/guardian_verify ') &&
388
- replyCallbackUrl &&
389
- body.senderExternalUserId
390
- ) {
391
- const token = trimmedContent.slice('/guardian_verify '.length).trim();
392
- if (token.length > 0) {
393
- const verifyResult = validateAndConsumeChallenge(
394
- assistantId,
395
- sourceChannel,
396
- token,
397
- body.senderExternalUserId,
398
- externalChatId,
399
- );
400
-
401
- const replyText = verifyResult.success
402
- ? 'Guardian verified successfully. Your identity is now linked to this bot.'
403
- : 'Verification failed. Please try again later.';
404
-
405
- try {
406
- await deliverChannelReply(replyCallbackUrl, {
407
- chatId: externalChatId,
408
- text: replyText,
409
- assistantId,
410
- }, bearerToken);
411
- } catch (err) {
412
- log.error({ err, externalChatId }, 'Failed to deliver guardian verification reply');
413
- }
414
-
415
- return Response.json({
416
- accepted: true,
417
- duplicate: false,
418
- eventId: result.eventId,
419
- guardianVerification: verifyResult.success ? 'verified' : 'failed',
420
- });
421
- }
422
- }
423
-
424
- // ── Actor role resolution ──
425
- // Determine whether the sender is the guardian for this channel.
426
- // When a guardian binding exists, non-guardian actors get stricter
427
- // side-effect controls and their approvals route to the guardian's chat.
428
- //
429
- // Guardian actor-role resolution always runs.
430
- let guardianCtx: GuardianContext = { actorRole: 'guardian' };
431
- if (body.senderExternalUserId) {
432
- const senderIsGuardian = isGuardian(assistantId, sourceChannel, body.senderExternalUserId);
433
- if (!senderIsGuardian) {
434
- const binding = getGuardianBinding(assistantId, sourceChannel);
435
- if (binding) {
436
- const requesterLabel = body.senderUsername
437
- ? `@${body.senderUsername}`
438
- : body.senderExternalUserId;
439
- guardianCtx = {
440
- actorRole: 'non-guardian',
441
- guardianChatId: binding.guardianDeliveryChatId,
442
- guardianExternalUserId: binding.guardianExternalUserId,
443
- requesterIdentifier: requesterLabel,
444
- requesterExternalUserId: body.senderExternalUserId,
445
- requesterChatId: externalChatId,
446
- };
447
- } else {
448
- // No guardian binding configured for this channel — the sender is
449
- // unverified. Sensitive actions will be auto-denied (fail-closed).
450
- guardianCtx = {
451
- actorRole: 'unverified_channel',
452
- denialReason: 'no_binding',
453
- requesterExternalUserId: body.senderExternalUserId,
454
- requesterChatId: externalChatId,
455
- };
456
- }
457
- }
458
- } else {
459
- // No sender identity available — treat as unverified and fail closed.
460
- // Multi-actor channels must not grant default guardian permissions when
461
- // the inbound actor cannot be identified.
462
- guardianCtx = {
463
- actorRole: 'unverified_channel',
464
- denialReason: 'no_identity',
465
- requesterExternalUserId: undefined,
466
- requesterChatId: externalChatId,
467
- };
468
- }
469
-
470
- // ── Approval interception ──
471
- // Keep this active whenever orchestrator + callback context are available.
472
- if (
473
- runOrchestrator &&
474
- replyCallbackUrl &&
475
- !result.duplicate
476
- ) {
477
- const approvalResult = await handleApprovalInterception({
478
- conversationId: result.conversationId,
479
- callbackData: body.callbackData,
480
- content: trimmedContent,
481
- externalChatId,
482
- sourceChannel,
483
- senderExternalUserId: body.senderExternalUserId,
484
- replyCallbackUrl,
485
- bearerToken,
486
- orchestrator: runOrchestrator,
487
- guardianCtx,
488
- assistantId,
489
- });
490
-
491
- if (approvalResult.handled) {
492
- return Response.json({
493
- accepted: true,
494
- duplicate: false,
495
- eventId: result.eventId,
496
- approval: approvalResult.type,
497
- });
498
- }
499
-
500
- // When a callback payload was not handled by approval interception, it's
501
- // a stale button press with no pending approval. Return early regardless
502
- // of whether content/attachments are present — callback payloads always
503
- // have non-empty content (normalize.ts sets message.content to cbq.data),
504
- // so checking for empty content alone would miss stale callbacks.
505
- if (hasCallbackData) {
506
- return Response.json({
507
- accepted: true,
508
- duplicate: false,
509
- eventId: result.eventId,
510
- approval: 'stale_ignored',
511
- });
512
- }
513
- }
514
-
515
- // For new (non-duplicate) messages, run the secret ingress check
516
- // synchronously, then fire off the agent loop in the background.
517
- if (!result.duplicate && processMessage) {
518
- // Persist the raw payload first so dead-lettered events can always be
519
- // replayed. If the ingress check later detects secrets we clear it
520
- // before throwing, so secret-bearing content is never left on disk.
521
- channelDeliveryStore.storePayload(result.eventId, {
522
- sourceChannel, externalChatId, externalMessageId, content,
523
- attachmentIds, sourceMetadata: body.sourceMetadata,
524
- senderName: body.senderName,
525
- senderExternalUserId: body.senderExternalUserId,
526
- senderUsername: body.senderUsername,
527
- replyCallbackUrl,
528
- assistantId,
529
- });
530
-
531
- const contentToCheck = content ?? '';
532
- let ingressCheck: ReturnType<typeof checkIngressForSecrets>;
533
- try {
534
- ingressCheck = checkIngressForSecrets(contentToCheck);
535
- } catch (checkErr) {
536
- channelDeliveryStore.clearPayload(result.eventId);
537
- throw checkErr;
538
- }
539
- if (ingressCheck.blocked) {
540
- channelDeliveryStore.clearPayload(result.eventId);
541
- throw new IngressBlockedError(ingressCheck.userNotice!, ingressCheck.detectedTypes);
542
- }
543
-
544
- // Use the approval-aware orchestrator path whenever orchestration and a
545
- // callback delivery target are available. This keeps approval handling
546
- // consistent across all channels and avoids silent prompt timeouts.
547
- const useApprovalPath = Boolean(
548
- runOrchestrator &&
549
- replyCallbackUrl,
550
- );
551
-
552
- if (useApprovalPath && runOrchestrator && replyCallbackUrl) {
553
- processChannelMessageWithApprovals({
554
- orchestrator: runOrchestrator,
555
- conversationId: result.conversationId,
556
- eventId: result.eventId,
557
- content: content ?? '',
558
- attachmentIds: hasAttachments ? attachmentIds : undefined,
559
- externalChatId,
560
- sourceChannel,
561
- replyCallbackUrl,
562
- bearerToken,
563
- guardianCtx,
564
- assistantId,
565
- });
566
- } else {
567
- // Fire-and-forget: process the message and deliver the reply in the background.
568
- // The HTTP response returns immediately so the gateway webhook is not blocked.
569
- processChannelMessageInBackground({
570
- processMessage,
571
- conversationId: result.conversationId,
572
- eventId: result.eventId,
573
- content: content ?? '',
574
- attachmentIds: hasAttachments ? attachmentIds : undefined,
575
- sourceChannel,
576
- externalChatId,
577
- metadataHints,
578
- metadataUxBrief,
579
- replyCallbackUrl,
580
- bearerToken,
581
- assistantId,
582
- });
583
- }
584
- }
585
-
586
- return Response.json({
587
- accepted: result.accepted,
588
- duplicate: result.duplicate,
589
- eventId: result.eventId,
590
- });
591
- }
592
-
593
- interface BackgroundProcessingParams {
594
- processMessage: MessageProcessor;
595
- conversationId: string;
596
- eventId: string;
597
- content: string;
598
- attachmentIds?: string[];
599
- sourceChannel: string;
600
- externalChatId: string;
601
- metadataHints: string[];
602
- metadataUxBrief?: string;
603
- replyCallbackUrl?: string;
604
- bearerToken?: string;
605
- assistantId?: string;
606
- }
607
-
608
- function processChannelMessageInBackground(params: BackgroundProcessingParams): void {
609
- const {
610
- processMessage,
611
- conversationId,
612
- eventId,
613
- content,
614
- attachmentIds,
615
- sourceChannel,
616
- externalChatId,
617
- metadataHints,
618
- metadataUxBrief,
619
- replyCallbackUrl,
620
- bearerToken,
621
- assistantId,
622
- } = params;
623
-
624
- (async () => {
625
- try {
626
- const { messageId: userMessageId } = await processMessage(
627
- conversationId,
628
- content,
629
- attachmentIds,
630
- {
631
- transport: {
632
- channelId: sourceChannel,
633
- hints: metadataHints.length > 0 ? metadataHints : undefined,
634
- uxBrief: metadataUxBrief,
635
- },
636
- },
637
- sourceChannel,
638
- );
639
- channelDeliveryStore.linkMessage(eventId, userMessageId);
640
- channelDeliveryStore.markProcessed(eventId);
641
-
642
- if (replyCallbackUrl) {
643
- await deliverReplyViaCallback(
644
- conversationId,
645
- externalChatId,
646
- replyCallbackUrl,
647
- bearerToken,
648
- assistantId,
649
- );
650
- }
651
- } catch (err) {
652
- log.error({ err, conversationId }, 'Background channel message processing failed');
653
- channelDeliveryStore.recordProcessingFailure(eventId, err);
654
- }
655
- })();
656
- }
657
-
658
- // ---------------------------------------------------------------------------
659
- // Orchestrator-backed channel processing with approval prompt delivery
660
- // ---------------------------------------------------------------------------
661
-
662
- const RUN_POLL_INTERVAL_MS = 500;
663
- const RUN_POLL_MAX_WAIT_MS = 300_000; // 5 minutes
664
-
665
- /** Post-decision delivery poll: uses the same budget as the main poll since
666
- * this is the only delivery path for late approvals after the main poll exits. */
667
- const POST_DECISION_POLL_INTERVAL_MS = 500;
668
- const POST_DECISION_POLL_MAX_WAIT_MS = RUN_POLL_MAX_WAIT_MS;
669
-
670
- /**
671
- * Override the poll max-wait for tests. When set, used in place of
672
- * RUN_POLL_MAX_WAIT_MS so tests can exercise timeout paths without
673
- * waiting 5 minutes.
674
- */
675
- let testPollMaxWaitOverride: number | null = null;
676
-
677
- /** @internal — test-only: set an override for the poll max-wait. */
678
- export function _setTestPollMaxWait(ms: number | null): void {
679
- testPollMaxWaitOverride = ms;
680
- }
681
-
682
- function getEffectivePollMaxWait(): number {
683
- return testPollMaxWaitOverride ?? RUN_POLL_MAX_WAIT_MS;
684
- }
685
-
686
- interface ApprovalProcessingParams {
687
- orchestrator: RunOrchestrator;
688
- conversationId: string;
689
- eventId: string;
690
- content: string;
691
- attachmentIds?: string[];
692
- externalChatId: string;
693
- sourceChannel: string;
694
- replyCallbackUrl: string;
695
- bearerToken?: string;
696
- guardianCtx: GuardianContext;
697
- assistantId: string;
698
- }
699
-
700
- /**
701
- * Process a channel message using the run orchestrator so that
702
- * `confirmation_request` events are intercepted and written to the
703
- * runs store. Polls the run until it reaches a terminal state,
704
- * sending approval prompts when `needs_confirmation` is detected.
705
- *
706
- * When the actor is a non-guardian:
707
- * - `forceStrictSideEffects` is set on the run so all side-effect tools
708
- * trigger the confirmation flow
709
- * - Approval prompts are routed to the guardian's chat
710
- * - A `channelGuardianApprovalRequest` record is created
711
- */
712
- function processChannelMessageWithApprovals(params: ApprovalProcessingParams): void {
713
- const {
714
- orchestrator,
715
- conversationId,
716
- eventId,
717
- content,
718
- attachmentIds,
719
- externalChatId,
720
- sourceChannel,
721
- replyCallbackUrl,
722
- bearerToken,
723
- guardianCtx,
724
- assistantId,
725
- } = params;
726
-
727
- const isNonGuardian = guardianCtx.actorRole === 'non-guardian';
728
- const isUnverifiedChannel = guardianCtx.actorRole === 'unverified_channel';
729
-
730
- (async () => {
731
- try {
732
- const run = await orchestrator.startRun(
733
- conversationId,
734
- content,
735
- attachmentIds,
736
- {
737
- ...((isNonGuardian || isUnverifiedChannel) ? { forceStrictSideEffects: true } : {}),
738
- sourceChannel,
739
- },
740
- );
741
-
742
- // Poll the run until it reaches a terminal state, delivering approval
743
- // prompts when it transitions to needs_confirmation.
744
- const startTime = Date.now();
745
- const pollMaxWait = getEffectivePollMaxWait();
746
- let lastStatus = run.status;
747
- // Track whether a post-decision delivery path is guaranteed for this
748
- // run. Set to true only when the approval prompt is successfully
749
- // delivered (guardian or standard path), meaning
750
- // handleApprovalInterception will schedule schedulePostDecisionDelivery
751
- // when a decision arrives. Auto-deny paths (unverified channel, prompt
752
- // delivery failures) do NOT set this flag because no post-decision
753
- // delivery is scheduled in those cases.
754
- let hasPostDecisionDelivery = false;
755
-
756
- while (Date.now() - startTime < pollMaxWait) {
757
- await new Promise((resolve) => setTimeout(resolve, RUN_POLL_INTERVAL_MS));
758
-
759
- const current = orchestrator.getRun(run.id);
760
- if (!current) break;
761
-
762
- if (current.status === 'needs_confirmation' && lastStatus !== 'needs_confirmation') {
763
- const pending = getPendingConfirmationsByConversation(conversationId);
764
-
765
- if (isUnverifiedChannel && pending.length > 0) {
766
- // Unverified channel — auto-deny the sensitive action (fail-closed).
767
- handleChannelDecision(
768
- conversationId,
769
- { action: 'reject', source: 'plain_text' },
770
- orchestrator,
771
- buildGuardianDenyContext(
772
- pending[0].toolName,
773
- guardianCtx.denialReason ?? 'no_binding',
774
- sourceChannel,
775
- ),
776
- );
777
- } else if (isNonGuardian && guardianCtx.guardianChatId && pending.length > 0) {
778
- // Non-guardian actor: route the approval prompt to the guardian's chat
779
- const guardianPrompt = buildGuardianApprovalPrompt(
780
- pending[0],
781
- guardianCtx.requesterIdentifier ?? 'Unknown user',
782
- );
783
- const uiMetadata = buildApprovalUIMetadata(guardianPrompt, pending[0]);
784
-
785
- // Persist the guardian approval request so we can look it up when
786
- // the guardian responds from their chat.
787
- const approvalReqRecord = createApprovalRequest({
788
- runId: run.id,
789
- conversationId,
790
- assistantId,
791
- channel: sourceChannel,
792
- requesterExternalUserId: guardianCtx.requesterExternalUserId ?? '',
793
- requesterChatId: guardianCtx.requesterChatId ?? externalChatId,
794
- guardianExternalUserId: guardianCtx.guardianExternalUserId ?? '',
795
- guardianChatId: guardianCtx.guardianChatId,
796
- toolName: pending[0].toolName,
797
- riskLevel: pending[0].riskLevel,
798
- expiresAt: Date.now() + GUARDIAN_APPROVAL_TTL_MS,
799
- });
800
-
801
- let guardianNotified = false;
802
- try {
803
- const guardianText = effectivePromptText(
804
- guardianPrompt.promptText,
805
- guardianPrompt.plainTextFallback,
806
- sourceChannel,
807
- );
808
- await deliverApprovalPrompt(
809
- replyCallbackUrl,
810
- guardianCtx.guardianChatId,
811
- guardianText,
812
- uiMetadata,
813
- assistantId,
814
- bearerToken,
815
- );
816
- guardianNotified = true;
817
- hasPostDecisionDelivery = true;
818
- } catch (err) {
819
- log.error({ err, runId: run.id }, 'Failed to deliver guardian approval prompt');
820
- // Deny the approval and the underlying run — fail-closed. If
821
- // the prompt could not be delivered, the guardian will never see
822
- // it, so the safest action is to deny rather than cancel (which
823
- // would allow requester fallback).
824
- updateApprovalDecision(approvalReqRecord.id, { status: 'denied' });
825
- handleChannelDecision(conversationId, { action: 'reject', source: 'plain_text' }, orchestrator);
826
- try {
827
- await deliverChannelReply(replyCallbackUrl, {
828
- chatId: guardianCtx.requesterChatId ?? externalChatId,
829
- text: `Your request to run "${pending[0].toolName}" could not be sent to the guardian for approval. The action has been denied.`,
830
- assistantId,
831
- }, bearerToken);
832
- } catch (notifyErr) {
833
- log.error({ err: notifyErr, runId: run.id }, 'Failed to notify requester of guardian delivery failure');
834
- }
835
- }
836
-
837
- // Only notify the requester if the guardian prompt was actually delivered
838
- if (guardianNotified) {
839
- try {
840
- await deliverChannelReply(replyCallbackUrl, {
841
- chatId: guardianCtx.requesterChatId ?? externalChatId,
842
- text: `Your request to run "${pending[0].toolName}" has been sent to the guardian for approval.`,
843
- assistantId,
844
- }, bearerToken);
845
- } catch (err) {
846
- log.error({ err, runId: run.id }, 'Failed to notify requester of pending guardian approval');
847
- }
848
- }
849
- } else {
850
- // Guardian actor or no guardian binding: standard approval prompt
851
- // sent to the requester's own chat.
852
- const prompt = getChannelApprovalPrompt(conversationId);
853
- if (prompt && pending.length > 0) {
854
- const uiMetadata = buildApprovalUIMetadata(prompt, pending[0]);
855
- try {
856
- const promptTextForChannel = effectivePromptText(
857
- prompt.promptText,
858
- prompt.plainTextFallback,
859
- sourceChannel,
860
- );
861
- await deliverApprovalPrompt(
862
- replyCallbackUrl,
863
- externalChatId,
864
- promptTextForChannel,
865
- uiMetadata,
866
- assistantId,
867
- bearerToken,
868
- );
869
- hasPostDecisionDelivery = true;
870
- } catch (err) {
871
- // Fail-closed: if we cannot deliver the approval prompt, the
872
- // user will never see it and the run would hang indefinitely
873
- // in needs_confirmation. Auto-deny to avoid silent wait states.
874
- log.error(
875
- { err, runId: run.id, conversationId },
876
- 'Failed to deliver standard approval prompt; auto-denying (fail-closed)',
877
- );
878
- handleChannelDecision(conversationId, { action: 'reject', source: 'plain_text' }, orchestrator);
879
- }
880
- }
881
- }
882
- }
883
-
884
- lastStatus = current.status;
885
-
886
- if (current.status === 'completed' || current.status === 'failed') {
887
- break;
888
- }
889
- }
890
-
891
- // Only mark processed and deliver the final reply when the run has
892
- // actually reached a terminal state.
893
- const finalRun = orchestrator.getRun(run.id);
894
- const isTerminal = finalRun?.status === 'completed' || finalRun?.status === 'failed';
895
-
896
- if (isTerminal) {
897
- // Link the inbound event to the user message created by the run so
898
- // that edit lookups and dead letter replay work correctly.
899
- if (run.messageId) {
900
- channelDeliveryStore.linkMessage(eventId, run.messageId);
901
- }
902
-
903
- channelDeliveryStore.markProcessed(eventId);
904
-
905
- // Deliver the final assistant reply exactly once. The post-decision
906
- // poll in schedulePostDecisionDelivery races with this path; the
907
- // claimRunDelivery guard ensures only the winner sends the reply.
908
- // If delivery fails, release the claim so the other poller can retry
909
- // rather than permanently losing the reply.
910
- if (channelDeliveryStore.claimRunDelivery(run.id)) {
911
- try {
912
- await deliverReplyViaCallback(
913
- conversationId,
914
- externalChatId,
915
- replyCallbackUrl,
916
- bearerToken,
917
- assistantId,
918
- );
919
- } catch (deliveryErr) {
920
- channelDeliveryStore.resetRunDeliveryClaim(run.id);
921
- throw deliveryErr;
922
- }
923
- }
924
-
925
- // If this was a non-guardian run that went through guardian approval,
926
- // also notify the guardian's chat about the outcome.
927
- if (isNonGuardian && guardianCtx.guardianChatId) {
928
- const approvalReq = getPendingApprovalForRun(run.id);
929
- if (approvalReq) {
930
- // The approval was resolved (run completed or failed) — mark it
931
- const outcomeStatus = finalRun?.status === 'completed' ? 'approved' as const : 'denied' as const;
932
- updateApprovalDecision(approvalReq.id, { status: outcomeStatus });
933
- }
934
- }
935
- } else if (
936
- finalRun?.status === 'needs_confirmation' ||
937
- (hasPostDecisionDelivery && finalRun?.status === 'running')
938
- ) {
939
- // The run is either still waiting for an approval decision or was
940
- // recently approved and has resumed execution. In both cases, mark
941
- // the event as processed rather than failed:
942
- //
943
- // - needs_confirmation: the run will resume when the user clicks
944
- // approve/reject, and `handleApprovalInterception` will deliver
945
- // the reply via `schedulePostDecisionDelivery`.
946
- //
947
- // - running (after successful prompt delivery): an approval was
948
- // applied near the poll deadline and the run resumed but hasn't
949
- // reached terminal state yet. `handleApprovalInterception` has
950
- // already scheduled post-decision delivery, so the final reply
951
- // will be delivered. This condition is only true when the approval
952
- // prompt was actually delivered (not in auto-deny paths), ensuring
953
- // we don't suppress retry/dead-letter for cases where no
954
- // post-decision delivery path exists.
955
- //
956
- // Marking either state as failed would cause the generic retry sweep
957
- // to replay through `processMessage`, which throws "Session is
958
- // already processing a message" and dead-letters a valid conversation.
959
- log.warn(
960
- { runId: run.id, status: finalRun.status, conversationId, hasPostDecisionDelivery },
961
- 'Approval-path poll loop timed out while run is in approval-related state; marking event as processed',
962
- );
963
- channelDeliveryStore.markProcessed(eventId);
964
- } else {
965
- // The run is in a non-terminal, non-approval state (e.g. running
966
- // without prior approval, needs_secret, or disappeared). Record a
967
- // processing failure so the retry/dead-letter machinery can handle it.
968
- const timeoutErr = new Error(
969
- `Approval poll timeout: run did not reach terminal state within ${pollMaxWait}ms (status: ${finalRun?.status ?? 'null'})`,
970
- );
971
- log.warn(
972
- { runId: run.id, status: finalRun?.status, conversationId },
973
- 'Approval-path poll loop timed out before run reached terminal state',
974
- );
975
- channelDeliveryStore.recordProcessingFailure(eventId, timeoutErr);
976
- }
977
- } catch (err) {
978
- log.error({ err, conversationId }, 'Approval-aware channel message processing failed');
979
- channelDeliveryStore.recordProcessingFailure(eventId, err);
980
- }
981
- })();
982
- }
983
-
984
- // ---------------------------------------------------------------------------
985
- // Approval interception
986
- // ---------------------------------------------------------------------------
987
-
988
- interface ApprovalInterceptionParams {
989
- conversationId: string;
990
- callbackData?: string;
991
- content: string;
992
- externalChatId: string;
993
- sourceChannel: string;
994
- senderExternalUserId?: string;
995
- replyCallbackUrl: string;
996
- bearerToken?: string;
997
- orchestrator: RunOrchestrator;
998
- guardianCtx: GuardianContext;
999
- assistantId: string;
1000
- }
1001
-
1002
- interface ApprovalInterceptionResult {
1003
- handled: boolean;
1004
- type?: 'decision_applied' | 'reminder_sent' | 'guardian_decision_applied' | 'stale_ignored';
1005
- }
1006
-
1007
- /**
1008
- * Check for pending approvals and handle inbound messages accordingly.
1009
- *
1010
- * Returns `{ handled: true }` when the message was consumed by the approval
1011
- * flow (either as a decision or a reminder), so the caller should NOT proceed
1012
- * to normal message processing.
1013
- *
1014
- * When the sender is a guardian responding from their chat, also checks for
1015
- * pending guardian approval requests and routes the decision accordingly.
1016
- */
1017
- async function handleApprovalInterception(
1018
- params: ApprovalInterceptionParams,
1019
- ): Promise<ApprovalInterceptionResult> {
1020
- const {
1021
- conversationId,
1022
- callbackData,
1023
- content,
1024
- externalChatId,
1025
- sourceChannel,
1026
- senderExternalUserId,
1027
- replyCallbackUrl,
1028
- bearerToken,
1029
- orchestrator,
1030
- guardianCtx,
1031
- assistantId,
1032
- } = params;
1033
-
1034
- // ── Guardian approval decision path ──
1035
- // When the sender is the guardian and there's a pending guardian approval
1036
- // request targeting this chat, the message might be a decision on behalf
1037
- // of a non-guardian requester.
1038
- if (
1039
- guardianCtx.actorRole === 'guardian' &&
1040
- senderExternalUserId
1041
- ) {
1042
- // First, try to parse the inbound payload to determine if it carries
1043
- // a run ID (callback button) or is plain text. This governs how we
1044
- // look up the target approval request.
1045
- let decision: ApprovalDecisionResult | null = null;
1046
- if (callbackData) {
1047
- decision = parseCallbackData(callbackData);
1048
- }
1049
- if (!decision && content) {
1050
- decision = parseApprovalDecision(content);
1051
- }
1052
-
1053
- // When a callback button provides a run ID, use the scoped lookup so
1054
- // the decision resolves to exactly the right approval even when
1055
- // multiple approvals target the same guardian chat.
1056
- let guardianApproval = decision?.runId
1057
- ? getPendingApprovalByRunAndGuardianChat(decision.runId, sourceChannel, externalChatId, assistantId)
1058
- : null;
1059
-
1060
- // For plain-text decisions (no run ID), check how many pending
1061
- // approvals exist for this guardian chat. If there are multiple,
1062
- // the guardian must use buttons to disambiguate.
1063
- if (!guardianApproval && decision && !decision.runId) {
1064
- const allPending = getAllPendingApprovalsByGuardianChat(sourceChannel, externalChatId, assistantId);
1065
- if (allPending.length > 1) {
1066
- try {
1067
- await deliverChannelReply(replyCallbackUrl, {
1068
- chatId: externalChatId,
1069
- text: `You have ${allPending.length} pending approval requests. Please use the approval buttons to respond to a specific request.`,
1070
- assistantId,
1071
- }, bearerToken);
1072
- } catch (err) {
1073
- log.error({ err, externalChatId }, 'Failed to deliver disambiguation notice');
1074
- }
1075
- return { handled: true, type: 'guardian_decision_applied' };
1076
- }
1077
- if (allPending.length === 1) {
1078
- guardianApproval = allPending[0];
1079
- }
1080
- }
1081
-
1082
- // Fall back to the single-result lookup for non-decision messages
1083
- // (reminder path) or when the scoped lookup found nothing.
1084
- if (!guardianApproval && !decision) {
1085
- guardianApproval = getPendingApprovalByGuardianChat(sourceChannel, externalChatId, assistantId);
1086
- }
1087
-
1088
- if (guardianApproval) {
1089
- // Validate that the sender is the specific guardian who was assigned
1090
- // this approval request. This is a defense-in-depth check — the
1091
- // actorRole check above already verifies the sender is a guardian,
1092
- // but this catches edge cases like binding rotation between request
1093
- // creation and decision.
1094
- if (senderExternalUserId !== guardianApproval.guardianExternalUserId) {
1095
- log.warn(
1096
- { externalChatId, senderExternalUserId, expectedGuardian: guardianApproval.guardianExternalUserId },
1097
- 'Non-guardian sender attempted to act on guardian approval request',
1098
- );
1099
- try {
1100
- await deliverChannelReply(replyCallbackUrl, {
1101
- chatId: externalChatId,
1102
- text: 'Only the verified guardian can approve or deny this request.',
1103
- assistantId,
1104
- }, bearerToken);
1105
- } catch (err) {
1106
- log.error({ err, externalChatId }, 'Failed to deliver guardian identity rejection notice');
1107
- }
1108
- return { handled: true, type: 'guardian_decision_applied' };
1109
- }
1110
-
1111
- if (decision) {
1112
- // approve_always is not available for guardian approvals — guardians
1113
- // should not be able to permanently allowlist tools on behalf of the
1114
- // requester. Downgrade to approve_once.
1115
- if (decision.action === 'approve_always') {
1116
- decision = { ...decision, action: 'approve_once' };
1117
- }
1118
-
1119
- // Apply the decision to the underlying run using the requester's
1120
- // conversation context
1121
- const result = handleChannelDecision(
1122
- guardianApproval.conversationId,
1123
- decision,
1124
- orchestrator,
1125
- );
1126
-
1127
- // Update the guardian approval request record
1128
- const approvalStatus = decision.action === 'reject' ? 'denied' as const : 'approved' as const;
1129
- updateApprovalDecision(guardianApproval.id, {
1130
- status: approvalStatus,
1131
- decidedByExternalUserId: senderExternalUserId,
1132
- });
1133
-
1134
- if (result.applied) {
1135
- // Notify the requester's chat about the outcome with the tool name
1136
- const toolLabel = guardianApproval.toolName;
1137
- const outcomeText = decision.action === 'reject'
1138
- ? `Your request to run "${toolLabel}" was denied by the guardian.`
1139
- : `Your request to run "${toolLabel}" was approved by the guardian.`;
1140
- try {
1141
- await deliverChannelReply(replyCallbackUrl, {
1142
- chatId: guardianApproval.requesterChatId,
1143
- text: outcomeText,
1144
- assistantId,
1145
- }, bearerToken);
1146
- } catch (err) {
1147
- log.error({ err, conversationId: guardianApproval.conversationId }, 'Failed to notify requester of guardian decision');
1148
- }
1149
-
1150
- // Schedule post-decision delivery to the requester's chat in case
1151
- // the original poll has already exited.
1152
- if (result.runId) {
1153
- schedulePostDecisionDelivery(
1154
- orchestrator,
1155
- result.runId,
1156
- guardianApproval.conversationId,
1157
- guardianApproval.requesterChatId,
1158
- replyCallbackUrl,
1159
- bearerToken,
1160
- assistantId,
1161
- );
1162
- }
1163
- }
1164
-
1165
- return { handled: true, type: 'guardian_decision_applied' };
1166
- }
1167
-
1168
- // Non-decision message from guardian while approval is pending — remind them
1169
- const pendingInfo = getPendingConfirmationsByConversation(guardianApproval.conversationId);
1170
- if (pendingInfo.length > 0) {
1171
- const guardianPrompt = buildGuardianApprovalPrompt(
1172
- pendingInfo[0],
1173
- `user ${guardianApproval.requesterExternalUserId}`,
1174
- );
1175
- const reminder = buildReminderPrompt(guardianPrompt);
1176
- const uiMetadata = buildApprovalUIMetadata(reminder, pendingInfo[0]);
1177
- try {
1178
- const reminderText = effectivePromptText(
1179
- reminder.promptText,
1180
- reminder.plainTextFallback,
1181
- sourceChannel,
1182
- );
1183
- await deliverApprovalPrompt(
1184
- replyCallbackUrl,
1185
- externalChatId,
1186
- reminderText,
1187
- uiMetadata,
1188
- assistantId,
1189
- bearerToken,
1190
- );
1191
- } catch (err) {
1192
- log.error({ err, conversationId: guardianApproval.conversationId }, 'Failed to deliver guardian approval reminder');
1193
- }
1194
- }
1195
-
1196
- return { handled: true, type: 'reminder_sent' };
1197
- }
1198
- }
1199
-
1200
- // ── Standard approval interception (existing flow) ──
1201
- const pendingPrompt = getChannelApprovalPrompt(conversationId);
1202
- if (!pendingPrompt) return { handled: false };
1203
-
1204
- // When the sender is from an unverified channel, auto-deny any pending
1205
- // confirmation and block self-approval.
1206
- if (guardianCtx.actorRole === 'unverified_channel') {
1207
- const pending = getPendingConfirmationsByConversation(conversationId);
1208
- if (pending.length > 0) {
1209
- const denyResult = handleChannelDecision(
1210
- conversationId,
1211
- { action: 'reject', source: 'plain_text' },
1212
- orchestrator,
1213
- buildGuardianDenyContext(
1214
- pending[0].toolName,
1215
- guardianCtx.denialReason ?? 'no_binding',
1216
- sourceChannel,
1217
- ),
1218
- );
1219
- if (denyResult.applied && denyResult.runId) {
1220
- schedulePostDecisionDelivery(
1221
- orchestrator,
1222
- denyResult.runId,
1223
- conversationId,
1224
- externalChatId,
1225
- replyCallbackUrl,
1226
- bearerToken,
1227
- assistantId,
1228
- );
1229
- }
1230
- return { handled: true, type: 'decision_applied' };
1231
- }
1232
- }
1233
-
1234
- // When the sender is a non-guardian and there's a pending guardian approval
1235
- // for this conversation's run, block self-approval. The non-guardian must
1236
- // wait for the guardian to decide.
1237
- if (guardianCtx.actorRole === 'non-guardian') {
1238
- const pending = getPendingConfirmationsByConversation(conversationId);
1239
- if (pending.length > 0) {
1240
- const guardianApprovalForRun = getPendingApprovalForRun(pending[0].runId);
1241
- if (guardianApprovalForRun) {
1242
- try {
1243
- await deliverChannelReply(replyCallbackUrl, {
1244
- chatId: externalChatId,
1245
- text: 'Your request is pending guardian approval. Only the verified guardian can approve or deny this request.',
1246
- assistantId,
1247
- }, bearerToken);
1248
- } catch (err) {
1249
- log.error({ err, conversationId }, 'Failed to deliver guardian-pending notice to requester');
1250
- }
1251
- return { handled: true, type: 'reminder_sent' };
1252
- }
1253
-
1254
- // Check for an expired-but-unresolved guardian approval. If the approval
1255
- // expired without a guardian decision, auto-deny the run and transition
1256
- // the approval to 'expired'. Without this, the requester could bypass
1257
- // guardian-only controls by simply waiting for the TTL to elapse.
1258
- const unresolvedApproval = getUnresolvedApprovalForRun(pending[0].runId);
1259
- if (unresolvedApproval) {
1260
- updateApprovalDecision(unresolvedApproval.id, { status: 'expired' });
1261
-
1262
- // Auto-deny the underlying run so it does not remain actionable
1263
- const expiredDecision: ApprovalDecisionResult = {
1264
- action: 'reject',
1265
- source: 'plain_text',
1266
- };
1267
- handleChannelDecision(conversationId, expiredDecision, orchestrator);
1268
-
1269
- try {
1270
- await deliverChannelReply(replyCallbackUrl, {
1271
- chatId: externalChatId,
1272
- text: 'Your guardian approval request has expired and the action has been denied. Please try again.',
1273
- assistantId,
1274
- }, bearerToken);
1275
- } catch (err) {
1276
- log.error({ err, conversationId }, 'Failed to deliver guardian-expiry notice to requester');
1277
- }
1278
- return { handled: true, type: 'decision_applied' };
1279
- }
1280
- }
1281
- }
1282
-
1283
- // Try to extract a decision from callback data (button press) first,
1284
- // then fall back to plain-text parsing.
1285
- let decision: ApprovalDecisionResult | null = null;
1286
-
1287
- if (callbackData) {
1288
- decision = parseCallbackData(callbackData);
1289
- }
1290
-
1291
- if (!decision && content) {
1292
- decision = parseApprovalDecision(content);
1293
- }
1294
-
1295
- if (decision) {
1296
- // When the decision came from a callback button, validate that the embedded
1297
- // run ID matches the currently pending run. A stale button (from a previous
1298
- // approval prompt) must not apply to a different pending run.
1299
- if (decision.runId) {
1300
- const pending = getPendingConfirmationsByConversation(conversationId);
1301
- if (pending.length === 0 || pending[0].runId !== decision.runId) {
1302
- log.warn(
1303
- { conversationId, callbackRunId: decision.runId, pendingRunId: pending[0]?.runId },
1304
- 'Callback run ID does not match pending run, ignoring stale button press',
1305
- );
1306
- return { handled: true, type: 'stale_ignored' };
1307
- }
1308
- }
1309
-
1310
- const result = handleChannelDecision(conversationId, decision, orchestrator);
1311
-
1312
- // Schedule a background poll for run terminal state and deliver the reply.
1313
- // This handles the case where the original poll in
1314
- // processChannelMessageWithApprovals has already exited due to timeout.
1315
- // The claimRunDelivery guard ensures at-most-once delivery when both
1316
- // pollers race to terminal state.
1317
- if (result.applied && result.runId) {
1318
- schedulePostDecisionDelivery(
1319
- orchestrator,
1320
- result.runId,
1321
- conversationId,
1322
- externalChatId,
1323
- replyCallbackUrl,
1324
- bearerToken,
1325
- assistantId,
1326
- );
1327
- }
1328
-
1329
- return { handled: true, type: 'decision_applied' };
1330
- }
1331
-
1332
- // The message is not a decision — send a reminder with the approval buttons.
1333
- const reminder = buildReminderPrompt(pendingPrompt);
1334
- const pending = getPendingConfirmationsByConversation(conversationId);
1335
- if (pending.length > 0) {
1336
- const uiMetadata = buildApprovalUIMetadata(reminder, pending[0]);
1337
- try {
1338
- const reminderText = effectivePromptText(
1339
- reminder.promptText,
1340
- reminder.plainTextFallback,
1341
- sourceChannel,
1342
- );
1343
- await deliverApprovalPrompt(
1344
- replyCallbackUrl,
1345
- externalChatId,
1346
- reminderText,
1347
- uiMetadata,
1348
- assistantId,
1349
- bearerToken,
1350
- );
1351
- } catch (err) {
1352
- log.error({ err, conversationId }, 'Failed to deliver approval reminder');
1353
- }
1354
- }
1355
-
1356
- return { handled: true, type: 'reminder_sent' };
1357
- }
1358
-
1359
- /**
1360
- * Fire-and-forget: after a decision is applied via `handleApprovalInterception`,
1361
- * poll the run briefly for terminal state and deliver the final reply. This
1362
- * handles the case where the original poll in `processChannelMessageWithApprovals`
1363
- * has already exited due to the 5-minute timeout.
1364
- *
1365
- * Uses the same `claimRunDelivery` guard as the main poll to guarantee
1366
- * at-most-once delivery: whichever poller reaches terminal state first
1367
- * claims the delivery, and the other silently skips it.
1368
- */
1369
- function schedulePostDecisionDelivery(
1370
- orchestrator: RunOrchestrator,
1371
- runId: string,
1372
- conversationId: string,
1373
- externalChatId: string,
1374
- replyCallbackUrl: string,
1375
- bearerToken?: string,
1376
- assistantId?: string,
1377
- ): void {
1378
- (async () => {
1379
- try {
1380
- const startTime = Date.now();
1381
- while (Date.now() - startTime < POST_DECISION_POLL_MAX_WAIT_MS) {
1382
- await new Promise((resolve) => setTimeout(resolve, POST_DECISION_POLL_INTERVAL_MS));
1383
- const current = orchestrator.getRun(runId);
1384
- if (!current) break;
1385
- if (current.status === 'completed' || current.status === 'failed') {
1386
- if (channelDeliveryStore.claimRunDelivery(runId)) {
1387
- try {
1388
- await deliverReplyViaCallback(
1389
- conversationId,
1390
- externalChatId,
1391
- replyCallbackUrl,
1392
- bearerToken,
1393
- assistantId,
1394
- );
1395
- } catch (deliveryErr) {
1396
- channelDeliveryStore.resetRunDeliveryClaim(runId);
1397
- throw deliveryErr;
1398
- }
1399
- }
1400
- return;
1401
- }
1402
- }
1403
- log.warn(
1404
- { runId, conversationId },
1405
- 'Post-decision delivery poll timed out without run reaching terminal state',
1406
- );
1407
- } catch (err) {
1408
- log.error({ err, runId, conversationId }, 'Post-decision delivery failed');
1409
- }
1410
- })();
1411
- }
1412
-
1413
- async function deliverReplyViaCallback(
1414
- conversationId: string,
1415
- externalChatId: string,
1416
- callbackUrl: string,
1417
- bearerToken?: string,
1418
- assistantId?: string,
1419
- ): Promise<void> {
1420
- const msgs = conversationStore.getMessages(conversationId);
1421
- for (let i = msgs.length - 1; i >= 0; i--) {
1422
- if (msgs[i].role === 'assistant') {
1423
- let parsed: unknown;
1424
- try { parsed = JSON.parse(msgs[i].content); } catch { parsed = msgs[i].content; }
1425
- const rendered = renderHistoryContent(parsed);
1426
-
1427
- const linked = attachmentsStore.getAttachmentMetadataForMessage(msgs[i].id);
1428
- const replyAttachments: RuntimeAttachmentMetadata[] = linked.map((a) => ({
1429
- id: a.id,
1430
- filename: a.originalFilename,
1431
- mimeType: a.mimeType,
1432
- sizeBytes: a.sizeBytes,
1433
- kind: a.kind,
1434
- }));
1435
-
1436
- if (rendered.text || replyAttachments.length > 0) {
1437
- await deliverChannelReply(callbackUrl, {
1438
- chatId: externalChatId,
1439
- text: rendered.text || undefined,
1440
- attachments: replyAttachments.length > 0 ? replyAttachments : undefined,
1441
- assistantId,
1442
- }, bearerToken);
1443
- }
1444
- break;
1445
- }
1446
- }
1447
- }
1448
-
1449
- export function handleListDeadLetters(): Response {
1450
- const events = channelDeliveryStore.getDeadLetterEvents();
1451
- return Response.json({ events });
1452
- }
1453
-
1454
- export async function handleReplayDeadLetters(req: Request): Promise<Response> {
1455
- const body = await req.json() as { eventIds?: string[] };
1456
- const eventIds = body.eventIds;
1457
-
1458
- if (!Array.isArray(eventIds) || eventIds.length === 0) {
1459
- return Response.json({ error: 'eventIds array is required' }, { status: 400 });
1460
- }
1461
-
1462
- const replayed = channelDeliveryStore.replayDeadLetters(eventIds);
1463
- return Response.json({ replayed });
1464
- }
1465
-
1466
- export async function handleChannelDeliveryAck(req: Request): Promise<Response> {
1467
- const body = await req.json() as {
1468
- sourceChannel?: string;
1469
- externalChatId?: string;
1470
- externalMessageId?: string;
1471
- };
1472
-
1473
- const { sourceChannel, externalChatId, externalMessageId } = body;
1474
-
1475
- if (!sourceChannel || typeof sourceChannel !== 'string') {
1476
- return Response.json({ error: 'sourceChannel is required' }, { status: 400 });
1477
- }
1478
- if (!externalChatId || typeof externalChatId !== 'string') {
1479
- return Response.json({ error: 'externalChatId is required' }, { status: 400 });
1480
- }
1481
- if (!externalMessageId || typeof externalMessageId !== 'string') {
1482
- return Response.json({ error: 'externalMessageId is required' }, { status: 400 });
1483
- }
1484
-
1485
- const acked = channelDeliveryStore.acknowledgeDelivery(
1486
- sourceChannel,
1487
- externalChatId,
1488
- externalMessageId,
1489
- );
1490
-
1491
- if (!acked) {
1492
- return Response.json({ error: 'Inbound event not found' }, { status: 404 });
1493
- }
1494
-
1495
- return new Response(null, { status: 204 });
1496
- }
1497
-
1498
- // ---------------------------------------------------------------------------
1499
- // Proactive guardian approval expiry sweep
1500
- // ---------------------------------------------------------------------------
1501
-
1502
- /** Interval at which the expiry sweep runs (60 seconds). */
1503
- const GUARDIAN_EXPIRY_SWEEP_INTERVAL_MS = 60_000;
1504
-
1505
- /** Timer handle for the expiry sweep so it can be stopped in tests. */
1506
- let expirySweepTimer: ReturnType<typeof setInterval> | null = null;
1507
-
1508
- /**
1509
- * Sweep expired guardian approval requests, auto-deny the underlying runs,
1510
- * and notify both the requester and guardian. This runs proactively on a
1511
- * timer so expired approvals are closed without waiting for follow-up
1512
- * traffic from either party.
2
+ * Barrel re-export for channel route modules.
1513
3
  *
1514
- * Accepts a `gatewayBaseUrl` rather than a fixed delivery URL so that
1515
- * each approval's notification is routed to the correct channel-specific
1516
- * endpoint (e.g. `/deliver/telegram`, `/deliver/sms`).
1517
- */
1518
- export function sweepExpiredGuardianApprovals(
1519
- orchestrator: RunOrchestrator,
1520
- gatewayBaseUrl: string,
1521
- bearerToken?: string,
1522
- ): void {
1523
- const expired = getExpiredPendingApprovals();
1524
- for (const approval of expired) {
1525
- // Mark the approval as expired
1526
- updateApprovalDecision(approval.id, { status: 'expired' });
1527
-
1528
- // Auto-deny the underlying run
1529
- const expiredDecision: ApprovalDecisionResult = {
1530
- action: 'reject',
1531
- source: 'plain_text',
1532
- };
1533
- handleChannelDecision(approval.conversationId, expiredDecision, orchestrator);
1534
-
1535
- // Construct the per-channel delivery URL from the approval's channel
1536
- const deliverUrl = `${gatewayBaseUrl}/deliver/${approval.channel}`;
1537
-
1538
- // Notify the requester that the approval expired
1539
- deliverChannelReply(deliverUrl, {
1540
- chatId: approval.requesterChatId,
1541
- text: `Your guardian approval request for "${approval.toolName}" has expired and the action has been denied. Please try again.`,
1542
- assistantId: approval.assistantId,
1543
- }, bearerToken).catch((err) => {
1544
- log.error({ err, runId: approval.runId }, 'Failed to notify requester of guardian approval expiry');
1545
- });
1546
-
1547
- // Notify the guardian that the approval expired
1548
- deliverChannelReply(deliverUrl, {
1549
- chatId: approval.guardianChatId,
1550
- text: `The approval request for "${approval.toolName}" from user ${approval.requesterExternalUserId} has expired and was automatically denied.`,
1551
- assistantId: approval.assistantId,
1552
- }, bearerToken).catch((err) => {
1553
- log.error({ err, runId: approval.runId }, 'Failed to notify guardian of approval expiry');
1554
- });
1555
-
1556
- log.info(
1557
- { runId: approval.runId, approvalId: approval.id },
1558
- 'Auto-denied expired guardian approval request',
1559
- );
1560
- }
1561
- }
1562
-
1563
- /**
1564
- * Start the periodic expiry sweep. Idempotent — calling it multiple times
1565
- * re-uses the same timer.
1566
- */
1567
- export function startGuardianExpirySweep(
1568
- orchestrator: RunOrchestrator,
1569
- gatewayBaseUrl: string,
1570
- bearerToken?: string,
1571
- ): void {
1572
- if (expirySweepTimer) return;
1573
- expirySweepTimer = setInterval(() => {
1574
- try {
1575
- sweepExpiredGuardianApprovals(orchestrator, gatewayBaseUrl, bearerToken);
1576
- } catch (err) {
1577
- log.error({ err }, 'Guardian expiry sweep failed');
1578
- }
1579
- }, GUARDIAN_EXPIRY_SWEEP_INTERVAL_MS);
1580
- }
1581
-
1582
- /**
1583
- * Stop the periodic expiry sweep. Used in tests and shutdown.
1584
- */
1585
- export function stopGuardianExpirySweep(): void {
1586
- if (expirySweepTimer) {
1587
- clearInterval(expirySweepTimer);
1588
- expirySweepTimer = null;
1589
- }
1590
- }
4
+ * The implementation is split across:
5
+ * - channel-route-shared.ts — types, constants, shared utilities
6
+ * - channel-inbound-routes.ts — inbound message handling, conversation deletion
7
+ * - channel-delivery-routes.ts — delivery ack, dead letters, reply delivery
8
+ * - channel-guardian-routes.ts — guardian approval interception, expiry sweep
9
+ */
10
+ export {
11
+ GATEWAY_ORIGIN_HEADER,
12
+ verifyGatewayOrigin,
13
+ type ActorRole,
14
+ type DenialReason,
15
+ type GuardianContext,
16
+ _setTestPollMaxWait,
17
+ } from './channel-route-shared.js';
18
+
19
+ export {
20
+ handleDeleteConversation,
21
+ handleChannelInbound,
22
+ } from './channel-inbound-routes.js';
23
+
24
+ export {
25
+ handleListDeadLetters,
26
+ handleReplayDeadLetters,
27
+ handleChannelDeliveryAck,
28
+ } from './channel-delivery-routes.js';
29
+
30
+ export {
31
+ sweepExpiredGuardianApprovals,
32
+ startGuardianExpirySweep,
33
+ stopGuardianExpirySweep,
34
+ } from './channel-guardian-routes.js';