@vellumai/assistant 0.5.6 → 0.5.8

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 (442) hide show
  1. package/.env.example +16 -2
  2. package/ARCHITECTURE.md +6 -75
  3. package/Dockerfile +3 -2
  4. package/README.md +0 -2
  5. package/bun.lock +0 -414
  6. package/docker-entrypoint.sh +9 -0
  7. package/docs/architecture/keychain-broker.md +45 -240
  8. package/docs/architecture/memory.md +13 -11
  9. package/docs/architecture/security.md +0 -17
  10. package/docs/credential-execution-service.md +2 -2
  11. package/node_modules/@vellumai/ces-contracts/package.json +1 -0
  12. package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
  13. package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
  14. package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
  15. package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
  16. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +120 -1
  17. package/node_modules/@vellumai/credential-storage/package.json +1 -0
  18. package/node_modules/@vellumai/egress-proxy/package.json +1 -0
  19. package/package.json +2 -3
  20. package/src/__tests__/actor-token-service.test.ts +0 -114
  21. package/src/__tests__/approval-cascade.test.ts +0 -1
  22. package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
  23. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  24. package/src/__tests__/browser-skill-endstate.test.ts +6 -5
  25. package/src/__tests__/btw-routes.test.ts +0 -39
  26. package/src/__tests__/call-controller.test.ts +0 -1
  27. package/src/__tests__/call-domain.test.ts +0 -128
  28. package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
  29. package/src/__tests__/ces-startup-timeout.test.ts +40 -0
  30. package/src/__tests__/channel-approval-routes.test.ts +0 -5
  31. package/src/__tests__/channel-readiness-service.test.ts +1 -60
  32. package/src/__tests__/checker.test.ts +4 -2
  33. package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
  34. package/src/__tests__/config-schema-cmd.test.ts +0 -2
  35. package/src/__tests__/config-schema.test.ts +3 -1
  36. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
  37. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
  38. package/src/__tests__/conversation-agent-loop.test.ts +2 -4
  39. package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
  40. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
  41. package/src/__tests__/conversation-error.test.ts +15 -1
  42. package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
  43. package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
  44. package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
  45. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
  46. package/src/__tests__/conversation-queue.test.ts +0 -1
  47. package/src/__tests__/conversation-skill-tools.test.ts +0 -54
  48. package/src/__tests__/conversation-slash-queue.test.ts +0 -1
  49. package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
  50. package/src/__tests__/conversation-title-service.test.ts +87 -0
  51. package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
  52. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
  53. package/src/__tests__/credential-execution-client.test.ts +5 -2
  54. package/src/__tests__/credential-execution-feature-gates.test.ts +59 -30
  55. package/src/__tests__/credential-execution-managed-contract.test.ts +35 -20
  56. package/src/__tests__/credential-security-e2e.test.ts +1 -67
  57. package/src/__tests__/credential-security-invariants.test.ts +6 -50
  58. package/src/__tests__/credentials-cli.test.ts +82 -3
  59. package/src/__tests__/daemon-credential-client.test.ts +123 -0
  60. package/src/__tests__/db-migration-rollback.test.ts +2015 -1
  61. package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
  62. package/src/__tests__/docker-signing-key-bootstrap.test.ts +34 -143
  63. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
  64. package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
  65. package/src/__tests__/guardian-routing-state.test.ts +0 -5
  66. package/src/__tests__/host-shell-tool.test.ts +6 -7
  67. package/src/__tests__/http-user-message-parity.test.ts +3 -103
  68. package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
  69. package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
  70. package/src/__tests__/intent-routing.test.ts +0 -13
  71. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
  72. package/src/__tests__/journal-context.test.ts +335 -0
  73. package/src/__tests__/keychain-broker-client.test.ts +161 -22
  74. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
  75. package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
  76. package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
  77. package/src/__tests__/memory-recall-quality.test.ts +48 -17
  78. package/src/__tests__/memory-regressions.test.ts +408 -363
  79. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
  80. package/src/__tests__/migration-export-http.test.ts +2 -2
  81. package/src/__tests__/migration-import-commit-http.test.ts +2 -2
  82. package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
  83. package/src/__tests__/migration-validate-http.test.ts +2 -2
  84. package/src/__tests__/non-member-access-request.test.ts +2 -7
  85. package/src/__tests__/notification-decision-fallback.test.ts +4 -0
  86. package/src/__tests__/notification-decision-identity.test.ts +4 -0
  87. package/src/__tests__/notification-decision-strategy.test.ts +71 -0
  88. package/src/__tests__/oauth-cli.test.ts +5 -1
  89. package/src/__tests__/permission-types.test.ts +1 -0
  90. package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
  91. package/src/__tests__/provider-error-scenarios.test.ts +0 -267
  92. package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
  93. package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
  94. package/src/__tests__/qdrant-manager.test.ts +28 -2
  95. package/src/__tests__/registry.test.ts +0 -6
  96. package/src/__tests__/relay-server.test.ts +1 -2
  97. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
  98. package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
  99. package/src/__tests__/secret-onetime-send.test.ts +1 -1
  100. package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
  101. package/src/__tests__/secure-keys.test.ts +95 -272
  102. package/src/__tests__/shell-identity.test.ts +96 -6
  103. package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
  104. package/src/__tests__/skill-feature-flags.test.ts +46 -45
  105. package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
  106. package/src/__tests__/skill-load-inline-command.test.ts +8 -12
  107. package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
  108. package/src/__tests__/skill-load-tool.test.ts +0 -2
  109. package/src/__tests__/skill-memory.test.ts +17 -3
  110. package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
  111. package/src/__tests__/skills.test.ts +0 -2
  112. package/src/__tests__/slack-inbound-verification.test.ts +0 -4
  113. package/src/__tests__/stale-approval-dedup.test.ts +171 -0
  114. package/src/__tests__/stt-hints.test.ts +437 -0
  115. package/src/__tests__/suggestion-routes.test.ts +1 -32
  116. package/src/__tests__/system-prompt.test.ts +0 -1
  117. package/src/__tests__/task-memory-cleanup.test.ts +14 -0
  118. package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
  119. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
  120. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
  121. package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
  122. package/src/__tests__/update-bulletin.test.ts +0 -2
  123. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
  124. package/src/__tests__/voice-quality.test.ts +58 -0
  125. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -7
  126. package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
  127. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +220 -0
  128. package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
  129. package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
  130. package/src/acp/agent-process.ts +9 -1
  131. package/src/agent/loop.ts +1 -1
  132. package/src/approvals/guardian-request-resolvers.ts +164 -38
  133. package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
  134. package/src/calls/audio-store.test.ts +97 -0
  135. package/src/calls/audio-store.ts +205 -0
  136. package/src/calls/call-controller.ts +90 -8
  137. package/src/calls/call-domain.ts +3 -0
  138. package/src/calls/call-store.ts +10 -3
  139. package/src/calls/fish-audio-client.ts +129 -0
  140. package/src/calls/relay-server.ts +27 -0
  141. package/src/calls/stt-hints.ts +189 -0
  142. package/src/calls/tts-text-sanitizer.ts +61 -0
  143. package/src/calls/twilio-routes.ts +34 -5
  144. package/src/calls/types.ts +1 -0
  145. package/src/calls/voice-ingress-preflight.ts +0 -42
  146. package/src/calls/voice-quality.ts +38 -5
  147. package/src/calls/voice-session-bridge.ts +7 -12
  148. package/src/cli/commands/avatar.ts +2 -2
  149. package/src/cli/commands/config.ts +1 -4
  150. package/src/cli/commands/credentials.ts +128 -82
  151. package/src/cli/commands/doctor.ts +2 -2
  152. package/src/cli/commands/keys.ts +7 -7
  153. package/src/cli/commands/memory.ts +1 -1
  154. package/src/cli/commands/oauth/connections.ts +11 -29
  155. package/src/cli/commands/oauth/index.ts +7 -0
  156. package/src/cli/commands/oauth/platform.ts +525 -0
  157. package/src/cli/commands/platform.ts +3 -3
  158. package/src/cli/lib/daemon-credential-client.ts +284 -0
  159. package/src/cli.ts +1 -1
  160. package/src/config/assistant-feature-flags.ts +186 -5
  161. package/src/config/bundled-skills/AGENTS.md +34 -0
  162. package/src/config/bundled-skills/acp/SKILL.md +10 -0
  163. package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
  164. package/src/config/bundled-skills/messaging/SKILL.md +5 -5
  165. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
  166. package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
  167. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
  168. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
  169. package/src/config/bundled-skills/settings/SKILL.md +15 -2
  170. package/src/config/bundled-skills/settings/TOOLS.json +47 -2
  171. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
  172. package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
  173. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
  174. package/src/config/bundled-skills/slack/SKILL.md +1 -1
  175. package/src/config/bundled-tool-registry.ts +5 -11
  176. package/src/config/defaults.ts +0 -2
  177. package/src/config/env-registry.ts +5 -5
  178. package/src/config/env.ts +21 -14
  179. package/src/config/feature-flag-registry.json +49 -9
  180. package/src/config/loader.ts +106 -42
  181. package/src/config/schema.ts +9 -29
  182. package/src/config/schemas/calls.ts +30 -0
  183. package/src/config/schemas/fish-audio.ts +39 -0
  184. package/src/config/schemas/inference.ts +2 -2
  185. package/src/config/schemas/journal.ts +16 -0
  186. package/src/config/schemas/memory-processing.ts +2 -2
  187. package/src/config/schemas/security.ts +0 -4
  188. package/src/config/types.ts +1 -1
  189. package/src/contacts/contact-store.ts +39 -0
  190. package/src/contacts/types.ts +2 -0
  191. package/src/credential-execution/approval-bridge.ts +1 -0
  192. package/src/credential-execution/executable-discovery.ts +28 -4
  193. package/src/credential-execution/feature-gates.ts +16 -0
  194. package/src/credential-execution/process-manager.ts +38 -0
  195. package/src/credential-execution/startup-timeout.ts +36 -0
  196. package/src/daemon/approval-generators.ts +3 -9
  197. package/src/daemon/assistant-attachments.ts +9 -0
  198. package/src/daemon/config-watcher.ts +5 -0
  199. package/src/daemon/conversation-error.ts +13 -1
  200. package/src/daemon/conversation-memory.ts +1 -2
  201. package/src/daemon/conversation-process.ts +18 -1
  202. package/src/daemon/conversation-surfaces.ts +30 -1
  203. package/src/daemon/conversation-tool-setup.ts +0 -105
  204. package/src/daemon/conversation.ts +21 -1
  205. package/src/daemon/guardian-action-generators.ts +3 -9
  206. package/src/daemon/handlers/config-vercel.ts +92 -0
  207. package/src/daemon/handlers/skills.ts +2 -15
  208. package/src/daemon/install-symlink.ts +195 -0
  209. package/src/daemon/lifecycle.ts +234 -51
  210. package/src/daemon/message-types/conversations.ts +4 -4
  211. package/src/daemon/message-types/diagnostics.ts +3 -22
  212. package/src/daemon/message-types/messages.ts +0 -2
  213. package/src/daemon/message-types/upgrades.ts +8 -0
  214. package/src/daemon/server.ts +32 -95
  215. package/src/events/domain-events.ts +2 -1
  216. package/src/inbound/platform-callback-registration.ts +3 -3
  217. package/src/instrument.ts +8 -5
  218. package/src/memory/app-store.ts +31 -0
  219. package/src/memory/conversation-title-service.ts +50 -1
  220. package/src/memory/db-init.ts +16 -0
  221. package/src/memory/indexer.ts +19 -10
  222. package/src/memory/items-extractor.ts +328 -321
  223. package/src/memory/job-handlers/conversation-starters.ts +4 -1
  224. package/src/memory/job-handlers/summarization.ts +26 -16
  225. package/src/memory/jobs-store.ts +63 -6
  226. package/src/memory/jobs-worker.ts +31 -7
  227. package/src/memory/journal-memory.ts +214 -0
  228. package/src/memory/migrations/001-job-deferrals.ts +19 -0
  229. package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
  230. package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
  231. package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
  232. package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
  233. package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
  234. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
  235. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
  236. package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
  237. package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
  238. package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
  239. package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
  240. package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
  241. package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
  242. package/src/memory/migrations/116-messages-fts.ts +106 -1
  243. package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
  244. package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
  245. package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
  246. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
  247. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
  248. package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
  249. package/src/memory/migrations/141-rename-verification-table.ts +54 -0
  250. package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
  251. package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
  252. package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
  253. package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
  254. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
  255. package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
  256. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
  257. package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
  258. package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
  259. package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
  260. package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
  261. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
  262. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
  263. package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
  264. package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
  265. package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
  266. package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
  267. package/src/memory/migrations/index.ts +5 -0
  268. package/src/memory/migrations/registry.ts +98 -0
  269. package/src/memory/migrations/validate-migration-state.ts +137 -11
  270. package/src/memory/qdrant-circuit-breaker.ts +9 -0
  271. package/src/memory/qdrant-manager.ts +64 -7
  272. package/src/memory/retriever.test.ts +37 -25
  273. package/src/memory/retriever.ts +24 -49
  274. package/src/memory/schema/calls.ts +1 -0
  275. package/src/memory/schema/contacts.ts +1 -0
  276. package/src/memory/schema/memory-core.ts +2 -0
  277. package/src/memory/search/formatting.ts +7 -44
  278. package/src/memory/search/staleness.ts +4 -0
  279. package/src/memory/search/tier-classifier.ts +10 -2
  280. package/src/memory/search/types.ts +2 -5
  281. package/src/memory/task-memory-cleanup.ts +4 -3
  282. package/src/notifications/adapters/slack.ts +168 -6
  283. package/src/notifications/broadcaster.ts +1 -0
  284. package/src/notifications/copy-composer.ts +59 -2
  285. package/src/notifications/decision-engine.ts +4 -1
  286. package/src/notifications/signal.ts +2 -0
  287. package/src/notifications/types.ts +2 -0
  288. package/src/oauth/connection-resolver.ts +6 -4
  289. package/src/permissions/checker.ts +0 -38
  290. package/src/permissions/shell-identity.ts +76 -22
  291. package/src/permissions/types.ts +4 -2
  292. package/src/platform/client.ts +35 -7
  293. package/src/prompts/journal-context.ts +133 -0
  294. package/src/prompts/persona-resolver.ts +194 -0
  295. package/src/prompts/system-prompt.ts +44 -4
  296. package/src/prompts/templates/SOUL.md +10 -0
  297. package/src/prompts/templates/users/default.md +1 -0
  298. package/src/providers/provider-send-message.ts +3 -32
  299. package/src/providers/registry.ts +29 -179
  300. package/src/providers/types.ts +1 -1
  301. package/src/runtime/access-request-helper.ts +4 -0
  302. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
  303. package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
  304. package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
  305. package/src/runtime/auth/external-assistant-id.ts +13 -59
  306. package/src/runtime/auth/route-policy.ts +17 -1
  307. package/src/runtime/auth/token-service.ts +43 -138
  308. package/src/runtime/channel-readiness-service.ts +1 -16
  309. package/src/runtime/gateway-client.ts +47 -4
  310. package/src/runtime/guardian-decision-types.ts +45 -4
  311. package/src/runtime/http-server.ts +31 -3
  312. package/src/runtime/middleware/error-handler.ts +1 -9
  313. package/src/runtime/routes/access-request-decision.ts +2 -2
  314. package/src/runtime/routes/app-management-routes.ts +2 -1
  315. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
  316. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
  317. package/src/runtime/routes/audio-routes.ts +40 -0
  318. package/src/runtime/routes/btw-routes.ts +0 -17
  319. package/src/runtime/routes/channel-readiness-routes.ts +9 -4
  320. package/src/runtime/routes/conversation-query-routes.ts +63 -1
  321. package/src/runtime/routes/conversation-routes.ts +4 -44
  322. package/src/runtime/routes/debug-routes.ts +12 -9
  323. package/src/runtime/routes/diagnostics-routes.ts +1 -477
  324. package/src/runtime/routes/guardian-approval-interception.ts +168 -11
  325. package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
  326. package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
  327. package/src/runtime/routes/identity-routes.ts +19 -30
  328. package/src/runtime/routes/inbound-message-handler.ts +31 -1
  329. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
  330. package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
  331. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
  332. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
  333. package/src/runtime/routes/integrations/twilio.ts +52 -10
  334. package/src/runtime/routes/integrations/vercel.ts +89 -0
  335. package/src/runtime/routes/log-export-routes.ts +5 -0
  336. package/src/runtime/routes/memory-item-routes.test.ts +3 -3
  337. package/src/runtime/routes/memory-item-routes.ts +46 -14
  338. package/src/runtime/routes/migration-rollback-routes.ts +209 -0
  339. package/src/runtime/routes/migration-routes.ts +17 -1
  340. package/src/runtime/routes/notification-routes.ts +58 -0
  341. package/src/runtime/routes/schedule-routes.ts +65 -0
  342. package/src/runtime/routes/secret-routes.ts +141 -10
  343. package/src/runtime/routes/settings-routes.ts +41 -1
  344. package/src/runtime/routes/tts-routes.ts +96 -0
  345. package/src/runtime/routes/upgrade-broadcast-routes.ts +26 -2
  346. package/src/runtime/routes/workspace-commit-routes.ts +62 -0
  347. package/src/runtime/routes/workspace-routes.test.ts +22 -1
  348. package/src/runtime/routes/workspace-routes.ts +1 -1
  349. package/src/runtime/routes/workspace-utils.ts +86 -2
  350. package/src/security/ces-credential-client.ts +75 -29
  351. package/src/security/ces-rpc-credential-backend.ts +86 -0
  352. package/src/security/credential-backend.ts +22 -92
  353. package/src/security/keychain-broker-client.ts +10 -2
  354. package/src/security/secure-keys.ts +113 -115
  355. package/src/skills/catalog-install.ts +6 -32
  356. package/src/skills/skill-memory.ts +1 -0
  357. package/src/subagent/manager.ts +2 -5
  358. package/src/telemetry/usage-telemetry-reporter.ts +4 -2
  359. package/src/tools/acp/spawn.ts +78 -1
  360. package/src/tools/calls/call-start.ts +1 -0
  361. package/src/tools/credentials/vault.ts +5 -3
  362. package/src/tools/executor.ts +0 -4
  363. package/src/tools/memory/definitions.ts +3 -2
  364. package/src/tools/memory/handlers.ts +10 -7
  365. package/src/tools/network/script-proxy/session-manager.ts +19 -4
  366. package/src/tools/network/web-fetch.ts +3 -1
  367. package/src/tools/skills/execute.ts +1 -1
  368. package/src/tools/terminal/safe-env.ts +1 -0
  369. package/src/tools/types.ts +0 -8
  370. package/src/util/browser.ts +15 -0
  371. package/src/util/errors.ts +0 -12
  372. package/src/util/platform.ts +4 -51
  373. package/src/workspace/git-service.ts +5 -2
  374. package/src/workspace/migrations/001-avatar-rename.ts +15 -0
  375. package/src/workspace/migrations/003-seed-device-id.ts +17 -1
  376. package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
  377. package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
  378. package/src/workspace/migrations/006-services-config.ts +49 -0
  379. package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
  380. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
  381. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
  382. package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
  383. package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
  384. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
  385. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
  386. package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
  387. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
  388. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
  389. package/src/workspace/migrations/017-seed-persona-dirs.ts +96 -0
  390. package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
  391. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
  392. package/src/workspace/migrations/migrate-to-workspace-volume.ts +27 -5
  393. package/src/workspace/migrations/registry.ts +12 -0
  394. package/src/workspace/migrations/runner.ts +106 -2
  395. package/src/workspace/migrations/types.ts +4 -0
  396. package/src/workspace/provider-commit-message-generator.ts +12 -21
  397. package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
  398. package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
  399. package/src/__tests__/diagnostics-export.test.ts +0 -288
  400. package/src/__tests__/local-gateway-health.test.ts +0 -209
  401. package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
  402. package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
  403. package/src/__tests__/secret-ingress-handler.test.ts +0 -120
  404. package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
  405. package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
  406. package/src/__tests__/swarm-orchestrator.test.ts +0 -463
  407. package/src/__tests__/swarm-plan-validator.test.ts +0 -384
  408. package/src/__tests__/swarm-recursion.test.ts +0 -197
  409. package/src/__tests__/swarm-router-planner.test.ts +0 -234
  410. package/src/__tests__/swarm-tool.test.ts +0 -185
  411. package/src/__tests__/swarm-worker-backend.test.ts +0 -144
  412. package/src/__tests__/swarm-worker-runner.test.ts +0 -288
  413. package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
  414. package/src/commands/cc-command-registry.ts +0 -248
  415. package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
  416. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
  417. package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
  418. package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
  419. package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
  420. package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
  421. package/src/config/schemas/swarm.ts +0 -82
  422. package/src/logfire.ts +0 -135
  423. package/src/memory/search/lexical.ts +0 -48
  424. package/src/providers/failover.ts +0 -186
  425. package/src/runtime/local-gateway-health.ts +0 -275
  426. package/src/security/secret-ingress.ts +0 -68
  427. package/src/swarm/backend-claude-code.ts +0 -225
  428. package/src/swarm/checkpoint.ts +0 -137
  429. package/src/swarm/graph-utils.ts +0 -53
  430. package/src/swarm/index.ts +0 -55
  431. package/src/swarm/limits.ts +0 -66
  432. package/src/swarm/orchestrator.ts +0 -424
  433. package/src/swarm/plan-validator.ts +0 -117
  434. package/src/swarm/router-planner.ts +0 -162
  435. package/src/swarm/router-prompts.ts +0 -39
  436. package/src/swarm/synthesizer.ts +0 -81
  437. package/src/swarm/types.ts +0 -72
  438. package/src/swarm/worker-backend.ts +0 -131
  439. package/src/swarm/worker-prompts.ts +0 -80
  440. package/src/swarm/worker-runner.ts +0 -170
  441. package/src/tools/claude-code/claude-code.ts +0 -610
  442. package/src/tools/swarm/delegate.ts +0 -205
