@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.
- package/.env.example +16 -2
- package/ARCHITECTURE.md +6 -75
- package/Dockerfile +3 -2
- package/README.md +0 -2
- package/bun.lock +0 -414
- package/docker-entrypoint.sh +9 -0
- package/docs/architecture/keychain-broker.md +45 -240
- package/docs/architecture/memory.md +13 -11
- package/docs/architecture/security.md +0 -17
- package/docs/credential-execution-service.md +2 -2
- package/node_modules/@vellumai/ces-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +120 -1
- package/node_modules/@vellumai/credential-storage/package.json +1 -0
- package/node_modules/@vellumai/egress-proxy/package.json +1 -0
- package/package.json +2 -3
- package/src/__tests__/actor-token-service.test.ts +0 -114
- package/src/__tests__/approval-cascade.test.ts +0 -1
- package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/browser-skill-endstate.test.ts +6 -5
- package/src/__tests__/btw-routes.test.ts +0 -39
- package/src/__tests__/call-controller.test.ts +0 -1
- package/src/__tests__/call-domain.test.ts +0 -128
- package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
- package/src/__tests__/ces-startup-timeout.test.ts +40 -0
- package/src/__tests__/channel-approval-routes.test.ts +0 -5
- package/src/__tests__/channel-readiness-service.test.ts +1 -60
- package/src/__tests__/checker.test.ts +4 -2
- package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
- package/src/__tests__/config-schema-cmd.test.ts +0 -2
- package/src/__tests__/config-schema.test.ts +3 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop.test.ts +2 -4
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
- package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
- package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
- package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
- package/src/__tests__/conversation-queue.test.ts +0 -1
- package/src/__tests__/conversation-skill-tools.test.ts +0 -54
- package/src/__tests__/conversation-slash-queue.test.ts +0 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
- package/src/__tests__/conversation-title-service.test.ts +87 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
- package/src/__tests__/credential-execution-client.test.ts +5 -2
- package/src/__tests__/credential-execution-feature-gates.test.ts +59 -30
- package/src/__tests__/credential-execution-managed-contract.test.ts +35 -20
- package/src/__tests__/credential-security-e2e.test.ts +1 -67
- package/src/__tests__/credential-security-invariants.test.ts +6 -50
- package/src/__tests__/credentials-cli.test.ts +82 -3
- package/src/__tests__/daemon-credential-client.test.ts +123 -0
- package/src/__tests__/db-migration-rollback.test.ts +2015 -1
- package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +34 -143
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
- package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
- package/src/__tests__/guardian-routing-state.test.ts +0 -5
- package/src/__tests__/host-shell-tool.test.ts +6 -7
- package/src/__tests__/http-user-message-parity.test.ts +3 -103
- package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
- package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
- package/src/__tests__/intent-routing.test.ts +0 -13
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
- package/src/__tests__/journal-context.test.ts +335 -0
- package/src/__tests__/keychain-broker-client.test.ts +161 -22
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
- package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
- package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
- package/src/__tests__/memory-recall-quality.test.ts +48 -17
- package/src/__tests__/memory-regressions.test.ts +408 -363
- package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
- package/src/__tests__/migration-export-http.test.ts +2 -2
- package/src/__tests__/migration-import-commit-http.test.ts +2 -2
- package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
- package/src/__tests__/migration-validate-http.test.ts +2 -2
- package/src/__tests__/non-member-access-request.test.ts +2 -7
- package/src/__tests__/notification-decision-fallback.test.ts +4 -0
- package/src/__tests__/notification-decision-identity.test.ts +4 -0
- package/src/__tests__/notification-decision-strategy.test.ts +71 -0
- package/src/__tests__/oauth-cli.test.ts +5 -1
- package/src/__tests__/permission-types.test.ts +1 -0
- package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
- package/src/__tests__/provider-error-scenarios.test.ts +0 -267
- package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
- package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
- package/src/__tests__/qdrant-manager.test.ts +28 -2
- package/src/__tests__/registry.test.ts +0 -6
- package/src/__tests__/relay-server.test.ts +1 -2
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
- package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -1
- package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
- package/src/__tests__/secure-keys.test.ts +95 -272
- package/src/__tests__/shell-identity.test.ts +96 -6
- package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
- package/src/__tests__/skill-feature-flags.test.ts +46 -45
- package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
- package/src/__tests__/skill-load-inline-command.test.ts +8 -12
- package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
- package/src/__tests__/skill-load-tool.test.ts +0 -2
- package/src/__tests__/skill-memory.test.ts +17 -3
- package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
- package/src/__tests__/skills.test.ts +0 -2
- package/src/__tests__/slack-inbound-verification.test.ts +0 -4
- package/src/__tests__/stale-approval-dedup.test.ts +171 -0
- package/src/__tests__/stt-hints.test.ts +437 -0
- package/src/__tests__/suggestion-routes.test.ts +1 -32
- package/src/__tests__/system-prompt.test.ts +0 -1
- package/src/__tests__/task-memory-cleanup.test.ts +14 -0
- package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
- package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
- package/src/__tests__/update-bulletin.test.ts +0 -2
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
- package/src/__tests__/voice-quality.test.ts +58 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -7
- package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
- package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +220 -0
- package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
- package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
- package/src/acp/agent-process.ts +9 -1
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-request-resolvers.ts +164 -38
- package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
- package/src/calls/audio-store.test.ts +97 -0
- package/src/calls/audio-store.ts +205 -0
- package/src/calls/call-controller.ts +90 -8
- package/src/calls/call-domain.ts +3 -0
- package/src/calls/call-store.ts +10 -3
- package/src/calls/fish-audio-client.ts +129 -0
- package/src/calls/relay-server.ts +27 -0
- package/src/calls/stt-hints.ts +189 -0
- package/src/calls/tts-text-sanitizer.ts +61 -0
- package/src/calls/twilio-routes.ts +34 -5
- package/src/calls/types.ts +1 -0
- package/src/calls/voice-ingress-preflight.ts +0 -42
- package/src/calls/voice-quality.ts +38 -5
- package/src/calls/voice-session-bridge.ts +7 -12
- package/src/cli/commands/avatar.ts +2 -2
- package/src/cli/commands/config.ts +1 -4
- package/src/cli/commands/credentials.ts +128 -82
- package/src/cli/commands/doctor.ts +2 -2
- package/src/cli/commands/keys.ts +7 -7
- package/src/cli/commands/memory.ts +1 -1
- package/src/cli/commands/oauth/connections.ts +11 -29
- package/src/cli/commands/oauth/index.ts +7 -0
- package/src/cli/commands/oauth/platform.ts +525 -0
- package/src/cli/commands/platform.ts +3 -3
- package/src/cli/lib/daemon-credential-client.ts +284 -0
- package/src/cli.ts +1 -1
- package/src/config/assistant-feature-flags.ts +186 -5
- package/src/config/bundled-skills/AGENTS.md +34 -0
- package/src/config/bundled-skills/acp/SKILL.md +10 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
- package/src/config/bundled-skills/messaging/SKILL.md +5 -5
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
- package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
- package/src/config/bundled-skills/settings/SKILL.md +15 -2
- package/src/config/bundled-skills/settings/TOOLS.json +47 -2
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
- package/src/config/bundled-skills/slack/SKILL.md +1 -1
- package/src/config/bundled-tool-registry.ts +5 -11
- package/src/config/defaults.ts +0 -2
- package/src/config/env-registry.ts +5 -5
- package/src/config/env.ts +21 -14
- package/src/config/feature-flag-registry.json +49 -9
- package/src/config/loader.ts +106 -42
- package/src/config/schema.ts +9 -29
- package/src/config/schemas/calls.ts +30 -0
- package/src/config/schemas/fish-audio.ts +39 -0
- package/src/config/schemas/inference.ts +2 -2
- package/src/config/schemas/journal.ts +16 -0
- package/src/config/schemas/memory-processing.ts +2 -2
- package/src/config/schemas/security.ts +0 -4
- package/src/config/types.ts +1 -1
- package/src/contacts/contact-store.ts +39 -0
- package/src/contacts/types.ts +2 -0
- package/src/credential-execution/approval-bridge.ts +1 -0
- package/src/credential-execution/executable-discovery.ts +28 -4
- package/src/credential-execution/feature-gates.ts +16 -0
- package/src/credential-execution/process-manager.ts +38 -0
- package/src/credential-execution/startup-timeout.ts +36 -0
- package/src/daemon/approval-generators.ts +3 -9
- package/src/daemon/assistant-attachments.ts +9 -0
- package/src/daemon/config-watcher.ts +5 -0
- package/src/daemon/conversation-error.ts +13 -1
- package/src/daemon/conversation-memory.ts +1 -2
- package/src/daemon/conversation-process.ts +18 -1
- package/src/daemon/conversation-surfaces.ts +30 -1
- package/src/daemon/conversation-tool-setup.ts +0 -105
- package/src/daemon/conversation.ts +21 -1
- package/src/daemon/guardian-action-generators.ts +3 -9
- package/src/daemon/handlers/config-vercel.ts +92 -0
- package/src/daemon/handlers/skills.ts +2 -15
- package/src/daemon/install-symlink.ts +195 -0
- package/src/daemon/lifecycle.ts +234 -51
- package/src/daemon/message-types/conversations.ts +4 -4
- package/src/daemon/message-types/diagnostics.ts +3 -22
- package/src/daemon/message-types/messages.ts +0 -2
- package/src/daemon/message-types/upgrades.ts +8 -0
- package/src/daemon/server.ts +32 -95
- package/src/events/domain-events.ts +2 -1
- package/src/inbound/platform-callback-registration.ts +3 -3
- package/src/instrument.ts +8 -5
- package/src/memory/app-store.ts +31 -0
- package/src/memory/conversation-title-service.ts +50 -1
- package/src/memory/db-init.ts +16 -0
- package/src/memory/indexer.ts +19 -10
- package/src/memory/items-extractor.ts +328 -321
- package/src/memory/job-handlers/conversation-starters.ts +4 -1
- package/src/memory/job-handlers/summarization.ts +26 -16
- package/src/memory/jobs-store.ts +63 -6
- package/src/memory/jobs-worker.ts +31 -7
- package/src/memory/journal-memory.ts +214 -0
- package/src/memory/migrations/001-job-deferrals.ts +19 -0
- package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
- package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
- package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
- package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
- package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
- package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
- package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
- package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
- package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
- package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
- package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
- package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
- package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
- package/src/memory/migrations/116-messages-fts.ts +106 -1
- package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
- package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
- package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
- package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
- package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
- package/src/memory/migrations/141-rename-verification-table.ts +54 -0
- package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
- package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
- package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
- package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
- package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
- package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
- package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
- package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
- package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
- package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
- package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
- package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
- package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
- package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
- package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
- package/src/memory/migrations/index.ts +5 -0
- package/src/memory/migrations/registry.ts +98 -0
- package/src/memory/migrations/validate-migration-state.ts +137 -11
- package/src/memory/qdrant-circuit-breaker.ts +9 -0
- package/src/memory/qdrant-manager.ts +64 -7
- package/src/memory/retriever.test.ts +37 -25
- package/src/memory/retriever.ts +24 -49
- package/src/memory/schema/calls.ts +1 -0
- package/src/memory/schema/contacts.ts +1 -0
- package/src/memory/schema/memory-core.ts +2 -0
- package/src/memory/search/formatting.ts +7 -44
- package/src/memory/search/staleness.ts +4 -0
- package/src/memory/search/tier-classifier.ts +10 -2
- package/src/memory/search/types.ts +2 -5
- package/src/memory/task-memory-cleanup.ts +4 -3
- package/src/notifications/adapters/slack.ts +168 -6
- package/src/notifications/broadcaster.ts +1 -0
- package/src/notifications/copy-composer.ts +59 -2
- package/src/notifications/decision-engine.ts +4 -1
- package/src/notifications/signal.ts +2 -0
- package/src/notifications/types.ts +2 -0
- package/src/oauth/connection-resolver.ts +6 -4
- package/src/permissions/checker.ts +0 -38
- package/src/permissions/shell-identity.ts +76 -22
- package/src/permissions/types.ts +4 -2
- package/src/platform/client.ts +35 -7
- package/src/prompts/journal-context.ts +133 -0
- package/src/prompts/persona-resolver.ts +194 -0
- package/src/prompts/system-prompt.ts +44 -4
- package/src/prompts/templates/SOUL.md +10 -0
- package/src/prompts/templates/users/default.md +1 -0
- package/src/providers/provider-send-message.ts +3 -32
- package/src/providers/registry.ts +29 -179
- package/src/providers/types.ts +1 -1
- package/src/runtime/access-request-helper.ts +4 -0
- package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
- package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
- package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
- package/src/runtime/auth/external-assistant-id.ts +13 -59
- package/src/runtime/auth/route-policy.ts +17 -1
- package/src/runtime/auth/token-service.ts +43 -138
- package/src/runtime/channel-readiness-service.ts +1 -16
- package/src/runtime/gateway-client.ts +47 -4
- package/src/runtime/guardian-decision-types.ts +45 -4
- package/src/runtime/http-server.ts +31 -3
- package/src/runtime/middleware/error-handler.ts +1 -9
- package/src/runtime/routes/access-request-decision.ts +2 -2
- package/src/runtime/routes/app-management-routes.ts +2 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
- package/src/runtime/routes/audio-routes.ts +40 -0
- package/src/runtime/routes/btw-routes.ts +0 -17
- package/src/runtime/routes/channel-readiness-routes.ts +9 -4
- package/src/runtime/routes/conversation-query-routes.ts +63 -1
- package/src/runtime/routes/conversation-routes.ts +4 -44
- package/src/runtime/routes/debug-routes.ts +12 -9
- package/src/runtime/routes/diagnostics-routes.ts +1 -477
- package/src/runtime/routes/guardian-approval-interception.ts +168 -11
- package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
- package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
- package/src/runtime/routes/identity-routes.ts +19 -30
- package/src/runtime/routes/inbound-message-handler.ts +31 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
- package/src/runtime/routes/integrations/twilio.ts +52 -10
- package/src/runtime/routes/integrations/vercel.ts +89 -0
- package/src/runtime/routes/log-export-routes.ts +5 -0
- package/src/runtime/routes/memory-item-routes.test.ts +3 -3
- package/src/runtime/routes/memory-item-routes.ts +46 -14
- package/src/runtime/routes/migration-rollback-routes.ts +209 -0
- package/src/runtime/routes/migration-routes.ts +17 -1
- package/src/runtime/routes/notification-routes.ts +58 -0
- package/src/runtime/routes/schedule-routes.ts +65 -0
- package/src/runtime/routes/secret-routes.ts +141 -10
- package/src/runtime/routes/settings-routes.ts +41 -1
- package/src/runtime/routes/tts-routes.ts +96 -0
- package/src/runtime/routes/upgrade-broadcast-routes.ts +26 -2
- package/src/runtime/routes/workspace-commit-routes.ts +62 -0
- package/src/runtime/routes/workspace-routes.test.ts +22 -1
- package/src/runtime/routes/workspace-routes.ts +1 -1
- package/src/runtime/routes/workspace-utils.ts +86 -2
- package/src/security/ces-credential-client.ts +75 -29
- package/src/security/ces-rpc-credential-backend.ts +86 -0
- package/src/security/credential-backend.ts +22 -92
- package/src/security/keychain-broker-client.ts +10 -2
- package/src/security/secure-keys.ts +113 -115
- package/src/skills/catalog-install.ts +6 -32
- package/src/skills/skill-memory.ts +1 -0
- package/src/subagent/manager.ts +2 -5
- package/src/telemetry/usage-telemetry-reporter.ts +4 -2
- package/src/tools/acp/spawn.ts +78 -1
- package/src/tools/calls/call-start.ts +1 -0
- package/src/tools/credentials/vault.ts +5 -3
- package/src/tools/executor.ts +0 -4
- package/src/tools/memory/definitions.ts +3 -2
- package/src/tools/memory/handlers.ts +10 -7
- package/src/tools/network/script-proxy/session-manager.ts +19 -4
- package/src/tools/network/web-fetch.ts +3 -1
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/tools/types.ts +0 -8
- package/src/util/browser.ts +15 -0
- package/src/util/errors.ts +0 -12
- package/src/util/platform.ts +4 -51
- package/src/workspace/git-service.ts +5 -2
- package/src/workspace/migrations/001-avatar-rename.ts +15 -0
- package/src/workspace/migrations/003-seed-device-id.ts +17 -1
- package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
- package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
- package/src/workspace/migrations/006-services-config.ts +49 -0
- package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
- package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
- package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
- package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
- package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
- package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
- package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
- package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
- package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
- package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
- package/src/workspace/migrations/017-seed-persona-dirs.ts +96 -0
- package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
- package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +27 -5
- package/src/workspace/migrations/registry.ts +12 -0
- package/src/workspace/migrations/runner.ts +106 -2
- package/src/workspace/migrations/types.ts +4 -0
- package/src/workspace/provider-commit-message-generator.ts +12 -21
- package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
- package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
- package/src/__tests__/diagnostics-export.test.ts +0 -288
- package/src/__tests__/local-gateway-health.test.ts +0 -209
- package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
- package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
- package/src/__tests__/secret-ingress-handler.test.ts +0 -120
- package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
- package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
- package/src/__tests__/swarm-orchestrator.test.ts +0 -463
- package/src/__tests__/swarm-plan-validator.test.ts +0 -384
- package/src/__tests__/swarm-recursion.test.ts +0 -197
- package/src/__tests__/swarm-router-planner.test.ts +0 -234
- package/src/__tests__/swarm-tool.test.ts +0 -185
- package/src/__tests__/swarm-worker-backend.test.ts +0 -144
- package/src/__tests__/swarm-worker-runner.test.ts +0 -288
- package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
- package/src/commands/cc-command-registry.ts +0 -248
- package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
- package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
- package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
- package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
- package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
- package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
- package/src/config/schemas/swarm.ts +0 -82
- package/src/logfire.ts +0 -135
- package/src/memory/search/lexical.ts +0 -48
- package/src/providers/failover.ts +0 -186
- package/src/runtime/local-gateway-health.ts +0 -275
- package/src/security/secret-ingress.ts +0 -68
- package/src/swarm/backend-claude-code.ts +0 -225
- package/src/swarm/checkpoint.ts +0 -137
- package/src/swarm/graph-utils.ts +0 -53
- package/src/swarm/index.ts +0 -55
- package/src/swarm/limits.ts +0 -66
- package/src/swarm/orchestrator.ts +0 -424
- package/src/swarm/plan-validator.ts +0 -117
- package/src/swarm/router-planner.ts +0 -162
- package/src/swarm/router-prompts.ts +0 -39
- package/src/swarm/synthesizer.ts +0 -81
- package/src/swarm/types.ts +0 -72
- package/src/swarm/worker-backend.ts +0 -131
- package/src/swarm/worker-prompts.ts +0 -80
- package/src/swarm/worker-runner.ts +0 -170
- package/src/tools/claude-code/claude-code.ts +0 -610
- 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 {
|
|
530
|
+
const { handleDetailedHealth } =
|
|
531
531
|
await import("../runtime/routes/identity-routes.js");
|
|
532
|
-
const res =
|
|
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 {
|
|
942
|
+
const { handleDetailedHealth } =
|
|
943
943
|
await import("../runtime/routes/identity-routes.js");
|
|
944
|
-
const res =
|
|
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 {
|
|
795
|
+
const { handleDetailedHealth } =
|
|
796
796
|
await import("../runtime/routes/identity-routes.js");
|
|
797
|
-
const res =
|
|
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 {
|
|
687
|
+
const { handleDetailedHealth } =
|
|
688
688
|
await import("../runtime/routes/identity-routes.js");
|
|
689
|
-
const res =
|
|
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("
|
|
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("
|
|
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
|
|
|
@@ -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 →
|
|
197
|
-
//
|
|
198
|
-
// and AnthropicProvider stores the SDK client as
|
|
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
|
-
|
|
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;
|