@vellumai/assistant 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (506) hide show
  1. package/Dockerfile +2 -0
  2. package/README.md +88 -2
  3. package/eslint.config.mjs +31 -0
  4. package/package.json +1 -1
  5. package/scripts/ipc/check-swift-decoder-drift.ts +4 -1
  6. package/scripts/ipc/generate-swift.ts +31 -2
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +438 -1
  8. package/src/__tests__/approval-conversation-turn.test.ts +214 -0
  9. package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
  10. package/src/__tests__/approval-message-composer.test.ts +253 -0
  11. package/src/__tests__/browser-manager.test.ts +1 -0
  12. package/src/__tests__/call-conversation-messages.test.ts +130 -0
  13. package/src/__tests__/call-domain.test.ts +12 -2
  14. package/src/__tests__/call-orchestrator.test.ts +799 -249
  15. package/src/__tests__/call-pointer-messages.test.ts +148 -0
  16. package/src/__tests__/call-recovery.test.ts +3 -0
  17. package/src/__tests__/call-routes-http.test.ts +32 -2
  18. package/src/__tests__/call-store.test.ts +3 -0
  19. package/src/__tests__/channel-approval-routes.test.ts +1277 -98
  20. package/src/__tests__/channel-approval.test.ts +37 -0
  21. package/src/__tests__/channel-approvals.test.ts +36 -50
  22. package/src/__tests__/channel-guardian.test.ts +630 -22
  23. package/src/__tests__/channel-readiness-service.test.ts +324 -0
  24. package/src/__tests__/checker.test.ts +14 -7
  25. package/src/__tests__/clarification-resolver.test.ts +44 -24
  26. package/src/__tests__/commit-message-enrichment-service.test.ts +9 -4
  27. package/src/__tests__/computer-use-session-working-dir.test.ts +8 -0
  28. package/src/__tests__/config-schema.test.ts +14 -8
  29. package/src/__tests__/context-window-manager.test.ts +30 -2
  30. package/src/__tests__/contradiction-checker.test.ts +20 -5
  31. package/src/__tests__/credential-security-invariants.test.ts +7 -2
  32. package/src/__tests__/daemon-lifecycle.test.ts +13 -12
  33. package/src/__tests__/db-migration-rollback.test.ts +752 -0
  34. package/src/__tests__/dictation-mode-detection.test.ts +63 -0
  35. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +2 -0
  36. package/src/__tests__/entity-search.test.ts +615 -0
  37. package/src/__tests__/fuzzy-match-property.test.ts +5 -5
  38. package/src/__tests__/guardian-action-store.test.ts +123 -0
  39. package/src/__tests__/guardian-action-sweep.test.ts +277 -0
  40. package/src/__tests__/guardian-dispatch.test.ts +389 -0
  41. package/src/__tests__/guardian-question-copy.test.ts +47 -0
  42. package/src/__tests__/handlers-telegram-config.test.ts +4 -2
  43. package/src/__tests__/handlers-twilio-config.test.ts +533 -0
  44. package/src/__tests__/intent-routing.test.ts +2 -0
  45. package/src/__tests__/ipc-snapshot.test.ts +291 -1
  46. package/src/__tests__/memory-upsert-concurrency.test.ts +828 -0
  47. package/src/__tests__/messaging-send-tool.test.ts +65 -0
  48. package/src/__tests__/model-intents.test.ts +96 -0
  49. package/src/__tests__/no-direct-anthropic-sdk-imports.test.ts +42 -0
  50. package/src/__tests__/oauth2-gateway-transport.test.ts +130 -0
  51. package/src/__tests__/onboarding-starter-tasks.test.ts +2 -0
  52. package/src/__tests__/provider-commit-message-generator.test.ts +89 -13
  53. package/src/__tests__/provider-error-scenarios.test.ts +621 -0
  54. package/src/__tests__/provider-fail-open-selection.test.ts +119 -0
  55. package/src/__tests__/qdrant-manager.test.ts +27 -20
  56. package/src/__tests__/relay-server.test.ts +779 -40
  57. package/src/__tests__/run-orchestrator-assistant-events.test.ts +6 -0
  58. package/src/__tests__/run-orchestrator.test.ts +42 -4
  59. package/src/__tests__/runtime-runs-http.test.ts +17 -1
  60. package/src/__tests__/runtime-runs.test.ts +16 -0
  61. package/src/__tests__/schedule-store.test.ts +18 -4
  62. package/src/__tests__/scheduler-recurrence.test.ts +13 -4
  63. package/src/__tests__/session-abort-tool-results.test.ts +6 -0
  64. package/src/__tests__/session-agent-loop.test.ts +857 -0
  65. package/src/__tests__/session-conflict-gate.test.ts +6 -0
  66. package/src/__tests__/session-pre-run-repair.test.ts +6 -0
  67. package/src/__tests__/session-profile-injection.test.ts +6 -0
  68. package/src/__tests__/session-provider-retry-repair.test.ts +6 -0
  69. package/src/__tests__/session-queue.test.ts +6 -0
  70. package/src/__tests__/session-runtime-assembly.test.ts +321 -13
  71. package/src/__tests__/session-slash-known.test.ts +6 -0
  72. package/src/__tests__/session-slash-queue.test.ts +6 -0
  73. package/src/__tests__/session-slash-unknown.test.ts +6 -0
  74. package/src/__tests__/session-surfaces-task-progress.test.ts +2 -0
  75. package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
  76. package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
  77. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
  78. package/src/__tests__/session-workspace-injection.test.ts +6 -0
  79. package/src/__tests__/session-workspace-tool-tracking.test.ts +6 -0
  80. package/src/__tests__/skills.test.ts +2 -0
  81. package/src/__tests__/sms-messaging-provider.test.ts +126 -0
  82. package/src/__tests__/starter-task-flow.test.ts +2 -0
  83. package/src/__tests__/swarm-dag-pathological.test.ts +535 -0
  84. package/src/__tests__/system-prompt.test.ts +2 -0
  85. package/src/__tests__/task-management-tools.test.ts +2 -2
  86. package/src/__tests__/task-runner.test.ts +14 -4
  87. package/src/__tests__/terminal-tools.test.ts +25 -19
  88. package/src/__tests__/tool-execution-abort-cleanup.test.ts +545 -0
  89. package/src/__tests__/tool-executor-shell-integration.test.ts +11 -11
  90. package/src/__tests__/tool-executor.test.ts +23 -24
  91. package/src/__tests__/trust-store.test.ts +3 -3
  92. package/src/__tests__/twilio-rest.test.ts +29 -0
  93. package/src/__tests__/twilio-routes-elevenlabs.test.ts +3 -0
  94. package/src/__tests__/twilio-routes-twiml.test.ts +11 -0
  95. package/src/__tests__/twilio-routes.test.ts +167 -11
  96. package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
  97. package/src/__tests__/user-reference.test.ts +2 -0
  98. package/src/__tests__/voice-quality.test.ts +222 -0
  99. package/src/__tests__/web-search.test.ts +46 -30
  100. package/src/__tests__/work-item-output.test.ts +110 -0
  101. package/src/agent/loop.ts +1 -1
  102. package/src/agent-heartbeat/agent-heartbeat-service.ts +2 -10
  103. package/src/amazon/client.ts +1418 -0
  104. package/src/amazon/request-extractor.ts +135 -0
  105. package/src/amazon/session.ts +109 -0
  106. package/src/autonomy/autonomy-store.ts +5 -5
  107. package/src/browser-extension-relay/client.ts +124 -0
  108. package/src/browser-extension-relay/protocol.ts +63 -0
  109. package/src/browser-extension-relay/server.ts +177 -0
  110. package/src/bundler/app-bundler.ts +3 -3
  111. package/src/bundler/bundle-signer.ts +1 -1
  112. package/src/bundler/signature-verifier.ts +1 -1
  113. package/src/calls/call-conversation-messages.ts +33 -0
  114. package/src/calls/call-domain.ts +114 -10
  115. package/src/calls/call-orchestrator.ts +268 -59
  116. package/src/calls/call-pointer-messages.ts +53 -0
  117. package/src/calls/call-recovery.ts +3 -8
  118. package/src/calls/call-store.ts +69 -87
  119. package/src/calls/elevenlabs-config.ts +3 -2
  120. package/src/calls/guardian-action-sweep.ts +105 -0
  121. package/src/calls/guardian-dispatch.ts +203 -0
  122. package/src/calls/guardian-question-copy.ts +133 -0
  123. package/src/calls/relay-server.ts +466 -8
  124. package/src/calls/speaker-identification.ts +1 -1
  125. package/src/calls/twilio-config.ts +22 -14
  126. package/src/calls/twilio-provider.ts +6 -4
  127. package/src/calls/twilio-rest.ts +308 -7
  128. package/src/calls/twilio-routes.ts +65 -12
  129. package/src/calls/types.ts +3 -1
  130. package/src/channels/types.ts +25 -0
  131. package/src/cli/amazon.ts +815 -0
  132. package/src/cli/config-commands.ts +2 -2
  133. package/src/cli/core-commands.ts +4 -3
  134. package/src/cli/influencer.ts +244 -0
  135. package/src/cli/map.ts +89 -6
  136. package/src/cli.ts +1 -1
  137. package/src/config/agent-schema.ts +171 -0
  138. package/src/config/bundled-skills/amazon/SKILL.md +127 -0
  139. package/src/config/bundled-skills/amazon/icon.svg +13 -0
  140. package/src/config/bundled-skills/api-mapping/SKILL.md +78 -0
  141. package/src/config/bundled-skills/browser/SKILL.md +1 -0
  142. package/src/config/bundled-skills/browser/TOOLS.json +17 -0
  143. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +25 -0
  144. package/src/config/bundled-skills/doordash/SKILL.md +51 -51
  145. package/src/config/bundled-skills/email-setup/SKILL.md +14 -5
  146. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +183 -0
  147. package/src/config/bundled-skills/influencer/SKILL.md +144 -0
  148. package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
  149. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
  150. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
  151. package/src/config/bundled-skills/macos-automation/icon.svg +12 -0
  152. package/src/config/bundled-skills/media-processing/SKILL.md +176 -0
  153. package/src/config/bundled-skills/media-processing/TOOLS.json +230 -0
  154. package/src/config/bundled-skills/media-processing/__tests__/concurrency-pool.test.ts +77 -0
  155. package/src/config/bundled-skills/media-processing/__tests__/cost-tracker.test.ts +69 -0
  156. package/src/config/bundled-skills/media-processing/__tests__/preprocess.test.ts +303 -0
  157. package/src/config/bundled-skills/media-processing/services/concurrency-pool.ts +55 -0
  158. package/src/config/bundled-skills/media-processing/services/cost-tracker.ts +86 -0
  159. package/src/config/bundled-skills/media-processing/services/gemini-map.ts +339 -0
  160. package/src/config/bundled-skills/media-processing/services/preprocess.ts +551 -0
  161. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +259 -0
  162. package/src/config/bundled-skills/media-processing/services/reduce.ts +197 -0
  163. package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +136 -0
  164. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +59 -0
  165. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
  166. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
  167. package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +143 -0
  168. package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
  169. package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +65 -0
  170. package/src/config/bundled-skills/messaging/SKILL.md +33 -8
  171. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -7
  172. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +2 -1
  173. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
  174. package/src/config/bundled-skills/phone-calls/SKILL.md +88 -23
  175. package/src/config/bundled-skills/twitter/SKILL.md +19 -3
  176. package/src/config/bundled-skills/twitter/icon.svg +14 -0
  177. package/src/config/bundled-tool-registry.ts +310 -0
  178. package/src/config/calls-schema.ts +181 -0
  179. package/src/config/core-schema.ts +309 -0
  180. package/src/config/defaults.ts +28 -3
  181. package/src/config/env-registry.ts +162 -0
  182. package/src/config/env.ts +175 -0
  183. package/src/config/loader.ts +6 -6
  184. package/src/config/memory-schema.ts +528 -0
  185. package/src/config/sandbox-schema.ts +55 -0
  186. package/src/config/schema.ts +158 -1133
  187. package/src/config/skill-state.ts +1 -1
  188. package/src/config/skills-schema.ts +32 -0
  189. package/src/config/skills.ts +35 -24
  190. package/src/config/system-prompt.ts +131 -56
  191. package/src/config/templates/IDENTITY.md +2 -2
  192. package/src/config/templates/SOUL.md +1 -1
  193. package/src/config/types.ts +1 -0
  194. package/src/config/user-reference.ts +4 -9
  195. package/src/config/vellum-skills/catalog.json +6 -7
  196. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +5 -1
  197. package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +4 -3
  198. package/src/config/vellum-skills/sms-setup/SKILL.md +216 -0
  199. package/src/config/vellum-skills/twilio-setup/SKILL.md +40 -8
  200. package/src/context/window-manager.ts +27 -7
  201. package/src/daemon/approval-generators.ts +186 -0
  202. package/src/daemon/approved-devices-store.ts +140 -0
  203. package/src/daemon/assistant-attachments.ts +1 -1
  204. package/src/daemon/classifier.ts +35 -32
  205. package/src/daemon/config-watcher.ts +1 -1
  206. package/src/daemon/daemon-control.ts +217 -0
  207. package/src/daemon/handlers/apps.ts +2 -3
  208. package/src/daemon/handlers/config-channels.ts +158 -0
  209. package/src/daemon/handlers/config-inbox.ts +540 -0
  210. package/src/daemon/handlers/config-ingress.ts +231 -0
  211. package/src/daemon/handlers/config-integrations.ts +258 -0
  212. package/src/daemon/handlers/config-model.ts +143 -0
  213. package/src/daemon/handlers/config-parental.ts +163 -0
  214. package/src/daemon/handlers/config-scheduling.ts +172 -0
  215. package/src/daemon/handlers/config-slack.ts +92 -0
  216. package/src/daemon/handlers/config-telegram.ts +301 -0
  217. package/src/daemon/handlers/config-tools.ts +177 -0
  218. package/src/daemon/handlers/config-trust.ts +104 -0
  219. package/src/daemon/handlers/config-twilio.ts +1080 -0
  220. package/src/daemon/handlers/config.ts +53 -1689
  221. package/src/daemon/handlers/diagnostics.ts +1 -1
  222. package/src/daemon/handlers/dictation.ts +180 -0
  223. package/src/daemon/handlers/documents.ts +18 -32
  224. package/src/daemon/handlers/identity.ts +14 -23
  225. package/src/daemon/handlers/index.ts +11 -0
  226. package/src/daemon/handlers/misc.ts +3 -5
  227. package/src/daemon/handlers/pairing.ts +98 -0
  228. package/src/daemon/handlers/sessions.ts +56 -5
  229. package/src/daemon/handlers/shared.ts +6 -1
  230. package/src/daemon/handlers/skills.ts +1 -1
  231. package/src/daemon/handlers/twitter-auth.ts +2 -0
  232. package/src/daemon/handlers/work-items.ts +17 -9
  233. package/src/daemon/handlers/workspace-files.ts +4 -3
  234. package/src/daemon/install-cli-launchers.ts +113 -0
  235. package/src/daemon/ipc-contract/apps.ts +356 -0
  236. package/src/daemon/ipc-contract/browser.ts +74 -0
  237. package/src/daemon/ipc-contract/computer-use.ts +151 -0
  238. package/src/daemon/ipc-contract/diagnostics.ts +56 -0
  239. package/src/daemon/ipc-contract/documents.ts +74 -0
  240. package/src/daemon/ipc-contract/inbox.ts +209 -0
  241. package/src/daemon/ipc-contract/integrations.ts +284 -0
  242. package/src/daemon/ipc-contract/memory.ts +48 -0
  243. package/src/daemon/ipc-contract/messages.ts +211 -0
  244. package/src/daemon/ipc-contract/pairing.ts +45 -0
  245. package/src/daemon/ipc-contract/parental-control.ts +95 -0
  246. package/src/daemon/ipc-contract/schedules.ts +97 -0
  247. package/src/daemon/ipc-contract/sessions.ts +315 -0
  248. package/src/daemon/ipc-contract/shared.ts +42 -0
  249. package/src/daemon/ipc-contract/skills.ts +120 -0
  250. package/src/daemon/ipc-contract/subagents.ts +58 -0
  251. package/src/daemon/ipc-contract/surfaces.ts +250 -0
  252. package/src/daemon/ipc-contract/trust.ts +60 -0
  253. package/src/daemon/ipc-contract/work-items.ts +225 -0
  254. package/src/daemon/ipc-contract/workspace.ts +113 -0
  255. package/src/daemon/ipc-contract-inventory.json +70 -0
  256. package/src/daemon/ipc-contract-inventory.ts +55 -29
  257. package/src/daemon/ipc-contract.ts +229 -2426
  258. package/src/daemon/ipc-protocol.ts +1 -1
  259. package/src/daemon/ipc-validate.ts +7 -0
  260. package/src/daemon/lifecycle.ts +97 -377
  261. package/src/daemon/pairing-store.ts +177 -0
  262. package/src/daemon/providers-setup.ts +43 -0
  263. package/src/daemon/ride-shotgun-handler.ts +68 -3
  264. package/src/daemon/server.ts +66 -46
  265. package/src/daemon/session-agent-loop-handlers.ts +421 -0
  266. package/src/daemon/session-agent-loop.ts +117 -275
  267. package/src/daemon/session-dynamic-profile.ts +1 -1
  268. package/src/daemon/session-history.ts +1 -1
  269. package/src/daemon/session-media-retry.ts +1 -1
  270. package/src/daemon/session-messaging.ts +37 -2
  271. package/src/daemon/session-notifiers.ts +5 -25
  272. package/src/daemon/session-process.ts +99 -59
  273. package/src/daemon/session-queue-manager.ts +96 -4
  274. package/src/daemon/session-runtime-assembly.ts +199 -10
  275. package/src/daemon/session-surfaces.ts +19 -4
  276. package/src/daemon/session-tool-setup.ts +30 -30
  277. package/src/daemon/session-workspace.ts +1 -1
  278. package/src/daemon/session.ts +35 -2
  279. package/src/daemon/shutdown-handlers.ts +122 -0
  280. package/src/daemon/trace-emitter.ts +1 -1
  281. package/src/daemon/watch-handler.ts +36 -33
  282. package/src/doordash/cart-queries.ts +787 -0
  283. package/src/doordash/client.ts +144 -127
  284. package/src/doordash/order-queries.ts +85 -0
  285. package/src/doordash/queries.ts +10 -1308
  286. package/src/doordash/search-queries.ts +203 -0
  287. package/src/doordash/session.ts +3 -2
  288. package/src/doordash/store-queries.ts +246 -0
  289. package/src/doordash/types.ts +367 -0
  290. package/src/email/providers/agentmail.ts +2 -1
  291. package/src/email/providers/index.ts +3 -2
  292. package/src/email/service.ts +3 -2
  293. package/src/errors.ts +43 -0
  294. package/src/home-base/prebuilt/seed.ts +1 -1
  295. package/src/hooks/cli.ts +6 -5
  296. package/src/hooks/config.ts +6 -8
  297. package/src/hooks/discovery.ts +6 -5
  298. package/src/hooks/manager.ts +4 -3
  299. package/src/hooks/runner.ts +2 -2
  300. package/src/hooks/templates.ts +5 -5
  301. package/src/inbound/public-ingress-urls.ts +6 -4
  302. package/src/index.ts +4 -2
  303. package/src/influencer/client.ts +1104 -0
  304. package/src/instrument.ts +4 -3
  305. package/src/logfire.ts +4 -3
  306. package/src/memory/admin.ts +25 -35
  307. package/src/memory/attachments-store.ts +4 -7
  308. package/src/memory/channel-delivery-store.ts +30 -1
  309. package/src/memory/channel-guardian-store.ts +202 -2
  310. package/src/memory/clarification-resolver.ts +37 -33
  311. package/src/memory/conflict-store.ts +67 -61
  312. package/src/memory/contradiction-checker.ts +141 -117
  313. package/src/memory/conversation-store.ts +335 -51
  314. package/src/memory/db-connection.ts +27 -4
  315. package/src/memory/db-init.ts +265 -4
  316. package/src/memory/db.ts +14 -1
  317. package/src/memory/embedding-backend.ts +27 -5
  318. package/src/memory/embedding-ollama.ts +2 -1
  319. package/src/memory/entity-extractor.ts +38 -35
  320. package/src/memory/guardian-action-store.ts +430 -0
  321. package/src/memory/inbox-escalation-projection.ts +59 -0
  322. package/src/memory/inbox-thread-store.ts +218 -0
  323. package/src/memory/ingress-invite-store.ts +338 -0
  324. package/src/memory/ingress-member-store.ts +350 -0
  325. package/src/memory/items-extractor.ts +91 -97
  326. package/src/memory/job-handlers/index-maintenance.ts +3 -3
  327. package/src/memory/job-handlers/media-processing.ts +69 -0
  328. package/src/memory/job-handlers/summarization.ts +32 -26
  329. package/src/memory/job-utils.ts +3 -10
  330. package/src/memory/jobs-store.ts +8 -10
  331. package/src/memory/jobs-worker.ts +55 -36
  332. package/src/memory/media-store.ts +759 -0
  333. package/src/memory/migrations/001-job-deferrals.ts +45 -0
  334. package/src/memory/migrations/002-tool-invocations-fk.ts +43 -0
  335. package/src/memory/migrations/003-memory-fts-backfill.ts +24 -0
  336. package/src/memory/migrations/004-entity-relation-dedup.ts +87 -0
  337. package/src/memory/migrations/005-fingerprint-scope-unique.ts +80 -0
  338. package/src/memory/migrations/006-scope-salted-fingerprints.ts +62 -0
  339. package/src/memory/migrations/007-assistant-id-to-self.ts +254 -0
  340. package/src/memory/migrations/008-remove-assistant-id-columns.ts +208 -0
  341. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +83 -0
  342. package/src/memory/migrations/010-ext-conv-bindings-channel-chat-unique.ts +56 -0
  343. package/src/memory/migrations/011-call-sessions-provider-sid-dedup.ts +63 -0
  344. package/src/memory/migrations/012-call-sessions-add-initiated-from.ts +19 -0
  345. package/src/memory/migrations/013-guardian-action-tables.ts +68 -0
  346. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +76 -0
  347. package/src/memory/migrations/015-drop-active-search-index.ts +27 -0
  348. package/src/memory/migrations/016-memory-segments-indexes.ts +11 -0
  349. package/src/memory/migrations/017-memory-items-indexes.ts +10 -0
  350. package/src/memory/migrations/018-remaining-table-indexes.ts +13 -0
  351. package/src/memory/migrations/index.ts +24 -0
  352. package/src/memory/migrations/registry.ts +79 -0
  353. package/src/memory/migrations/validate-migration-state.ts +69 -0
  354. package/src/memory/qdrant-manager.ts +49 -8
  355. package/src/memory/query-builder.ts +1 -1
  356. package/src/memory/raw-query.ts +119 -0
  357. package/src/memory/recall-cache.ts +4 -1
  358. package/src/memory/retriever.ts +165 -47
  359. package/src/memory/schema-migration.ts +25 -984
  360. package/src/memory/schema.ts +228 -7
  361. package/src/memory/search/entity.ts +205 -31
  362. package/src/memory/search/lexical.ts +81 -52
  363. package/src/memory/search/ranking.ts +27 -23
  364. package/src/memory/search/semantic.ts +157 -19
  365. package/src/memory/search/types.ts +24 -0
  366. package/src/memory/shared-app-links-store.ts +4 -5
  367. package/src/memory/validation.ts +19 -0
  368. package/src/messaging/draft-store.ts +5 -6
  369. package/src/messaging/provider-types.ts +2 -0
  370. package/src/messaging/providers/sms/adapter.ts +201 -0
  371. package/src/messaging/providers/sms/client.ts +93 -0
  372. package/src/messaging/providers/sms/types.ts +7 -0
  373. package/src/messaging/providers/telegram-bot/adapter.ts +2 -5
  374. package/src/messaging/providers/whatsapp/adapter.ts +136 -0
  375. package/src/messaging/providers/whatsapp/client.ts +67 -0
  376. package/src/messaging/style-analyzer.ts +5 -4
  377. package/src/messaging/thread-summarizer.ts +61 -69
  378. package/src/messaging/triage-engine.ts +62 -71
  379. package/src/migrations/config-merge.ts +53 -0
  380. package/src/migrations/data-layout.ts +68 -0
  381. package/src/migrations/data-merge.ts +33 -0
  382. package/src/migrations/hooks-merge.ts +90 -0
  383. package/src/migrations/index.ts +6 -0
  384. package/src/migrations/log.ts +23 -0
  385. package/src/migrations/skills-merge.ts +33 -0
  386. package/src/migrations/workspace-layout.ts +79 -0
  387. package/src/permissions/checker.ts +133 -11
  388. package/src/permissions/prompter.ts +14 -0
  389. package/src/permissions/shell-identity.ts +31 -1
  390. package/src/permissions/trust-store.ts +21 -1
  391. package/src/providers/anthropic/client.ts +4 -4
  392. package/src/providers/failover.ts +2 -2
  393. package/src/providers/model-intents.ts +70 -0
  394. package/src/providers/ollama/client.ts +2 -1
  395. package/src/providers/provider-send-message.ts +176 -0
  396. package/src/providers/registry.ts +71 -30
  397. package/src/providers/retry.ts +35 -1
  398. package/src/providers/types.ts +12 -1
  399. package/src/runtime/approval-conversation-turn.ts +97 -0
  400. package/src/runtime/approval-message-composer.ts +253 -0
  401. package/src/runtime/channel-approval-parser.ts +36 -2
  402. package/src/runtime/channel-approvals.ts +11 -24
  403. package/src/runtime/channel-guardian-service.ts +88 -21
  404. package/src/runtime/channel-readiness-service.ts +418 -0
  405. package/src/runtime/channel-readiness-types.ts +35 -0
  406. package/src/runtime/channel-retry-sweep.ts +184 -0
  407. package/src/runtime/guardian-context-resolver.ts +108 -0
  408. package/src/runtime/http-server.ts +275 -717
  409. package/src/runtime/http-types.ts +59 -3
  410. package/src/runtime/middleware/auth.ts +116 -0
  411. package/src/runtime/middleware/error-handler.ts +33 -0
  412. package/src/runtime/middleware/twilio-validation.ts +127 -0
  413. package/src/runtime/routes/app-routes.ts +1 -1
  414. package/src/runtime/routes/call-routes.ts +51 -7
  415. package/src/runtime/routes/channel-delivery-routes.ts +170 -0
  416. package/src/runtime/routes/channel-guardian-routes.ts +1191 -0
  417. package/src/runtime/routes/channel-inbound-routes.ts +1152 -0
  418. package/src/runtime/routes/channel-route-shared.ts +144 -0
  419. package/src/runtime/routes/channel-routes.ts +32 -1588
  420. package/src/runtime/routes/conversation-routes.ts +50 -7
  421. package/src/runtime/routes/events-routes.ts +2 -2
  422. package/src/runtime/routes/identity-routes.ts +126 -0
  423. package/src/runtime/routes/pairing-routes.ts +143 -0
  424. package/src/runtime/routes/run-routes.ts +15 -1
  425. package/src/runtime/run-orchestrator.ts +86 -35
  426. package/src/schedule/schedule-store.ts +36 -32
  427. package/src/schedule/scheduler.ts +3 -3
  428. package/src/security/encrypted-store.ts +5 -7
  429. package/src/security/oauth2.ts +45 -15
  430. package/src/security/parental-control-store.ts +183 -0
  431. package/src/security/secret-allowlist.ts +4 -3
  432. package/src/security/secret-scanner.ts +5 -5
  433. package/src/security/secure-keys.ts +1 -1
  434. package/src/security/token-manager.ts +3 -2
  435. package/src/services/vercel-deploy.ts +6 -2
  436. package/src/skills/tool-manifest.ts +3 -3
  437. package/src/skills/vellum-catalog-remote.ts +75 -16
  438. package/src/slack/slack-webhook.ts +2 -1
  439. package/src/swarm/orchestrator.ts +92 -1
  440. package/src/swarm/router-planner.ts +6 -9
  441. package/src/swarm/worker-prompts.ts +9 -12
  442. package/src/tasks/task-compiler.ts +19 -28
  443. package/src/tasks/task-runner.ts +1 -1
  444. package/src/tools/assets/materialize.ts +2 -2
  445. package/src/tools/assets/search.ts +15 -14
  446. package/src/tools/browser/__tests__/auth-detector.test.ts +1 -0
  447. package/src/tools/browser/auto-navigate.ts +1 -0
  448. package/src/tools/browser/browser-execution.ts +10 -1
  449. package/src/tools/browser/browser-manager.ts +119 -4
  450. package/src/tools/browser/network-recorder.ts +5 -0
  451. package/src/tools/calls/call-start.ts +1 -0
  452. package/src/tools/credentials/broker.ts +11 -2
  453. package/src/tools/credentials/metadata-store.ts +18 -14
  454. package/src/tools/credentials/post-connect-hooks.ts +61 -0
  455. package/src/tools/credentials/vault.ts +49 -23
  456. package/src/tools/execution-target.ts +11 -1
  457. package/src/tools/executor.ts +68 -9
  458. package/src/tools/host-terminal/cli-discover.ts +1 -1
  459. package/src/tools/network/script-proxy/http-forwarder.ts +1 -1
  460. package/src/tools/network/script-proxy/mitm-handler.ts +1 -1
  461. package/src/tools/network/script-proxy/server.ts +1 -1
  462. package/src/tools/network/script-proxy/session-manager.ts +6 -5
  463. package/src/tools/network/web-fetch.ts +18 -2
  464. package/src/tools/network/web-search.ts +8 -4
  465. package/src/tools/reminder/reminder-store.ts +14 -15
  466. package/src/tools/schedule/create.ts +1 -0
  467. package/src/tools/schedule/list.ts +2 -1
  468. package/src/tools/shared/filesystem/file-ops-service.ts +5 -7
  469. package/src/tools/skills/skill-script-runner.ts +24 -9
  470. package/src/tools/skills/skill-tool-factory.ts +1 -0
  471. package/src/tools/tasks/work-item-enqueue.ts +2 -2
  472. package/src/tools/terminal/evaluate-typescript.ts +21 -12
  473. package/src/tools/terminal/parser.ts +50 -0
  474. package/src/tools/types.ts +2 -0
  475. package/src/tools/watcher/delete.ts +6 -0
  476. package/src/tools/weather/service.ts +1 -1
  477. package/src/twitter/client.ts +190 -24
  478. package/src/twitter/router.ts +1 -1
  479. package/src/twitter/session.ts +4 -3
  480. package/src/util/clipboard.ts +1 -1
  481. package/src/util/errors.ts +65 -8
  482. package/src/util/fs.ts +40 -0
  483. package/src/util/json.ts +10 -0
  484. package/src/util/log-redact.ts +189 -0
  485. package/src/util/logger.ts +19 -17
  486. package/src/util/object.ts +3 -0
  487. package/src/util/platform.ts +105 -363
  488. package/src/util/pricing.ts +1 -1
  489. package/src/util/promise-guard.ts +1 -1
  490. package/src/util/retry.ts +19 -0
  491. package/src/util/row-mapper.ts +79 -0
  492. package/src/util/silently.ts +21 -0
  493. package/src/watcher/engine.ts +5 -1
  494. package/src/watcher/provider-types.ts +20 -0
  495. package/src/watcher/providers/github.ts +156 -0
  496. package/src/watcher/providers/gmail.ts +1 -0
  497. package/src/watcher/providers/google-calendar.ts +1 -0
  498. package/src/watcher/providers/linear.ts +460 -0
  499. package/src/watcher/providers/slack.ts +1 -0
  500. package/src/work-items/work-item-runner.ts +1 -1
  501. package/src/workspace/git-service.ts +1 -1
  502. package/src/workspace/provider-commit-message-generator.ts +51 -22
  503. package/src/__tests__/call-bridge.test.ts +0 -517
  504. package/src/__tests__/session-process-bridge.test.ts +0 -244
  505. package/src/calls/call-bridge.ts +0 -168
  506. package/src/config/vellum-skills/google-oauth-setup/SKILL.md +0 -199
