@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,17 +1,66 @@
1
1
  /**
2
2
  * Shared types for the runtime HTTP server and its route handlers.
3
3
  */
4
+ import type { ChannelId } from '../channels/types.js';
4
5
  import type { RunOrchestrator } from './run-orchestrator.js';
5
6
  import type { GuardianRuntimeContext } from '../daemon/session-runtime-assembly.js';
7
+ import type { ApprovalMessageContext, ComposeApprovalMessageGenerativeOptions } from './approval-message-composer.js';
8
+
9
+ /**
10
+ * Daemon-injected function that generates approval copy using a provider.
11
+ * Returns generated text or `null` on failure (caller falls back to deterministic text).
12
+ */
13
+ export type ApprovalCopyGenerator = (
14
+ context: ApprovalMessageContext,
15
+ options?: ComposeApprovalMessageGenerativeOptions,
16
+ ) => Promise<string | null>;
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Approval conversation flow types
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /** The disposition returned by the approval conversation engine. */
23
+ export type ApprovalConversationDisposition =
24
+ | 'keep_pending'
25
+ | 'approve_once'
26
+ | 'approve_always'
27
+ | 'reject';
28
+
29
+ /** Structured result from a single turn of the approval conversation. */
30
+ export interface ApprovalConversationResult {
31
+ disposition: ApprovalConversationDisposition;
32
+ replyText: string;
33
+ /** Required when there are multiple pending approvals and the disposition is decision-bearing. */
34
+ targetRunId?: string;
35
+ }
36
+
37
+ /** Input context for the approval conversation engine. */
38
+ export interface ApprovalConversationContext {
39
+ toolName: string;
40
+ allowedActions: string[];
41
+ role: 'requester' | 'guardian';
42
+ pendingApprovals: Array<{ runId: string; toolName: string }>;
43
+ userMessage: string;
44
+ }
45
+
46
+ /**
47
+ * Daemon-injected function that processes one turn of an approval conversation.
48
+ * Takes conversation context and returns a structured approval decision + reply.
49
+ */
50
+ export type ApprovalConversationGenerator = (
51
+ context: ApprovalConversationContext,
52
+ ) => Promise<ApprovalConversationResult>;
6
53
 
7
54
  export interface RuntimeMessageSessionOptions {
8
55
  transport?: {
9
- channelId: string;
56
+ channelId: ChannelId;
10
57
  hints?: string[];
11
58
  uxBrief?: string;
12
59
  };
13
60
  assistantId?: string;
14
61
  guardianContext?: GuardianRuntimeContext;
62
+ /** Channel command intent metadata (e.g. Telegram /start). */
63
+ commandIntent?: { type: string; payload?: string; languageCode?: string };
15
64
  }
16
65
 
17
66
  export type MessageProcessor = (
@@ -19,7 +68,7 @@ export type MessageProcessor = (
19
68
  content: string,
20
69
  attachmentIds?: string[],
21
70
  options?: RuntimeMessageSessionOptions,
22
- sourceChannel?: string,
71
+ sourceChannel?: ChannelId,
23
72
  ) => Promise<{ messageId: string }>;
24
73
 
25
74
  /**
@@ -32,7 +81,7 @@ export type NonBlockingMessageProcessor = (
32
81
  content: string,
33
82
  attachmentIds?: string[],
34
83
  options?: RuntimeMessageSessionOptions,
35
- sourceChannel?: string,
84
+ sourceChannel?: ChannelId,
36
85
  ) => Promise<{ messageId: string }>;
37
86
 
38
87
  export interface RuntimeHttpServerOptions {
@@ -48,6 +97,10 @@ export interface RuntimeHttpServerOptions {
48
97
  runOrchestrator?: RunOrchestrator;
49
98
  /** Root directory for interface files on disk. */
50
99
  interfacesDir?: string;
100
+ /** Daemon-injected generator for approval copy (provider-backed). */
101
+ approvalCopyGenerator?: ApprovalCopyGenerator;
102
+ /** Daemon-injected generator for conversational approval flow (provider-backed). */
103
+ approvalConversationGenerator?: ApprovalConversationGenerator;
51
104
  }
52
105
 
