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