@@ -197,7 +197,6 @@ describe("Memory retrieval benchmark", () => {
197
197
  expect(recall.enabled).toBe(true);
198
198
  expect(recall.degraded).toBe(false);
199
199
  // Recency search finds conversation-scoped segments
200
- expect(recall.recencyHits).toBeGreaterThan(0);
201
200
  // Relaxed threshold — guards against severe regressions, not precise benchmarking
202
201
  expect(recall.latencyMs).toBeLessThan(500);
203
202
  });
@@ -216,7 +215,6 @@ describe("Memory retrieval benchmark", () => {
216
215
 
217
216
  expect(recall.enabled).toBe(true);
218
217
  expect(recall.degraded).toBe(false);
219
- expect(recall.recencyHits).toBeGreaterThan(0);
220
218
  expect(recall.latencyMs).toBeLessThan(1000);
221
219
  });
222
220
 
@@ -234,7 +232,6 @@ describe("Memory retrieval benchmark", () => {
234
232
 
235
233
  expect(recall.enabled).toBe(true);
236
234
  expect(recall.degraded).toBe(false);
237
- expect(recall.recencyHits).toBeGreaterThan(0);
238
235
  expect(recall.latencyMs).toBeLessThan(2000);
239
236
  });
240
237
 
@@ -527,9 +527,9 @@ describe("integration: existing routes unaffected", () => {
527
527
  });