@@ -0,0 +1,621 @@
1
+ import { describe, test, expect, mock } from 'bun:test';
2
+ import { resolve } from 'node:path';
3
+
4
+ const retryModulePath = resolve(import.meta.dir, '../util/retry.ts');
5
+
6
+ mock.module('../util/logger.js', () => ({
7
+ getLogger: () =>
8
+ new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
9
+ isDebug: () => false,
10
+ }));
11
+
12
+ // Only mock sleep so retries complete instantly; keep real retry logic
13
+ mock.module('../util/retry.js', async () => {
14
+ const real = await import(retryModulePath);
15
+ return {
16
+ ...real,
17
+ sleep: () => Promise.resolve(),
18
+ };
19
+ });
20
+
21
+ import { RetryProvider } from '../providers/retry.js';
22
+ import { FailoverProvider } from '../providers/failover.js';
23
+ import { createStreamTimeout } from '../providers/stream-timeout.js';
24
+ import { ProviderError } from '../util/errors.js';
25
+ import { DEFAULT_MAX_RETRIES } from '../util/retry.js';
26
+ import type {
27
+ Provider,
28
+ ProviderResponse,
29
+ Message,
30
+ ProviderEvent,
31
+ } from '../providers/types.js';
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Helpers
35
+ // ---------------------------------------------------------------------------
36
+
37
+ const MESSAGES: Message[] = [
38
+ { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
39
+ ];
40
+
41
+ function successResponse(overrides?: Partial<ProviderResponse>): ProviderResponse {
42
+ return {
43
+ content: [{ type: 'text', text: 'ok' }],
44
+ model: 'test-model',
45
+ usage: { inputTokens: 10, outputTokens: 5 },
46
+ stopReason: 'end_turn',
47
+ ...overrides,
48
+ };
49
+ }
50
+
51
+ function makeProvider(name = 'mock'): Provider & { calls: number } {
52
+ const p = {
53
+ name,
54
+ calls: 0,
55
+ async sendMessage(): Promise<ProviderResponse> {
56
+ p.calls++;
57
+ return successResponse();
58
+ },
59
+ };
60
+ return p;
61
+ }
62
+
63
+ /** Provider that fails N times then succeeds. */
64
+ function makeFlaky(
65
+ failCount: number,
66
+ error: Error,
67
+ name = 'flaky',
68
+ ): Provider & { calls: number } {
69
+ const p = {
70
+ name,
71
+ calls: 0,
72
+ async sendMessage(): Promise<ProviderResponse> {
73
+ p.calls++;
74
+ if (p.calls <= failCount) throw error;
75
+ return successResponse();
76
+ },
77
+ };
78
+ return p;
79
+ }
80
+
81
+ /** Provider that always fails. */
82
+ function makeFailing(error: Error, name = 'failing'): Provider & { calls: number } {
83
+ const p = {
84
+ name,
85
+ calls: 0,
86
+ async sendMessage(): Promise<ProviderResponse> {
87
+ p.calls++;
88
+ throw error;
89
+ },
90
+ };
91
+ return p;
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // RetryProvider — rate limit backoff
96
+ // ---------------------------------------------------------------------------
97
+
98
+ describe('RetryProvider — rate limit backoff', () => {
99
+ test('retries on 429 and succeeds after transient rate limit', async () => {
100
+ const inner = makeFlaky(2, new ProviderError('rate limited', 'test', 429));
101
+ const provider = new RetryProvider(inner);
102
+
103
+ const result = await provider.sendMessage(MESSAGES);
104
+
105
+ expect(result.content[0]).toMatchObject({ type: 'text', text: 'ok' });
106
+ // 2 failures + 1 success = 3 calls
107
+ expect(inner.calls).toBe(3);
108
+ });
109
+
110
+ test('throws after exhausting all retries on persistent 429', async () => {
111
+ const inner = makeFailing(new ProviderError('rate limited', 'test', 429));
112
+ const provider = new RetryProvider(inner);
113
+
114
+ await expect(provider.sendMessage(MESSAGES)).rejects.toThrow('rate limited');
115
+ // 1 initial + DEFAULT_MAX_RETRIES retries
116
+ expect(inner.calls).toBe(DEFAULT_MAX_RETRIES + 1);
117
+ });
118
+
119
+ test('preserves ProviderError properties through retry exhaustion', async () => {
120
+ const inner = makeFailing(new ProviderError('quota exceeded', 'anthropic', 429));
121
+ const provider = new RetryProvider(inner);
122
+
123
+ try {
124
+ await provider.sendMessage(MESSAGES);
125
+ expect(true).toBe(false); // should not reach
126
+ } catch (err) {
127
+ expect(err).toBeInstanceOf(ProviderError);
128
+ const pe = err as ProviderError;
129
+ expect(pe.provider).toBe('anthropic');
130
+ expect(pe.statusCode).toBe(429);
131
+ expect(pe.message).toBe('quota exceeded');
132
+ }
133
+ });
134
+ });
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // RetryProvider — server error retries
138
+ // ---------------------------------------------------------------------------
139
+
140
+ describe('RetryProvider — server error retries', () => {
141
+ test('retries on 500 Internal Server Error', async () => {
142
+ const inner = makeFlaky(1, new ProviderError('internal error', 'test', 500));
143
+ const provider = new RetryProvider(inner);
144
+
145
+ const result = await provider.sendMessage(MESSAGES);
146
+ expect(result.stopReason).toBe('end_turn');
147
+ expect(inner.calls).toBe(2);
148
+ });
149
+
150
+ test('retries on 502 Bad Gateway', async () => {
151
+ const inner = makeFlaky(1, new ProviderError('bad gateway', 'test', 502));
152
+ const provider = new RetryProvider(inner);
153
+
154
+ const result = await provider.sendMessage(MESSAGES);
155
+ expect(inner.calls).toBe(2);
156
+ expect(result.model).toBe('test-model');
157
+ });
158
+
159
+ test('retries on 503 Service Unavailable', async () => {
160
+ const inner = makeFlaky(1, new ProviderError('unavailable', 'test', 503));
161
+ const provider = new RetryProvider(inner);
162
+
163
+ const result = await provider.sendMessage(MESSAGES);
164
+ expect(inner.calls).toBe(2);
165
+ expect(result.content).toHaveLength(1);
166
+ });
167
+
168
+ test('does not retry on 400 Bad Request', async () => {
169
+ const inner = makeFailing(new ProviderError('bad request', 'test', 400));
170
+ const provider = new RetryProvider(inner);
171
+
172
+ await expect(provider.sendMessage(MESSAGES)).rejects.toThrow('bad request');
173
+ expect(inner.calls).toBe(1);
174
+ });
175
+
176
+ test('does not retry on 401 Unauthorized', async () => {
177
+ const inner = makeFailing(new ProviderError('unauthorized', 'test', 401));
178
+ const provider = new RetryProvider(inner);
179
+
180
+ await expect(provider.sendMessage(MESSAGES)).rejects.toThrow('unauthorized');
181
+ expect(inner.calls).toBe(1);
182
+ });
183
+
184
+ test('does not retry on 403 Forbidden', async () => {
185
+ const inner = makeFailing(new ProviderError('forbidden', 'test', 403));
186
+ const provider = new RetryProvider(inner);
187
+
188
+ await expect(provider.sendMessage(MESSAGES)).rejects.toThrow('forbidden');
189
+ expect(inner.calls).toBe(1);
190
+ });
191
+
192
+ test('does not retry on 422 Unprocessable Entity', async () => {
193
+ const inner = makeFailing(new ProviderError('invalid input', 'test', 422));
194
+ const provider = new RetryProvider(inner);
195
+
196
+ await expect(provider.sendMessage(MESSAGES)).rejects.toThrow('invalid input');
197
+ expect(inner.calls).toBe(1);
198
+ });
199
+ });
200
+
201
+ // ---------------------------------------------------------------------------
202
+ // RetryProvider — network error retries
203
+ // ---------------------------------------------------------------------------
204
+
205
+ describe('RetryProvider — network error retries', () => {
206
+ test('retries on ECONNRESET', async () => {
207
+ const err = new Error('connection reset');
208
+ (err as NodeJS.ErrnoException).code = 'ECONNRESET';
209
+ const inner = makeFlaky(1, err);
210
+ const provider = new RetryProvider(inner);
211
+
212
+ const result = await provider.sendMessage(MESSAGES);
213
+ expect(inner.calls).toBe(2);
214
+ expect(result.stopReason).toBe('end_turn');
215
+ });
216
+
217
+ test('retries on ECONNREFUSED', async () => {
218
+ const err = new Error('connection refused');
219
+ (err as NodeJS.ErrnoException).code = 'ECONNREFUSED';
220
+ const inner = makeFlaky(1, err);
221
+ const provider = new RetryProvider(inner);
222
+
223
+ const result = await provider.sendMessage(MESSAGES);
224
+ expect(inner.calls).toBe(2);
225
+ expect(result.model).toBe('test-model');
226
+ });
227
+
228
+ test('retries on ETIMEDOUT', async () => {
229
+ const err = new Error('timed out');
230
+ (err as NodeJS.ErrnoException).code = 'ETIMEDOUT';
231
+ const inner = makeFlaky(1, err);
232
+ const provider = new RetryProvider(inner);
233
+
234
+ const _result = await provider.sendMessage(MESSAGES);
235
+ expect(inner.calls).toBe(2);
236
+ });
237
+
238
+ test('retries on ECONNRESET in error cause chain', async () => {
239
+ const cause = new Error('socket hangup');
240
+ (cause as NodeJS.ErrnoException).code = 'ECONNRESET';
241
+ const outer = new Error('fetch failed', { cause });
242
+ const inner = makeFlaky(1, outer);
243
+ const provider = new RetryProvider(inner);
244
+
245
+ const result = await provider.sendMessage(MESSAGES);
246
+ expect(inner.calls).toBe(2);
247
+ expect(result.content[0]).toMatchObject({ type: 'text', text: 'ok' });
248
+ });
249
+
250
+ test('does not retry on non-retryable errors', async () => {
251
+ const inner = makeFailing(new Error('unexpected error'));
252
+ const provider = new RetryProvider(inner);
253
+
254
+ await expect(provider.sendMessage(MESSAGES)).rejects.toThrow('unexpected error');
255
+ expect(inner.calls).toBe(1);
256
+ });
257
+
258
+ test('does not retry on ProviderError without status code (non-network)', async () => {
259
+ // ProviderError without a statusCode and without a retryable network code
260
+ const err = new ProviderError('model not found', 'test');
261
+ const inner = makeFailing(err);
262
+ const provider = new RetryProvider(inner);
263
+
264
+ await expect(provider.sendMessage(MESSAGES)).rejects.toThrow('model not found');
265
+ expect(inner.calls).toBe(1);
266
+ });
267
+ });
268
+
269
+ // ---------------------------------------------------------------------------
270
+ // RetryProvider — streaming + options passthrough
271
+ // ---------------------------------------------------------------------------
272
+
273
+ describe('RetryProvider — streaming response handling', () => {
274
+ test('passes onEvent callback through to inner provider', async () => {
275
+ const events: ProviderEvent[] = [];
276
+ const inner: Provider = {
277
+ name: 'streaming-mock',
278
+ async sendMessage(_m, _t, _s, options) {
279
+ options?.onEvent?.({ type: 'text_delta', text: 'hello ' });
280
+ options?.onEvent?.({ type: 'text_delta', text: 'world' });
281
+ return successResponse();
282
+ },
283
+ };
284
+ const provider = new RetryProvider(inner);
285
+
286
+ await provider.sendMessage(MESSAGES, undefined, undefined, {
287
+ onEvent: (e) => events.push(e),
288
+ });
289
+
290
+ expect(events).toHaveLength(2);
291
+ expect(events[0]).toMatchObject({ type: 'text_delta', text: 'hello ' });
292
+ expect(events[1]).toMatchObject({ type: 'text_delta', text: 'world' });
293
+ });
294
+
295
+ test('passes signal through to inner provider', async () => {
296
+ let receivedSignal: AbortSignal | undefined;
297
+ const inner: Provider = {
298
+ name: 'signal-mock',
299
+ async sendMessage(_m, _t, _s, options) {
300
+ receivedSignal = options?.signal;
301
+ return successResponse();
302
+ },
303
+ };
304
+ const provider = new RetryProvider(inner);
305
+ const controller = new AbortController();
306
+
307
+ await provider.sendMessage(MESSAGES, undefined, undefined, {
308
+ signal: controller.signal,
309
+ });
310
+
311
+ expect(receivedSignal).toBe(controller.signal);
312
+ });
313
+
314
+ test('events accumulate across retries (each attempt delivers events independently)', async () => {
315
+ let callCount = 0;
316
+ const inner: Provider = {
317
+ name: 'retry-stream',
318
+ async sendMessage(_m, _t, _s, options) {
319
+ callCount++;
320
+ options?.onEvent?.({ type: 'text_delta', text: `attempt${callCount} ` });
321
+ if (callCount <= 1) {
322
+ throw new ProviderError('overloaded', 'test', 529);
323
+ }
324
+ return successResponse();
325
+ },
326
+ };
327
+ const provider = new RetryProvider(inner);
328
+ const events: ProviderEvent[] = [];
329
+
330
+ await provider.sendMessage(MESSAGES, undefined, undefined, {
331
+ onEvent: (e) => events.push(e),
332
+ });
333
+
334
+ // Events from both attempts are delivered
335
+ expect(events).toHaveLength(2);
336
+ expect(events[0]).toMatchObject({ type: 'text_delta', text: 'attempt1 ' });
337
+ expect(events[1]).toMatchObject({ type: 'text_delta', text: 'attempt2 ' });
338
+ });
339
+ });
340
+
341
+ // ---------------------------------------------------------------------------
342
+ // FailoverProvider — model unavailability fallback
343
+ // ---------------------------------------------------------------------------
344
+
345
+ describe('FailoverProvider — model unavailability fallback', () => {
346
+ test('falls back to secondary when primary returns 500', async () => {
347
+ const primary = makeFailing(new ProviderError('down', 'primary', 500), 'primary');
348
+ const secondary = makeProvider('secondary');
349
+ const provider = new FailoverProvider([primary, secondary]);
350
+
351
+ const result = await provider.sendMessage(MESSAGES);
352
+
353
+ expect(primary.calls).toBe(1);
354
+ expect(secondary.calls).toBe(1);
355
+ expect(result.stopReason).toBe('end_turn');
356
+ });
357
+
358
+ test('falls back to secondary when primary returns 429', async () => {
359
+ const primary = makeFailing(new ProviderError('rate limited', 'primary', 429), 'primary');
360
+ const secondary = makeProvider('secondary');
361
+ const provider = new FailoverProvider([primary, secondary]);
362
+
363
+ const result = await provider.sendMessage(MESSAGES);
364
+
365
+ expect(primary.calls).toBe(1);
366
+ expect(secondary.calls).toBe(1);
367
+ expect(result.model).toBe('test-model');
368
+ });
369
+
370
+ test('falls back on ECONNREFUSED network error', async () => {
371
+ const err = new Error('connection refused');
372
+ (err as NodeJS.ErrnoException).code = 'ECONNREFUSED';
373
+ const primary = makeFailing(err, 'primary');
374
+ const secondary = makeProvider('secondary');
375
+ const provider = new FailoverProvider([primary, secondary]);
376
+
377
+ const result = await provider.sendMessage(MESSAGES);
378
+
379
+ expect(primary.calls).toBe(1);
380
+ expect(secondary.calls).toBe(1);
381
+ expect(result.content[0]).toMatchObject({ type: 'text', text: 'ok' });
382
+ });
383
+
384
+ test('falls back on ProviderError without status code (connection failure)', async () => {
385
+ const primary = makeFailing(new ProviderError('connection failed', 'primary'), 'primary');
386
+ const secondary = makeProvider('secondary');
387
+ const provider = new FailoverProvider([primary, secondary]);
388
+
389
+ const _result = await provider.sendMessage(MESSAGES);
390
+ expect(primary.calls).toBe(1);
391
+ expect(secondary.calls).toBe(1);
392
+ });
393
+
394
+ test('does NOT fall back on 400 Bad Request', async () => {
395
+ const primary = makeFailing(new ProviderError('bad request', 'primary', 400), 'primary');
396
+ const secondary = makeProvider('secondary');
397
+ const provider = new FailoverProvider([primary, secondary]);
398
+
399
+ await expect(provider.sendMessage(MESSAGES)).rejects.toThrow('bad request');
400
+ expect(primary.calls).toBe(1);
401
+ expect(secondary.calls).toBe(0);
402
+ });
403
+
404
+ test('does NOT fall back on 401 Unauthorized', async () => {
405
+ const primary = makeFailing(new ProviderError('unauthorized', 'primary', 401), 'primary');
406
+ const secondary = makeProvider('secondary');
407
+ const provider = new FailoverProvider([primary, secondary]);
408
+
409
+ await expect(provider.sendMessage(MESSAGES)).rejects.toThrow('unauthorized');
410
+ expect(secondary.calls).toBe(0);
411
+ });
412
+
413
+ test('throws last error when all providers fail', async () => {
414
+ const p1 = makeFailing(new ProviderError('p1 down', 'p1', 500), 'p1');
415
+ const p2 = makeFailing(new ProviderError('p2 down', 'p2', 503), 'p2');
416
+ const p3 = makeFailing(new ProviderError('p3 down', 'p3', 502), 'p3');
417
+ const provider = new FailoverProvider([p1, p2, p3]);
418
+
419
+ try {
420
+ await provider.sendMessage(MESSAGES);
421
+ expect(true).toBe(false);
422
+ } catch (err) {
423
+ expect(err).toBeInstanceOf(ProviderError);
424
+ // Last provider's error is thrown
425
+ expect((err as ProviderError).message).toBe('p3 down');
426
+ }
427
+ expect(p1.calls).toBe(1);
428
+ expect(p2.calls).toBe(1);
429
+ expect(p3.calls).toBe(1);
430
+ });
431
+
432
+ test('chains through three providers when first two fail', async () => {
433
+ const p1 = makeFailing(new ProviderError('p1 error', 'p1', 500), 'p1');
434
+ const p2 = makeFailing(new ProviderError('p2 error', 'p2', 502), 'p2');
435
+ const p3 = makeProvider('p3');
436
+ const provider = new FailoverProvider([p1, p2, p3]);
437
+
438
+ const result = await provider.sendMessage(MESSAGES);
439
+
440
+ expect(p1.calls).toBe(1);
441
+ expect(p2.calls).toBe(1);
442
+ expect(p3.calls).toBe(1);
443
+ expect(result.stopReason).toBe('end_turn');
444
+ });
445
+
446
+ test('requires at least one provider', () => {
447
+ expect(() => new FailoverProvider([])).toThrow(
448
+ 'FailoverProvider requires at least one provider',
449
+ );
450
+ });
451
+ });
452
+
453
+ // ---------------------------------------------------------------------------
454
+ // FailoverProvider — cooldown and recovery
455
+ // ---------------------------------------------------------------------------
456
+
457
+ describe('FailoverProvider — cooldown and recovery', () => {
458
+ test('skips provider in cooldown period', async () => {
459
+ const primary = makeFailing(new ProviderError('down', 'primary', 500), 'primary');
460
+ const secondary = makeProvider('secondary');
461
+ // Use a long cooldown so primary stays unhealthy
462
+ const provider = new FailoverProvider([primary, secondary], 60_000);
463
+
464
+ // First call: primary fails, secondary succeeds
465
+ await provider.sendMessage(MESSAGES);
466
+ expect(primary.calls).toBe(1);
467
+ expect(secondary.calls).toBe(1);
468
+
469
+ // Second call: primary is in cooldown, skipped — goes straight to secondary
470
+ await provider.sendMessage(MESSAGES);
471
+ expect(primary.calls).toBe(1); // not called again
472
+ expect(secondary.calls).toBe(2);
473
+ });
474
+
475
+ test('retries provider after cooldown expires', async () => {
476
+ let primaryCallCount = 0;
477
+ const primary: Provider = {
478
+ name: 'primary',
479
+ async sendMessage() {
480
+ primaryCallCount++;
481
+ if (primaryCallCount === 1) {
482
+ throw new ProviderError('temporarily down', 'primary', 500);
483
+ }
484
+ return successResponse();
485
+ },
486
+ };
487
+ const secondary = makeProvider('secondary');
488
+ // Very short cooldown
489
+ const provider = new FailoverProvider([primary, secondary], 1);
490
+
491
+ // First call: primary fails, marked unhealthy, secondary succeeds
492
+ await provider.sendMessage(MESSAGES);
493
+ expect(primaryCallCount).toBe(1);
494
+
495
+ // Wait for cooldown to expire
496
+ await new Promise((r) => setTimeout(r, 10));
497
+
498
+ // Second call: primary should be retried after cooldown expired
499
+ await provider.sendMessage(MESSAGES);
500
+ expect(primaryCallCount).toBe(2);
501
+ });
502
+
503
+ test('marks provider healthy after successful recovery', async () => {
504
+ let primaryCallCount = 0;
505
+ const primary: Provider = {
506
+ name: 'primary',
507
+ async sendMessage() {
508
+ primaryCallCount++;
509
+ if (primaryCallCount === 1) {
510
+ throw new ProviderError('blip', 'primary', 500);
511
+ }
512
+ return successResponse();
513
+ },
514
+ };
515
+ const secondary = makeProvider('secondary');
516
+ const provider = new FailoverProvider([primary, secondary], 1);
517
+
518
+ // First call: primary fails
519
+ await provider.sendMessage(MESSAGES);
520
+ expect(primaryCallCount).toBe(1);
521
+
522
+ // Wait for cooldown
523
+ await new Promise((r) => setTimeout(r, 10));
524
+
525
+ // Second call: primary recovers
526
+ await provider.sendMessage(MESSAGES);
527
+ expect(primaryCallCount).toBe(2);
528
+
529
+ // Third call: primary is healthy, used directly
530
+ await provider.sendMessage(MESSAGES);
531
+ expect(primaryCallCount).toBe(3);
532
+ expect(secondary.calls).toBe(1); // only called once during initial failover
533
+ });
534
+ });
535
+
536
+ // ---------------------------------------------------------------------------
537
+ // createStreamTimeout — edge cases
538
+ // ---------------------------------------------------------------------------
539
+
540
+ describe('createStreamTimeout — edge cases', () => {
541
+ test('propagates already-aborted external signal immediately', () => {
542
+ const external = new AbortController();
543
+ external.abort(new Error('already cancelled'));
544
+
545
+ const { signal, cleanup } = createStreamTimeout(60_000, external.signal);
546
+
547
+ expect(signal.aborted).toBe(true);
548
+ expect(signal.reason).toBeInstanceOf(Error);
549
+ expect((signal.reason as Error).message).toBe('already cancelled');
550
+ cleanup();
551
+ });
552
+
553
+ test('cleanup prevents timeout from firing', async () => {
554
+ const { signal, cleanup } = createStreamTimeout(50);
555
+ cleanup();
556
+
557
+ await new Promise((r) => setTimeout(r, 100));
558
+ expect(signal.aborted).toBe(false);
559
+ });
560
+
561
+ test('cleanup removes external signal listener', () => {
562
+ const external = new AbortController();
563
+ const { signal, cleanup } = createStreamTimeout(60_000, external.signal);
564
+
565
+ cleanup();
566
+
567
+ // Aborting external after cleanup should NOT propagate
568
+ external.abort(new Error('late abort'));
569
+ expect(signal.aborted).toBe(false);
570
+ });
571
+
572
+ test('timeout error message includes duration', async () => {
573
+ const { signal, cleanup } = createStreamTimeout(100);
574
+
575
+ await new Promise<void>((resolve) => {
576
+ signal.addEventListener('abort', () => resolve(), { once: true });
577
+ });
578
+
579
+ expect(signal.reason).toBeInstanceOf(Error);
580
+ expect((signal.reason as Error).message).toContain('0.1s');
581
+ cleanup();
582
+ });
583
+ });
584
+
585
+ // ---------------------------------------------------------------------------
586
+ // RetryProvider + FailoverProvider — combined scenarios
587
+ // ---------------------------------------------------------------------------
588
+
589
+ describe('RetryProvider + FailoverProvider — combined', () => {
590
+ test('failover wrapping retry: each provider in the chain retries independently', async () => {
591
+ // Primary always fails with 500, secondary succeeds
592
+ const primary = makeFailing(new ProviderError('primary down', 'primary', 500), 'primary');
593
+ const secondary = makeProvider('secondary');
594
+
595
+ // Wrap each in RetryProvider, then combine with FailoverProvider
596
+ const retryPrimary = new RetryProvider(primary);
597
+ const retrySecondary = new RetryProvider(secondary);
598
+ const failover = new FailoverProvider([retryPrimary, retrySecondary]);
599
+
600
+ const result = await failover.sendMessage(MESSAGES);
601
+ expect(result.stopReason).toBe('end_turn');
602
+ // Primary should have been retried MAX_RETRIES + 1 times before failover
603
+ expect(primary.calls).toBe(DEFAULT_MAX_RETRIES + 1);
604
+ expect(secondary.calls).toBe(1);
605
+ });
606
+
607
+ test('single provider: retry exhaustion produces the original error', async () => {
608
+ const inner = makeFailing(new ProviderError('always fail', 'solo', 500));
609
+ const retrying = new RetryProvider(inner);
610
+
611
+ try {
612
+ await retrying.sendMessage(MESSAGES);
613
+ expect(true).toBe(false);
614
+ } catch (err) {
615
+ expect(err).toBeInstanceOf(ProviderError);
616
+ expect((err as ProviderError).message).toBe('always fail');
617
+ expect((err as ProviderError).statusCode).toBe(500);
618
+ }
619
+ expect(inner.calls).toBe(DEFAULT_MAX_RETRIES + 1);
620
+ });
621
+ });