53
106
  export interface RuntimeAttachmentMetadata {
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Auth middleware: bearer token validation, private network checks,
3
+ * and gateway-origin verification.
4
+ */
5
+
6
+ import { timingSafeEqual } from 'node:crypto';
7
+
8
+ /**
9
+ * Constant-time comparison of two bearer tokens to prevent timing attacks.
10
+ */
11
+ export function verifyBearerToken(provided: string, expected: string): boolean {
12
+ const a = Buffer.from(provided);
13
+ const b = Buffer.from(expected);
14
+ if (a.length !== b.length) return false;
15
+ return timingSafeEqual(a, b);
16
+ }
17
+
18
+ /**
19
+ * Check if a hostname is a loopback address.
20
+ */
21
+ export function isLoopbackHost(hostname: string): boolean {
22
+ return hostname === '127.0.0.1' || hostname === '::1' || hostname === 'localhost';
23
+ }
24
+
25
+ /**
26
+ * @internal Exported for testing.
27
+ *
28
+ * Determine whether an IP address string belongs to a private/internal
29
+ * network range:
30
+ * - Loopback: 127.0.0.0/8, ::1
31
+ * - RFC 1918: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
32
+ * - Link-local: 169.254.0.0/16
33
+ * - IPv6 unique local: fc00::/7 (fc00::--fdff::)
34
+ * - IPv4-mapped IPv6 variants of all of the above (::ffff:x.x.x.x)
35
+ */
36
+ export function isPrivateAddress(addr: string): boolean {
37
+ // Handle IPv4-mapped IPv6 (e.g. ::ffff:10.0.0.1) -- extract the IPv4 part
38
+ const v4Mapped = addr.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i);
39
+ const normalized = v4Mapped ? v4Mapped[1] : addr;
40
+
41
+ // IPv4 checks
42
+ if (normalized.includes('.')) {
43
+ const parts = normalized.split('.').map(Number);
44
+ if (parts.length !== 4 || parts.some(p => isNaN(p) || p < 0 || p > 255)) return false;
45
+
46
+ // Loopback: 127.0.0.0/8
47
+ if (parts[0] === 127) return true;
48
+ // 10.0.0.0/8
49
+ if (parts[0] === 10) return true;
50
+ // 172.16.0.0/12 (172.16.x.x -- 172.31.x.x)
51
+ if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true;
52
+ // 192.168.0.0/16
53
+ if (parts[0] === 192 && parts[1] === 168) return true;
54
+ // Link-local: 169.254.0.0/16
55
+ if (parts[0] === 169 && parts[1] === 254) return true;
56
+
57
+ return false;
58
+ }
59
+
60
+ // IPv6 checks
61
+ const lower = normalized.toLowerCase();
62
+ // Loopback
63
+ if (lower === '::1') return true;
64
+ // Unique local: fc00::/7 (fc00:: through fdff::)
65
+ if (lower.startsWith('fc') || lower.startsWith('fd')) return true;
66
+ // Link-local: fe80::/10
67
+ if (lower.startsWith('fe80')) return true;
68
+
69
+ return false;
70
+ }
71
+
72
+ /**
73
+ * Check if the actual peer/remote address of a connection is from a
74
+ * private/internal network. Uses Bun's server.requestIP() to get the
75
+ * real peer address, which cannot be spoofed unlike the Origin header.
76
+ *
77
+ * Accepts loopback, RFC 1918 private IPv4, link-local, and RFC 4193
78
+ * unique-local IPv6 -- including their IPv4-mapped IPv6 forms. This
79
+ * supports container/pod deployments (e.g. Kubernetes sidecars) where
80
+ * gateway and runtime communicate over pod-internal private IPs.
81
+ */
82
+ export function isPrivateNetworkPeer(server: { requestIP(req: Request): { address: string; family: string; port: number } | null }, req: Request): boolean {
83
+ const ip = server.requestIP(req);
84
+ if (!ip) return false;
85
+ return isPrivateAddress(ip.address);
86
+ }
87
+
88
+ /**
89
+ * Check if a request origin is from a private/internal network address.
90
+ * Extracts the hostname from the Origin header and validates it against
91
+ * isPrivateAddress(), consistent with the isPrivateNetworkPeer check.
92
+ */
93
+ export function isPrivateNetworkOrigin(req: Request): boolean {
94
+ const origin = req.headers.get('origin');
95
+ // No origin header (e.g., server-initiated or same-origin) -- allow
96
+ if (!origin) return true;
97
+ try {
98
+ const url = new URL(origin);
99
+ const host = url.hostname;
100
+ if (host === 'localhost') return true;
101
+ // URL.hostname wraps IPv6 addresses in brackets (e.g. "[::1]") -- strip them
102
+ const rawHost = host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;
103
+ return isPrivateAddress(rawHost);
104
+ } catch {
105
+ return false;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Extract and validate a bearer token from the Authorization header.
111
+ * Returns the token string if present, or null.
112
+ */
113
+ export function extractBearerToken(req: Request): string | null {
114
+ const authHeader = req.headers.get('authorization');
115
+ return authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null;
116
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Centralized error handling for runtime HTTP request dispatch.
3
+ */
4
+
5
+ import { ConfigError, IngressBlockedError } from '../../util/errors.js';
6
+ import { getLogger } from '../../util/logger.js';
7
+
8
+ const log = getLogger('runtime-http');
9
+
10
+ /**
11
+ * Wrap an async endpoint handler with standard error handling.
12
+ * Catches IngressBlockedError (422), ConfigError (422), and generic errors (500).
13
+ */
14
+ export async function withErrorHandling(
15
+ endpoint: string,
16
+ handler: () => Promise<Response>,
17
+ ): Promise<Response> {
18
+ try {
19
+ return await handler();
20
+ } catch (err) {
21
+ if (err instanceof IngressBlockedError) {
22
+ log.warn({ endpoint, detectedTypes: err.detectedTypes }, 'Blocked HTTP request containing secrets');
23
+ return Response.json({ error: err.message, code: err.code }, { status: 422 });
24
+ }
25
+ if (err instanceof ConfigError) {
26
+ log.warn({ err, endpoint }, 'Runtime HTTP config error');
27
+ return Response.json({ error: err.message, code: err.code }, { status: 422 });
28
+ }
29
+ log.error({ err, endpoint }, 'Runtime HTTP handler error');
30
+ const message = err instanceof Error ? err.message : 'Internal server error';
31
+ return Response.json({ error: message }, { status: 500 });
32
+ }
33
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Twilio webhook signature validation and related constants.
3
+ */
4
+
5
+ import { getLogger } from '../../util/logger.js';
6
+ import { isTwilioWebhookValidationDisabled } from '../../config/env.js';
7
+ import { TwilioConversationRelayProvider } from '../../calls/twilio-provider.js';
8
+ import { loadConfig } from '../../config/loader.js';
9
+ import { getPublicBaseUrl } from '../../inbound/public-ingress-urls.js';
10
+
11
+ const log = getLogger('runtime-http');
12
+
13
+ /**
14
+ * Regex to extract the Twilio webhook subpath from both top-level and
15
+ * assistant-scoped route shapes:
16
+ * /v1/calls/twilio/<subpath>
17
+ * /v1/assistants/<id>/calls/twilio/<subpath>
18
+ */
19
+ export const TWILIO_WEBHOOK_RE = /^\/v1\/(?:assistants\/[^/]+\/)?calls\/twilio\/(.+)$/;
20
+
21
+ /**
22
+ * Gateway-compatible Twilio webhook paths:
23
+ * /webhooks/twilio/<subpath>
24
+ *
25
+ * Maps gateway path segments to the internal subpath names used by the
26
+ * dispatcher below (e.g. "voice" -> "voice-webhook").
27
+ */
28
+ export const TWILIO_GATEWAY_WEBHOOK_RE = /^\/webhooks\/twilio\/(.+)$/;
29
+ export const GATEWAY_SUBPATH_MAP: Record<string, string> = {
30
+ voice: 'voice-webhook',
31
+ status: 'status',
32
+ 'connect-action': 'connect-action',
33
+ sms: 'sms',
34
+ };
35
+
36
+ /**
37
+ * Direct Twilio webhook subpaths that are blocked in gateway_only mode.
38
+ * Includes all public-facing webhook paths (voice, status, connect-action, SMS)
39
+ * because the runtime must never serve as a direct ingress for external webhooks.
40
+ * Internal forwarding endpoints (gateway->runtime) are unaffected.
41
+ */
42
+ export const GATEWAY_ONLY_BLOCKED_SUBPATHS = new Set(['voice-webhook', 'status', 'connect-action', 'sms']);
43
+
44
+ /**
45
+ * Validate a Twilio webhook request's X-Twilio-Signature header.
46
+ *
47
+ * Returns the raw body text on success so callers can reconstruct the Request
48
+ * for downstream handlers (which also need to read the body).
49
+ * Returns a 403 Response if signature validation fails.
50
+ *
51
+ * Fail-closed: if the auth token is not configured, the request is rejected
52
+ * with 403 rather than silently skipping validation. An explicit local-dev
53
+ * bypass is available via TWILIO_WEBHOOK_VALIDATION_DISABLED=true.
54
+ */
55
+ export async function validateTwilioWebhook(
56
+ req: Request,
57
+ ): Promise<{ body: string } | Response> {
58
+ const rawBody = await req.text();
59
+
60
+ // Allow explicit local-dev bypass -- must be exactly "true"
61
+ if (isTwilioWebhookValidationDisabled()) {
62
+ log.warn('Twilio webhook signature validation explicitly disabled via TWILIO_WEBHOOK_VALIDATION_DISABLED');
63
+ return { body: rawBody };
64
+ }
65
+
66
+ const authToken = TwilioConversationRelayProvider.getAuthToken();
67
+
68
+ // Fail-closed: reject if no auth token is configured
69
+ if (!authToken) {
70
+ log.error('Twilio auth token not configured — rejecting webhook request (fail-closed)');
71
+ return Response.json({ error: 'Forbidden' }, { status: 403 });
72
+ }
73
+
74
+ const signature = req.headers.get('x-twilio-signature');
75
+ if (!signature) {
76
+ log.warn('Twilio webhook request missing X-Twilio-Signature header');
77
+ return Response.json({ error: 'Forbidden' }, { status: 403 });
78
+ }
79
+
80
+ // Parse form-urlencoded body into key-value params for signature computation
81
+ const params: Record<string, string> = {};
82
+ const formData = new URLSearchParams(rawBody);
83
+ for (const [key, value] of formData.entries()) {
84
+ params[key] = value;
85
+ }
86
+
87
+ // Reconstruct the public-facing URL that Twilio signed against.
88
+ // Behind proxies/gateways, req.url is the local server URL (e.g.
89
+ // http://127.0.0.1:7821/...) which differs from the public URL Twilio
90
+ // used to compute the HMAC-SHA1 signature.
91
+ let publicBaseUrl: string | undefined;
92
+ try {
93
+ publicBaseUrl = getPublicBaseUrl(loadConfig());
94
+ } catch {
95
+ // No webhook base URL configured -- fall back to using req.url as-is
96
+ }
97
+ const parsedUrl = new URL(req.url);
98
+ const publicUrl = publicBaseUrl
99
+ ? publicBaseUrl + parsedUrl.pathname + parsedUrl.search
100
+ : req.url;
101
+
102
+ const isValid = TwilioConversationRelayProvider.verifyWebhookSignature(
103
+ publicUrl,
104
+ params,
105
+ signature,
106
+ authToken,
107
+ );
108
+
109
+ if (!isValid) {
110
+ log.warn('Twilio webhook signature validation failed');
111
+ return Response.json({ error: 'Forbidden' }, { status: 403 });
112
+ }
113
+
114
+ return { body: rawBody };
115
+ }
116
+
117
+ /**
118
+ * Re-create a Request with the same method, headers, and URL but with a
119
+ * pre-read body string so downstream handlers can call req.text() again.
120
+ */
121
+ export function cloneRequestWithBody(original: Request, body: string): Request {
122
+ return new Request(original.url, {
123
+ method: original.method,
124
+ headers: original.headers,
125
+ body,
126
+ });
127
+ }
@@ -22,7 +22,7 @@ const HTML_ESCAPE_MAP: Record<string, string> = {
22
22
  let designSystemCssCache: string | null = null;
23
23
 
24
24
  function loadDesignSystemCss(): string {
25
- if (designSystemCssCache !== null) return designSystemCssCache;
25
+ if (designSystemCssCache != null) return designSystemCssCache;
26
26
  try {
27
27
  const cssPath = join(
28
28
  import.meta.dirname ?? __dirname,
@@ -12,10 +12,33 @@ import { startCall, getCallStatus, cancelCall, answerCall, relayInstruction } fr
12
12
  import { getConfig } from '../../config/loader.js';
13
13
  import { VALID_CALLER_IDENTITY_MODES } from '../../config/schema.js';
14
14
 
15
+ // ── Idempotency cache ─────────────────────────────────────────────────────────
16
+ // Stores serialized 201 responses keyed by idempotencyKey for 5 minutes so
17
+ // that network-retry duplicates from the client don't start a second call.
18
+
19
+ const IDEMPOTENCY_TTL_MS = 5 * 60 * 1000; // 5 minutes
20
+
21
+ interface IdempotencyEntry {
22
+ body: unknown;
23
+ expiresAt: number;
24
+ }
25
+
26
+ const idempotencyCache = new Map<string, IdempotencyEntry>();
27
+
28
+ function pruneIdempotencyCache(): void {
29
+ const now = Date.now();
30
+ for (const [key, entry] of idempotencyCache) {
31
+ if (entry.expiresAt <= now) idempotencyCache.delete(key);
32
+ }
33
+ }
34
+
15
35
  /**
16
36
  * POST /v1/calls/start
17
37
  *
18
- * Body: { phoneNumber: string; task: string; context?: string; conversationId: string; callerIdentityMode?: 'assistant_number' | 'user_number' }
38
+ * Body: { phoneNumber: string; task: string; context?: string; conversationId: string; callerIdentityMode?: 'assistant_number' | 'user_number'; idempotencyKey?: string }
39
+ *
40
+ * Optional `idempotencyKey`: if supplied, duplicate requests with the same key
41
+ * within 5 minutes return the cached 201 response without starting a second call.
19
42
  */
20
43
  export async function handleStartCall(req: Request, assistantId: string = 'self'): Promise<Response> {
21
44
  if (!getConfig().calls.enabled) {
@@ -31,6 +54,7 @@ export async function handleStartCall(req: Request, assistantId: string = 'self'
31
54
  context?: string;
32
55
  conversationId?: string;
33
56
  callerIdentityMode?: 'assistant_number' | 'user_number';
57
+ idempotencyKey?: string;
34
58
  };
35
59
  try {
36
60
  body = await req.json() as typeof body;
@@ -38,7 +62,7 @@ export async function handleStartCall(req: Request, assistantId: string = 'self'
38
62
  return Response.json({ error: 'Invalid JSON in request body' }, { status: 400 });
39
63
  }
40
64
 
41
- if (typeof body !== 'object' || body === null || Array.isArray(body)) {
65
+ if (typeof body !== 'object' || body == null || Array.isArray(body)) {
42
66
  return Response.json({ error: 'Request body must be a JSON object' }, { status: 400 });
43
67
  }
44
68
 
@@ -54,6 +78,19 @@ export async function handleStartCall(req: Request, assistantId: string = 'self'
54
78
  );
55
79
  }
56
80
 
81
+ // Idempotency check: return cached response for duplicate requests
82
+ const idempotencyKey = typeof body.idempotencyKey === 'string' && body.idempotencyKey
83
+ ? body.idempotencyKey
84
+ : null;
85
+
86
+ if (idempotencyKey) {
87
+ pruneIdempotencyCache();
88
+ const cached = idempotencyCache.get(idempotencyKey);
89
+ if (cached && cached.expiresAt > Date.now()) {
90
+ return Response.json(cached.body, { status: 201 });
91
+ }
92
+ }
93
+
57
94
  const result = await startCall({
58
95
  phoneNumber: body.phoneNumber ?? '',
59
96
  task: body.task ?? '',
@@ -67,14 +104,20 @@ export async function handleStartCall(req: Request, assistantId: string = 'self'
67
104
  return Response.json({ error: result.error }, { status: result.status ?? 500 });
68
105
  }
69
106
 
70
- return Response.json({
107
+ const responseBody = {
71
108
  callSessionId: result.session.id,
72
109
  callSid: result.callSid,
73
110
  status: result.session.status,
74
111
  toNumber: result.session.toNumber,
75
112
  fromNumber: result.session.fromNumber,
76
113
  callerIdentityMode: result.callerIdentityMode,
77
- }, { status: 201 });
114
+ };
115
+
116
+ if (idempotencyKey) {
117
+ idempotencyCache.set(idempotencyKey, { body: responseBody, expiresAt: Date.now() + IDEMPOTENCY_TTL_MS });
118
+ }
119
+
120
+ return Response.json(responseBody, { status: 201 });
78
121
  }
79
122
 
80
123
  /**
@@ -145,7 +188,7 @@ export async function handleAnswerCall(req: Request, callSessionId: string): Pro
145
188
  return Response.json({ error: 'Invalid JSON in request body' }, { status: 400 });
146
189
  }
147
190
 
148
- if (typeof body !== 'object' || body === null || Array.isArray(body)) {
191
+ if (typeof body !== 'object' || body == null || Array.isArray(body)) {
149
192
  return Response.json({ error: 'Request body must be a JSON object' }, { status: 400 });
150
193
  }
151
194
 
@@ -174,7 +217,7 @@ export async function handleInstructionCall(req: Request, callSessionId: string)
174
217
  return Response.json({ error: 'Invalid JSON in request body' }, { status: 400 });
175
218
  }
176
219
 
177
- if (typeof body !== 'object' || body === null || Array.isArray(body)) {
220
+ if (typeof body !== 'object' || body == null || Array.isArray(body)) {
178
221
  return Response.json({ error: 'Request body must be a JSON object' }, { status: 400 });
179
222
  }
180
223
 
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Channel delivery routes: delivery ack, dead letters, reply delivery,
3
+ * and post-decision delivery scheduling.
4
+ */
5
+ import * as conversationStore from '../../memory/conversation-store.js';
6
+ import * as attachmentsStore from '../../memory/attachments-store.js';
7
+ import * as channelDeliveryStore from '../../memory/channel-delivery-store.js';
8
+ import { renderHistoryContent } from '../../daemon/handlers.js';
9
+ import { getLogger } from '../../util/logger.js';
10
+ import { deliverChannelReply } from '../gateway-client.js';
11
+ import type { RuntimeAttachmentMetadata } from '../http-types.js';
12
+ import type { RunOrchestrator } from '../run-orchestrator.js';
13
+ import { POST_DECISION_POLL_INTERVAL_MS, POST_DECISION_POLL_MAX_WAIT_MS } from './channel-route-shared.js';
14
+
15
+ const log = getLogger('runtime-http');
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Dead letter management
19
+ // ---------------------------------------------------------------------------
20
+
21
+ export function handleListDeadLetters(): Response {
22
+ const events = channelDeliveryStore.getDeadLetterEvents();
23
+ return Response.json({ events });
24
+ }
25
+
26
+ export async function handleReplayDeadLetters(req: Request): Promise<Response> {
27
+ const body = await req.json() as { eventIds?: string[] };
28
+ const eventIds = body.eventIds;
29
+
30
+ if (!Array.isArray(eventIds) || eventIds.length === 0) {
31
+ return Response.json({ error: 'eventIds array is required' }, { status: 400 });
32
+ }
33
+
34
+ const replayed = channelDeliveryStore.replayDeadLetters(eventIds);
35
+ return Response.json({ replayed });
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Delivery acknowledgement
40
+ // ---------------------------------------------------------------------------
41
+
42
+ export async function handleChannelDeliveryAck(req: Request): Promise<Response> {
43
+ const body = await req.json() as {
44
+ sourceChannel?: string;
45
+ externalChatId?: string;
46
+ externalMessageId?: string;
47
+ };
48
+
49
+ const { sourceChannel, externalChatId, externalMessageId } = body;
50
+
51
+ if (!sourceChannel || typeof sourceChannel !== 'string') {
52
+ return Response.json({ error: 'sourceChannel is required' }, { status: 400 });
53
+ }
54
+ if (!externalChatId || typeof externalChatId !== 'string') {
55
+ return Response.json({ error: 'externalChatId is required' }, { status: 400 });
56
+ }
57
+ if (!externalMessageId || typeof externalMessageId !== 'string') {
58
+ return Response.json({ error: 'externalMessageId is required' }, { status: 400 });
59
+ }
60
+
61
+ const acked = channelDeliveryStore.acknowledgeDelivery(
62
+ sourceChannel,
63
+ externalChatId,
64
+ externalMessageId,
65
+ );
66
+
67
+ if (!acked) {
68
+ return Response.json({ error: 'Inbound event not found' }, { status: 404 });
69
+ }
70
+
71
+ return new Response(null, { status: 204 });
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Reply delivery via callback
76
+ // ---------------------------------------------------------------------------
77
+
78
+ export async function deliverReplyViaCallback(
79
+ conversationId: string,
80
+ externalChatId: string,
81
+ callbackUrl: string,
82
+ bearerToken?: string,
83
+ assistantId?: string,
84
+ ): Promise<void> {
85
+ const msgs = conversationStore.getMessages(conversationId);
86
+ for (let i = msgs.length - 1; i >= 0; i--) {
87
+ if (msgs[i].role === 'assistant') {
88
+ let parsed: unknown;
89
+ try { parsed = JSON.parse(msgs[i].content); } catch { parsed = msgs[i].content; }
90
+ const rendered = renderHistoryContent(parsed);
91
+
92
+ const linked = attachmentsStore.getAttachmentMetadataForMessage(msgs[i].id);
93
+ const replyAttachments: RuntimeAttachmentMetadata[] = linked.map((a) => ({
94
+ id: a.id,
95
+ filename: a.originalFilename,
96
+ mimeType: a.mimeType,
97
+ sizeBytes: a.sizeBytes,
98
+ kind: a.kind,
99
+ }));
100
+
101
+ if (rendered.text || replyAttachments.length > 0) {
102
+ await deliverChannelReply(callbackUrl, {
103
+ chatId: externalChatId,
104
+ text: rendered.text || undefined,
105
+ attachments: replyAttachments.length > 0 ? replyAttachments : undefined,
106
+ assistantId,
107
+ }, bearerToken);
108
+ }
109
+ break;
110
+ }
111
+ }
112
+ }
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // Post-decision delivery scheduling
116
+ // ---------------------------------------------------------------------------
117
+
118
+ /**
119
+ * Fire-and-forget: after a decision is applied via `handleApprovalInterception`,
120
+ * poll the run briefly for terminal state and deliver the final reply. This
121
+ * handles the case where the original poll in `processChannelMessageWithApprovals`
122
+ * has already exited due to the 5-minute timeout.
123
+ *
124
+ * Uses the same `claimRunDelivery` guard as the main poll to guarantee
125
+ * at-most-once delivery: whichever poller reaches terminal state first
126
+ * claims the delivery, and the other silently skips it.
127
+ */
128
+ export function schedulePostDecisionDelivery(
129
+ orchestrator: RunOrchestrator,
130
+ runId: string,
131
+ conversationId: string,
132
+ externalChatId: string,
133
+ replyCallbackUrl: string,
134
+ bearerToken?: string,
135
+ assistantId?: string,
136
+ ): void {
137
+ (async () => {
138
+ try {
139
+ const startTime = Date.now();
140
+ while (Date.now() - startTime < POST_DECISION_POLL_MAX_WAIT_MS) {
141
+ await new Promise((resolve) => setTimeout(resolve, POST_DECISION_POLL_INTERVAL_MS));
142
+ const current = orchestrator.getRun(runId);
143
+ if (!current) break;
144
+ if (current.status === 'completed' || current.status === 'failed') {
145
+ if (channelDeliveryStore.claimRunDelivery(runId)) {
146
+ try {
147
+ await deliverReplyViaCallback(
148
+ conversationId,
149
+ externalChatId,
150
+ replyCallbackUrl,
151
+ bearerToken,
152
+ assistantId,
153
+ );
154
+ } catch (deliveryErr) {
155
+ channelDeliveryStore.resetRunDeliveryClaim(runId);
156
+ throw deliveryErr;
157
+ }
158
+ }
159
+ return;
160
+ }
161
+ }
162
+ log.warn(
163
+ { runId, conversationId },
164
+ 'Post-decision delivery poll timed out without run reaching terminal state',
165
+ );
166
+ } catch (err) {
167
+ log.error({ err, runId, conversationId }, 'Post-decision delivery failed');
168
+ }
169
+ })();
170
+ }