528
528
 
529
529
  test("GET /v1/health still works (not intercepted by migration routes)", async () => {
530
- const { handleHealth } =
530
+ const { handleDetailedHealth } =
531
531
  await import("../runtime/routes/identity-routes.js");
532
- const res = handleHealth();
532
+ const res = handleDetailedHealth();
533
533
  const body = (await res.json()) as Record<string, unknown>;
534
534
 
535
535
  expect(res.status).toBe(200);
@@ -939,9 +939,9 @@ describe("route policy registration", () => {
939
939
 
940
940
  describe("integration: existing routes unaffected", () => {
941
941
  test("GET /v1/health still works", async () => {
942
- const { handleHealth } =
942
+ const { handleDetailedHealth } =
943
943
  await import("../runtime/routes/identity-routes.js");
944
- const res = handleHealth();
944
+ const res = handleDetailedHealth();
945
945
  const body = (await res.json()) as Record<string, unknown>;
946
946
 
947
947
  expect(res.status).toBe(200);
@@ -792,9 +792,9 @@ describe("route policy registration", () => {
792
792
 
793
793
  describe("integration: existing routes unaffected", () => {
794
794
  test("GET /v1/health still works", async () => {
795
- const { handleHealth } =
795
+ const { handleDetailedHealth } =
796
796
  await import("../runtime/routes/identity-routes.js");
797
- const res = handleHealth();
797
+ const res = handleDetailedHealth();
798
798
  const body = (await res.json()) as Record<string, unknown>;
799
799
 
800
800
  expect(res.status).toBe(200);
@@ -684,9 +684,9 @@ describe("route policy registration", () => {
684
684
 
685
685
  describe("integration: existing routes unaffected", () => {
686
686
  test("GET /v1/health still works (not intercepted by migration routes)", async () => {
687
- const { handleHealth } =
687
+ const { handleDetailedHealth } =
688
688
  await import("../runtime/routes/identity-routes.js");
689
- const res = handleHealth();
689
+ const res = handleDetailedHealth();
690
690
  const body = (await res.json()) as Record<string, unknown>;
691
691
 
692
692
  expect(res.status).toBe(200);
@@ -37,11 +37,6 @@ mock.module("../util/logger.js", () => ({
37
37
  }),
38
38
  }));
39
39
 
40
- // Mock security check to always pass
41
- mock.module("../security/secret-ingress.js", () => ({
42
- checkIngressForSecrets: () => ({ blocked: false }),
43
- }));
44
-
45
40
  mock.module("../config/env.js", () => ({
46
41
  isHttpAuthDisabled: () => true,
47
42
  getGatewayInternalBaseUrl: () => "http://127.0.0.1:7830",
@@ -191,7 +186,7 @@ describe("non-member access request notification", () => {
191
186
  expect(deliverReplyCalls.length).toBe(1);
192
187
  expect(
193
188
  (deliverReplyCalls[0].payload as Record<string, unknown>).text,
194
- ).toContain("let them know");
189
+ ).toContain("know you tried talking to me");
195
190
  });
196
191
 
197
192
  test("guardian is notified when a non-member messages and a guardian binding exists", async () => {
@@ -290,7 +285,7 @@ describe("non-member access request notification", () => {
290
285
  expect(deliverReplyCalls.length).toBe(1);
291
286
  expect(
292
287
  (deliverReplyCalls[0].payload as Record<string, unknown>).text,
293
- ).toContain("let them know");
288
+ ).toContain("know you tried talking to me");
294
289
 
295
290
  // Notification signal was emitted
296
291
  expect(emitSignalCalls.length).toBe(1);
@@ -34,6 +34,10 @@ mock.module("../notifications/conversation-candidates.js", () => ({
34
34
  serializeCandidatesForPrompt: () => undefined,
35
35
  }));
36
36
 
37
+ mock.module("../prompts/persona-resolver.js", () => ({
38
+ resolveGuardianPersona: () => null,
39
+ }));
40
+
37
41
  let configuredProvider: { sendMessage: () => Promise<unknown> } | null = null;
38
42
  let extractedToolUse: unknown = null;
39
43
 
@@ -36,6 +36,10 @@ mock.module("../notifications/conversation-candidates.js", () => ({
36
36
  serializeCandidatesForPrompt: () => undefined,
37
37
  }));
38
38
 
39
+ mock.module("../prompts/persona-resolver.js", () => ({
40
+ resolveGuardianPersona: () => null,
41
+ }));
42
+
39
43
  // ── Identity context mock ─────────────────────────────────────────────
40
44
 
41
45
  let mockIdentityContext: string | null = null;
@@ -18,6 +18,7 @@ import {
18
18
  hasInviteFlowDirective,
19
19
  normalizeForDirectiveMatching,
20
20
  sanitizeIdentityField,
21
+ sanitizeMessagePreview,
21
22
  } from "../notifications/copy-composer.js";
22
23
  import {
23
24
  enforceGuardianCallConversationAffinity,
@@ -596,6 +597,31 @@ describe("notification decision strategy", () => {
596
597
  });
597
598
  });
598
599
 
600
+ describe("access-request message preview sanitization", () => {
601
+ test("strips control characters from message previews", () => {
602
+ expect(sanitizeMessagePreview("Hello\nWorld")).toBe("Hello World");
603
+ expect(sanitizeMessagePreview("Test\r\nMessage")).toBe("Test Message");
604
+ });
605
+
606
+ test("clamps to 200 characters (not 120)", () => {
607
+ const longMessage = "A".repeat(250);
608
+ const result = sanitizeMessagePreview(longMessage);
609
+ expect(result.length).toBeLessThanOrEqual(201); // 200 + '…'
610
+ expect(result).toEndWith("…");
611
+
612
+ // Verify it allows messages longer than the identity field limit (120)
613
+ const midMessage = "B".repeat(150);
614
+ const midResult = sanitizeMessagePreview(midMessage);
615
+ expect(midResult).toBe(midMessage); // no truncation at 150 chars
616
+ });
617
+
618
+ test("preserves normal messages", () => {
619
+ expect(sanitizeMessagePreview("Hello, can you help me?")).toBe(
620
+ "Hello, can you help me?",
621
+ );
622
+ });
623
+ });
624
+
599
625
  describe("access-request identity line builder", () => {
600
626
  test("builds voice identity line with caller name and phone", () => {
601
627
  const line = buildAccessRequestIdentityLine({
@@ -627,6 +653,51 @@ describe("notification decision strategy", () => {
627
653
  expect(line).toContain("requesting access");
628
654
  });
629
655
 
656
+ test("uses <@U...> mention format for Slack external IDs", () => {
657
+ const line = buildAccessRequestIdentityLine({
658
+ senderIdentifier: "Alice",
659
+ actorExternalId: "U04BTP01B2S",
660
+ sourceChannel: "slack",
661
+ });
662
+ expect(line).toContain("<@U04BTP01B2S>");
663
+ expect(line).not.toContain("[U04BTP01B2S]");
664
+ expect(line).toContain("via slack");
665
+ });
666
+
667
+ test("does not use <@U...> format for non-Slack channels", () => {
668
+ const line = buildAccessRequestIdentityLine({
669
+ senderIdentifier: "Alice",
670
+ actorExternalId: "U04BTP01B2S",
671
+ sourceChannel: "telegram",
672
+ });
673
+ expect(line).toContain("[U04BTP01B2S]");
674
+ expect(line).not.toContain("<@U04BTP01B2S>");
675
+ });
676
+
677
+ test("does not duplicate Slack mention when senderIdentifier equals raw external ID", () => {
678
+ // When actorDisplayName and actorUsername are missing, senderIdentifier
679
+ // falls back to the raw actorExternalId. The identity line should produce
680
+ // exactly one <@U...> mention, not two.
681
+ const line = buildAccessRequestIdentityLine({
682
+ senderIdentifier: "U04BTP01B2S",
683
+ actorExternalId: "U04BTP01B2S",
684
+ sourceChannel: "slack",
685
+ });
686
+ const mentionCount = (line.match(/<@U04BTP01B2S>/g) || []).length;
687
+ expect(mentionCount).toBe(1);
688
+ expect(line).toContain("via slack");
689
+ });
690
+
691
+ test("does not use <@U...> format for non-user-ID external IDs on Slack", () => {
692
+ const line = buildAccessRequestIdentityLine({
693
+ senderIdentifier: "Alice",
694
+ actorExternalId: "someone@example.com",
695
+ sourceChannel: "slack",
696
+ });
697
+ expect(line).not.toContain("<@someone@example.com>");
698
+ expect(line).toContain("[someone@example.com]");
699
+ });
700
+
630
701
  test("sanitizes adversarial display names", () => {
631
702
  const line = buildAccessRequestIdentityLine({
632
703
  senderIdentifier: "Alice",
@@ -155,6 +155,10 @@ mock.module("../oauth/oauth-store.js", () => ({
155
155
  // Stub out transitive dependencies that token-manager would normally pull in
156
156
  mock.module("../security/secure-keys.js", () => ({
157
157
  getSecureKeyAsync: async (account: string) => mockGetSecureKey(account),
158
+ getSecureKeyResultAsync: async (account: string) => ({
159
+ value: mockGetSecureKey(account),
160
+ unreachable: false,
161
+ }),
158
162
  setSecureKeyAsync: async () => true,
159
163
  deleteSecureKeyAsync: async (account: string) => {
160
164
  if (secureKeyStore.has(account)) {
@@ -163,7 +167,7 @@ mock.module("../security/secure-keys.js", () => ({
163
167
  }
164
168
  return "not-found" as const;
165
169
  },
166
- listSecureKeysAsync: async () => [...secureKeyStore.keys()],
170
+ listSecureKeysAsync: async () => ({ accounts: [...secureKeyStore.keys()], unreachable: false }),
167
171
  _resetBackend: () => {},
168
172
  }));
169
173
 
@@ -15,6 +15,7 @@ describe("isAllowDecision", () => {
15
15
  "always_allow",
16
16
  "always_allow_high_risk",
17
17
  "temporary_override",
18
+ "dangerously_skip_permissions",
18
19
  ];
19
20
 
20
21
  for (const decision of allowDecisions) {
@@ -69,13 +69,9 @@ const mockProvider: Provider = {
69
69
  let resolvedProvider: {
70
70
  provider: Provider;
71
71
  configuredProviderName: string;
72
- selectedProviderName: string;
73
- usedFallbackPrimary: boolean;
74
72
  } | null = {
75
73
  provider: mockProvider,
76
74
  configuredProviderName: "anthropic",
77
- selectedProviderName: "anthropic",
78
- usedFallbackPrimary: false,
79
75
  };
80
76
 
81
77
  mock.module("../providers/provider-send-message.js", () => ({
@@ -130,8 +126,6 @@ describe("ProviderCommitMessageGenerator", () => {
130
126
  resolvedProvider = {
131
127
  provider: mockProvider,
132
128
  configuredProviderName: "anthropic",
133
- selectedProviderName: "anthropic",
134
- usedFallbackPrimary: false,
135
129
  };
136
130
  });
137
131
 
@@ -343,8 +337,6 @@ describe("ProviderCommitMessageGenerator", () => {
343
337
  resolvedProvider = {
344
338
  provider: mockProvider,
345
339
  configuredProviderName: "ollama",
346
- selectedProviderName: "ollama",
347
- usedFallbackPrimary: false,
348
340
  };
349
341
  const gen = getCommitMessageGenerator();
350
342
  const result = await gen.generateCommitMessage(baseContext, {
@@ -364,8 +356,6 @@ describe("ProviderCommitMessageGenerator", () => {
364
356
  resolvedProvider = {
365
357
  provider: mockProvider,
366
358
  configuredProviderName: "exotic-provider",
367
- selectedProviderName: "exotic-provider",
368
- usedFallbackPrimary: false,
369
359
  };
370
360
  const gen = getCommitMessageGenerator();
371
361
  const result = await gen.generateCommitMessage(baseContext, {
@@ -383,8 +373,6 @@ describe("ProviderCommitMessageGenerator", () => {
383
373
  resolvedProvider = {
384
374
  provider: mockProvider,
385
375
  configuredProviderName: "ollama",
386
- selectedProviderName: "ollama",
387
- usedFallbackPrimary: false,
388
376
  };
389
377
  currentConfig.workspaceGit.commitMessageLLM.providerFastModelOverrides = {
390
378
  ollama: "llama3.2:3b",
@@ -404,29 +392,4 @@ describe("ProviderCommitMessageGenerator", () => {
404
392
  expect(options.config.model).toBe("llama3.2:3b");
405
393
  });
406
394
 
407
- // 15. Fail-open fallback provider uses fallback provider's fast-model mapping
408
- test("configured provider unavailable -> selected fallback provider model mapping is used", async () => {
409
- currentConfig.services.inference.provider = "anthropic";
410
- currentConfig.providerOrder = ["openai"];
411
- mockSecureKeys = { openai: "sk-openai" };
412
- resolvedProvider = {
413
- provider: mockProvider,
414
- configuredProviderName: "anthropic",
415
- selectedProviderName: "openai",
416
- usedFallbackPrimary: true,
417
- };
418
- mockSendMessage.mockResolvedValueOnce(
419
- makeSuccessResponse("fix: fail-open commit"),
420
- );
421
-
422
- const gen = getCommitMessageGenerator();
423
- const result = await gen.generateCommitMessage(baseContext, {
424
- changedFiles: baseContext.changedFiles,
425
- });
426
-
427
- expect(result.source).toBe("llm");
428
- const callArgs = mockSendMessage.mock.calls[0];
429
- const options = callArgs[3] as { config: { model: string } };
430
- expect(options.config.model).toBe("gpt-4o-mini");
431
- });
432
395
  });
@@ -107,7 +107,6 @@ mock.module("../util/retry.js", () => {
107
107
  };
108
108
  });
109
109
 
110
- import { FailoverProvider } from "../providers/failover.js";
111
110
  import { RetryProvider } from "../providers/retry.js";
112
111
  import { createStreamTimeout } from "../providers/stream-timeout.js";
113
112
  import type {
@@ -139,18 +138,6 @@ function successResponse(
139
138
  };
140
139
  }
141
140
 
142
- function makeProvider(name = "mock"): Provider & { calls: number } {
143
- const p = {
144
- name,
145
- calls: 0,
146
- async sendMessage(): Promise<ProviderResponse> {
147
- p.calls++;
148
- return successResponse();
149
- },
150
- };
151
- return p;
152
- }
153
-
154
141
  /** Provider that fails N times then succeeds. */
155
142
  function makeFlaky(
156
143
  failCount: number,
@@ -646,220 +633,6 @@ describe("RetryProvider — streaming response handling", () => {
646
633
  });
647
634
  });
648
635
 
649
- // ---------------------------------------------------------------------------
650
- // FailoverProvider — model unavailability fallback
651
- // ---------------------------------------------------------------------------
652
-
653
- describe("FailoverProvider — model unavailability fallback", () => {
654
- test("falls back to secondary when primary returns 500", async () => {
655
- const primary = makeFailing(
656
- new ProviderError("down", "primary", 500),
657
- "primary",
658
- );
659
- const secondary = makeProvider("secondary");
660
- const provider = new FailoverProvider([primary, secondary]);
661
-
662
- const result = await provider.sendMessage(MESSAGES);
663
-
664
- expect(primary.calls).toBe(1);
665
- expect(secondary.calls).toBe(1);
666
- expect(result.stopReason).toBe("end_turn");
667
- });
668
-
669
- test("falls back to secondary when primary returns 429", async () => {
670
- const primary = makeFailing(
671
- new ProviderError("rate limited", "primary", 429),
672
- "primary",
673
- );
674
- const secondary = makeProvider("secondary");
675
- const provider = new FailoverProvider([primary, secondary]);
676
-
677
- const result = await provider.sendMessage(MESSAGES);
678
-
679
- expect(primary.calls).toBe(1);
680
- expect(secondary.calls).toBe(1);
681
- expect(result.model).toBe("test-model");
682
- });
683
-
684
- test("falls back on ECONNREFUSED network error", async () => {
685
- const err = new Error("connection refused");
686
- (err as NodeJS.ErrnoException).code = "ECONNREFUSED";
687
- const primary = makeFailing(err, "primary");
688
- const secondary = makeProvider("secondary");
689
- const provider = new FailoverProvider([primary, secondary]);
690
-
691
- const result = await provider.sendMessage(MESSAGES);
692
-
693
- expect(primary.calls).toBe(1);
694
- expect(secondary.calls).toBe(1);
695
- expect(result.content[0]).toMatchObject({ type: "text", text: "ok" });
696
- });
697
-
698
- test("falls back on ProviderError without status code (connection failure)", async () => {
699
- const primary = makeFailing(
700
- new ProviderError("connection failed", "primary"),
701
- "primary",
702
- );
703
- const secondary = makeProvider("secondary");
704
- const provider = new FailoverProvider([primary, secondary]);
705
-
706
- const _result = await provider.sendMessage(MESSAGES);
707
- expect(primary.calls).toBe(1);
708
- expect(secondary.calls).toBe(1);
709
- });
710
-
711
- test("does NOT fall back on 400 Bad Request", async () => {
712
- const primary = makeFailing(
713
- new ProviderError("bad request", "primary", 400),
714
- "primary",
715
- );
716
- const secondary = makeProvider("secondary");
717
- const provider = new FailoverProvider([primary, secondary]);
718
-
719
- await expect(provider.sendMessage(MESSAGES)).rejects.toThrow("bad request");
720
- expect(primary.calls).toBe(1);
721
- expect(secondary.calls).toBe(0);
722
- });
723
-
724
- test("does NOT fall back on 401 Unauthorized", async () => {
725
- const primary = makeFailing(
726
- new ProviderError("unauthorized", "primary", 401),
727
- "primary",
728
- );
729
- const secondary = makeProvider("secondary");
730
- const provider = new FailoverProvider([primary, secondary]);
731
-
732
- await expect(provider.sendMessage(MESSAGES)).rejects.toThrow(
733
- "unauthorized",
734
- );
735
- expect(secondary.calls).toBe(0);
736
- });
737
-
738
- test("throws last error when all providers fail", async () => {
739
- const p1 = makeFailing(new ProviderError("p1 down", "p1", 500), "p1");
740
- const p2 = makeFailing(new ProviderError("p2 down", "p2", 503), "p2");
741
- const p3 = makeFailing(new ProviderError("p3 down", "p3", 502), "p3");
742
- const provider = new FailoverProvider([p1, p2, p3]);
743
-
744
- try {
745
- await provider.sendMessage(MESSAGES);
746
- expect(true).toBe(false);
747
- } catch (err) {
748
- expect(err).toBeInstanceOf(ProviderError);
749
- // Last provider's error is thrown
750
- expect((err as ProviderError).message).toBe("p3 down");
751
- }
752
- expect(p1.calls).toBe(1);
753
- expect(p2.calls).toBe(1);
754
- expect(p3.calls).toBe(1);
755
- });
756
-
757
- test("chains through three providers when first two fail", async () => {
758
- const p1 = makeFailing(new ProviderError("p1 error", "p1", 500), "p1");
759
- const p2 = makeFailing(new ProviderError("p2 error", "p2", 502), "p2");
760
- const p3 = makeProvider("p3");
761
- const provider = new FailoverProvider([p1, p2, p3]);
762
-
763
- const result = await provider.sendMessage(MESSAGES);
764
-
765
- expect(p1.calls).toBe(1);
766
- expect(p2.calls).toBe(1);
767
- expect(p3.calls).toBe(1);
768
- expect(result.stopReason).toBe("end_turn");
769
- });
770
-
771
- test("requires at least one provider", () => {
772
- expect(() => new FailoverProvider([])).toThrow(
773
- "FailoverProvider requires at least one provider",
774
- );
775
- });
776
- });
777
-
778
- // ---------------------------------------------------------------------------
779
- // FailoverProvider — cooldown and recovery
780
- // ---------------------------------------------------------------------------
781
-
782
- describe("FailoverProvider — cooldown and recovery", () => {
783
- test("skips provider in cooldown period", async () => {
784
- const primary = makeFailing(
785
- new ProviderError("down", "primary", 500),
786
- "primary",
787
- );
788
- const secondary = makeProvider("secondary");
789
- // Use a long cooldown so primary stays unhealthy
790
- const provider = new FailoverProvider([primary, secondary], 60_000);
791
-
792
- // First call: primary fails, secondary succeeds
793
- await provider.sendMessage(MESSAGES);
794
- expect(primary.calls).toBe(1);
795
- expect(secondary.calls).toBe(1);
796
-
797
- // Second call: primary is in cooldown, skipped — goes straight to secondary
798
- await provider.sendMessage(MESSAGES);
799
- expect(primary.calls).toBe(1); // not called again
800
- expect(secondary.calls).toBe(2);
801
- });
802
-
803
- test("retries provider after cooldown expires", async () => {
804
- let primaryCallCount = 0;
805
- const primary: Provider = {
806
- name: "primary",
807
- async sendMessage() {
808
- primaryCallCount++;
809
- if (primaryCallCount === 1) {
810
- throw new ProviderError("temporarily down", "primary", 500);
811
- }
812
- return successResponse();
813
- },
814
- };
815
- const secondary = makeProvider("secondary");
816
- // Very short cooldown
817
- const provider = new FailoverProvider([primary, secondary], 1);
818
-
819
- // First call: primary fails, marked unhealthy, secondary succeeds
820
- await provider.sendMessage(MESSAGES);
821
- expect(primaryCallCount).toBe(1);
822
-
823
- // Wait for cooldown to expire
824
- await new Promise((r) => setTimeout(r, 10));
825
-
826
- // Second call: primary should be retried after cooldown expired
827
- await provider.sendMessage(MESSAGES);
828
- expect(primaryCallCount).toBe(2);
829
- });
830
-
831
- test("marks provider healthy after successful recovery", async () => {
832
- let primaryCallCount = 0;
833
- const primary: Provider = {
834
- name: "primary",
835
- async sendMessage() {
836
- primaryCallCount++;
837
- if (primaryCallCount === 1) {
838
- throw new ProviderError("blip", "primary", 500);
839
- }
840
- return successResponse();
841
- },
842
- };
843
- const secondary = makeProvider("secondary");
844
- const provider = new FailoverProvider([primary, secondary], 1);
845
-
846
- // First call: primary fails
847
- await provider.sendMessage(MESSAGES);
848
- expect(primaryCallCount).toBe(1);
849
-
850
- // Wait for cooldown
851
- await new Promise((r) => setTimeout(r, 10));
852
-
853
- // Second call: primary recovers
854
- await provider.sendMessage(MESSAGES);
855
- expect(primaryCallCount).toBe(2);
856
-
857
- // Third call: primary is healthy, used directly
858
- await provider.sendMessage(MESSAGES);
859
- expect(primaryCallCount).toBe(3);
860
- expect(secondary.calls).toBe(1); // only called once during initial failover
861
- });
862
- });
863
636
 
864
637
  // ---------------------------------------------------------------------------
865
638
  // createStreamTimeout — edge cases
@@ -910,43 +683,3 @@ describe("createStreamTimeout — edge cases", () => {
910
683
  });
911
684
  });
912
685
 
913
- // ---------------------------------------------------------------------------
914
- // RetryProvider + FailoverProvider — combined scenarios
915
- // ---------------------------------------------------------------------------
916
-
917
- describe("RetryProvider + FailoverProvider — combined", () => {
918
- test("failover wrapping retry: each provider in the chain retries independently", async () => {
919
- // Primary always fails with 500, secondary succeeds
920
- const primary = makeFailing(
921
- new ProviderError("primary down", "primary", 500),
922
- "primary",
923
- );
924
- const secondary = makeProvider("secondary");
925
-
926
- // Wrap each in RetryProvider, then combine with FailoverProvider
927
- const retryPrimary = new RetryProvider(primary);
928
- const retrySecondary = new RetryProvider(secondary);
929
- const failover = new FailoverProvider([retryPrimary, retrySecondary]);
930
-
931
- const result = await failover.sendMessage(MESSAGES);
932
- expect(result.stopReason).toBe("end_turn");
933
- // Primary should have been retried MAX_RETRIES + 1 times before failover
934
- expect(primary.calls).toBe(DEFAULT_MAX_RETRIES + 1);
935
- expect(secondary.calls).toBe(1);
936
- });
937
-
938
- test("single provider: retry exhaustion produces the original error", async () => {
939
- const inner = makeFailing(new ProviderError("always fail", "solo", 500));
940
- const retrying = new RetryProvider(inner);
941
-
942
- try {
943
- await retrying.sendMessage(MESSAGES);
944
- expect(true).toBe(false);
945
- } catch (err) {
946
- expect(err).toBeInstanceOf(ProviderError);
947
- expect((err as ProviderError).message).toBe("always fail");
948
- expect((err as ProviderError).statusCode).toBe(500);
949
- }
950
- expect(inner.calls).toBe(DEFAULT_MAX_RETRIES + 1);
951
- });
952
- });
@@ -193,13 +193,12 @@ describe("managed proxy integration — credential precedence", () => {
193
193
 
194
194
  const provider = getProvider("anthropic");
195
195
 
196
- // Unwrap RetryProvider → LogfireProvider → AnthropicProvider to inspect
197
- // the Anthropic SDK client's baseURL. The wrappers use private `inner`
198
- // and AnthropicProvider stores the SDK client as private `client`.
196
+ // Unwrap RetryProvider → AnthropicProvider to inspect the Anthropic
197
+ // SDK client's baseURL. RetryProvider stores the inner provider as
198
+ // private `inner` and AnthropicProvider stores the SDK client as
199
+ // private `client`.
199
200
  const retryInner = (provider as any).inner;
200
- // retryInner is the logfire wrapper; it also has an `inner` property
201
- const logfireInner = (retryInner as any).inner ?? retryInner;
202
- const anthropicClient = (logfireInner as any).client;
201
+ const anthropicClient = (retryInner as any).client;
203
202
 
204
203
  expect(anthropicClient).toBeDefined();
205
204
  const baseURL: string = anthropicClient.baseURL;