@vellumai/assistant 0.11.6 → 0.11.7-staging.2
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/ARCHITECTURE.md +31 -19
- package/Dockerfile +1 -0
- package/docs/architecture/security.md +3 -3
- package/docs/flux-turn-detection-spike.md +8 -7
- package/docs/runbook-trusted-contacts.md +14 -0
- package/docs/stt-provider-onboarding.md +8 -4
- package/knip.json +1 -0
- package/node_modules/@vellumai/avatar-manifest/package.json +18 -0
- package/node_modules/@vellumai/avatar-manifest/src/__tests__/manifest.test.ts +173 -0
- package/node_modules/@vellumai/avatar-manifest/src/__tests__/read.test.ts +114 -0
- package/node_modules/@vellumai/avatar-manifest/src/index.ts +29 -0
- package/node_modules/@vellumai/avatar-manifest/src/layout.ts +18 -0
- package/node_modules/@vellumai/avatar-manifest/src/manifest.ts +156 -0
- package/node_modules/@vellumai/avatar-manifest/src/read.ts +109 -0
- package/node_modules/@vellumai/avatar-manifest/tsconfig.json +20 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
- package/node_modules/@vellumai/ces-client/src/http-credentials.ts +161 -0
- package/node_modules/@vellumai/ces-client/src/index.ts +1 -0
- package/node_modules/@vellumai/environments/package.json +2 -1
- package/node_modules/@vellumai/environments/src/shell.test.ts +56 -0
- package/node_modules/@vellumai/environments/src/shell.ts +65 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +32 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +82 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/plugin-admission-denied-contract.test.ts +56 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +18 -0
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +5 -11
- package/node_modules/@vellumai/gateway-client/src/channel-permission-contract.ts +3 -1
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +9 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +4 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +75 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +17 -0
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +8 -0
- package/node_modules/@vellumai/gateway-client/src/plugin-admission-denied-contract.ts +47 -0
- package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +11 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
- package/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
- package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
- package/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
- package/openapi.yaml +419 -55
- package/package.json +4 -1
- package/scripts/sync-llm-catalog.ts +6 -0
- package/src/__tests__/approval-card-edit-inbound-handoff.test.ts +199 -0
- package/src/__tests__/approval-card-edit.test.ts +153 -0
- package/src/__tests__/attachments-store-heic-normalize.test.ts +1 -1
- package/src/__tests__/available-skills.test.ts +54 -0
- package/src/__tests__/avatar-identity-sync.test.ts +5 -1
- package/src/__tests__/catalog-install-normalize.test.ts +9 -1
- package/src/__tests__/channel-policy.test.ts +1 -3
- package/src/__tests__/channel-readiness-discord.test.ts +9 -70
- package/src/__tests__/compactor-call-site-logging.test.ts +61 -0
- package/src/__tests__/config-loader-backfill.test.ts +69 -0
- package/src/__tests__/config-schema.test.ts +2 -3
- package/src/__tests__/conversation-agent-loop.test.ts +238 -0
- package/src/__tests__/conversation-attachments.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +44 -0
- package/src/__tests__/conversation-evictor.test.ts +19 -0
- package/src/__tests__/conversation-fork-referential.test.ts +73 -63
- package/src/__tests__/conversation-fork-retrospective.test.ts +99 -74
- package/src/__tests__/conversation-runtime-assembly.test.ts +23 -0
- package/src/__tests__/conversation-slash.test.ts +9 -0
- package/src/__tests__/conversation-title-service.test.ts +97 -0
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +16 -0
- package/src/__tests__/credential-broker-server-use.test.ts +0 -218
- package/src/__tests__/credential-broker.test.ts +43 -420
- package/src/__tests__/credential-record-write-through.test.ts +78 -0
- package/src/__tests__/credential-routes.test.ts +17 -1
- package/src/__tests__/credential-security-invariants.test.ts +8 -9
- package/src/__tests__/default-profile-catalog-fallback.test.ts +289 -0
- package/src/__tests__/delete-propagation.test.ts +92 -2
- package/src/__tests__/discord-callback-round-trip.test.ts +93 -0
- package/src/__tests__/discord-channel-config.test.ts +237 -0
- package/src/__tests__/edit-propagation.test.ts +42 -2
- package/src/__tests__/evict-conversations-for-reload.test.ts +51 -1
- package/src/__tests__/fallback-breaker.test.ts +1335 -0
- package/src/__tests__/gateway-threshold-reader-mock.ts +6 -0
- package/src/__tests__/guardian-card-withdrawal.test.ts +30 -0
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -2
- package/src/__tests__/image-conversion.test.ts +38 -4
- package/src/__tests__/install-skill-routing.test.ts +25 -0
- package/src/__tests__/llm-catalog-parity.test.ts +4 -0
- package/src/__tests__/llm-resolver.test.ts +74 -0
- package/src/__tests__/llm-schema-fallback-profile.test.ts +359 -0
- package/src/__tests__/managed-fallback-dispatch.test.ts +430 -0
- package/src/__tests__/managed-profile-guard.test.ts +28 -0
- package/src/__tests__/managed-speech-defaults.test.ts +196 -1
- package/src/__tests__/mcp-cli.test.ts +23 -1
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +43 -34
- package/src/__tests__/media-stream-stt-session.test.ts +2 -0
- package/src/__tests__/notification-decision-fallback.test.ts +106 -0
- package/src/__tests__/notification-discord-adapter.test.ts +162 -0
- package/src/__tests__/oauth-connect-orchestrator.test.ts +95 -0
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +6 -8
- package/src/__tests__/persist-unsendable-image.test.ts +3 -3
- package/src/__tests__/platform.test.ts +19 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -1
- package/src/__tests__/provider-catalog-visibility.test.ts +17 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +8 -1
- package/src/__tests__/provider-usage-tracking.test.ts +56 -0
- package/src/__tests__/require-fresh-approval.test.ts +5 -2
- package/src/__tests__/retry-fallback-escalation.test.ts +1093 -0
- package/src/__tests__/schedule-retry.test.ts +183 -4
- package/src/__tests__/scheduler-reuse-conversation.test.ts +6 -6
- package/src/__tests__/secret-routes-platform-proxy.test.ts +55 -0
- package/src/__tests__/skill-load-tool.test.ts +34 -0
- package/src/__tests__/skill-tool-factory.test.ts +6 -1
- package/src/__tests__/skill-tool-manifest.test.ts +20 -0
- package/src/__tests__/skills.test.ts +25 -0
- package/src/__tests__/subagent-manager-notify.test.ts +38 -0
- package/src/__tests__/terminal-tools.test.ts +2 -0
- package/src/__tests__/tool-approval-handler.test.ts +146 -2
- package/src/__tests__/tool-approval-seed-content-blocks.test.ts +108 -2
- package/src/__tests__/tool-audit.test.ts +111 -40
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +228 -18
- package/src/__tests__/ui-shape-teaching.test.ts +1 -1
- package/src/__tests__/verification-control-plane-policy.test.ts +6 -2
- package/src/__tests__/voice-config-update.test.ts +43 -0
- package/src/__tests__/workspace-migration-147-rename-colliding-backup-profile-names.test.ts +480 -0
- package/src/__tests__/workspace-migration-148-strip-unsupported-fallback-profiles.test.ts +167 -0
- package/src/__tests__/workspace-migration-149-repoint-backup-profile-selections.test.ts +144 -0
- package/src/agent/loop.ts +31 -2
- package/src/approvals/guardian-card-withdrawal.ts +22 -0
- package/src/avatar/__tests__/ensure-raster.test.ts +291 -0
- package/src/avatar/avatar-manifest.ts +48 -100
- package/src/avatar/avatar-store.ts +7 -7
- package/src/avatar/ensure-raster.ts +146 -0
- package/src/avatar/resvg-lazy.test.ts +0 -1
- package/src/avatar/traits-png-sync.ts +9 -10
- package/src/background-wake/wake-intent-hooks.test.ts +0 -1
- package/src/backup/__tests__/paths.test.ts +6 -2
- package/src/backup/paths.ts +6 -4
- package/src/bundler/app-compiler.ts +2 -2
- package/src/bundler/compiler-tools.test.ts +36 -0
- package/src/bundler/compiler-tools.ts +54 -18
- package/src/calls/__tests__/telephony-synthesis-language.test.ts +81 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +57 -0
- package/src/calls/media-stream-stt-session.ts +2 -1
- package/src/calls/telephony-synthesis-language.ts +13 -5
- package/src/calls/voice-session-bridge.ts +19 -4
- package/src/channels/config.ts +23 -12
- package/src/cli/AGENTS.md +3 -1
- package/src/cli/commands/backup.help.ts +10 -13
- package/src/cli/commands/contacts.help.ts +16 -4
- package/src/cli/commands/contacts.ts +5 -1
- package/src/cli/commands/credentials.help.ts +1 -1
- package/src/cli/commands/inference-providers.ts +18 -53
- package/src/cli/commands/mcp.help.ts +6 -5
- package/src/cli/commands/mcp.ts +1 -1
- package/src/cli/commands/memory/memory-retrospective.ts +1 -0
- package/src/cli/commands/stt.help.ts +2 -2
- package/src/config/__tests__/backup-schema.test.ts +1 -1
- package/src/config/__tests__/default-profile-catalog.test.ts +83 -2
- package/src/config/__tests__/loader-sparse-array-cleanup.test.ts +1 -1
- package/src/config/__tests__/memory-retrospective-schema.test.ts +12 -15
- package/src/config/bundled-skills/computer-use/SKILL.md +6 -6
- package/src/config/bundled-skills/computer-use/TOOLS.json +9 -6
- package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +3 -1
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +1 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +16 -15
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +2 -1
- package/src/config/bundled-skills/settings/TOOLS.json +11 -5
- package/src/config/bundled-skills/settings/tools/open-system-settings.test.ts +85 -0
- package/src/config/bundled-skills/settings/tools/open-system-settings.ts +33 -8
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +54 -3
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +1 -1
- package/src/config/default-profile-catalog.ts +195 -10
- package/src/config/default-profile-names.ts +41 -0
- package/src/config/feature-flag-registry.json +34 -2
- package/src/config/inference-profile-validation.ts +11 -2
- package/src/config/llm-resolver.ts +25 -0
- package/src/config/loader.ts +3 -0
- package/src/config/managed-speech-defaults.ts +160 -10
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/live-voice.test.ts +7 -6
- package/src/config/schemas/__tests__/stt.test.ts +94 -0
- package/src/config/schemas/backup.ts +1 -1
- package/src/config/schemas/channels.ts +32 -0
- package/src/config/schemas/live-voice.ts +13 -4
- package/src/config/schemas/llm.ts +263 -15
- package/src/config/schemas/mcp.ts +10 -2
- package/src/config/schemas/memory-retrospective.ts +1 -19
- package/src/config/schemas/services.ts +24 -0
- package/src/config/schemas/stt.ts +128 -5
- package/src/config/seed-inference-profiles.ts +32 -3
- package/src/config/skill-state.ts +5 -0
- package/src/config/skills.ts +21 -0
- package/src/contacts/notify-contacts-changed.ts +5 -2
- package/src/context/compactor.ts +74 -1
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +138 -1
- package/src/daemon/config-watcher.ts +2 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +27 -0
- package/src/daemon/conversation-agent-loop.ts +86 -10
- package/src/daemon/conversation-error.ts +25 -0
- package/src/daemon/conversation-evictor.ts +2 -4
- package/src/daemon/conversation-runtime-assembly.ts +19 -7
- package/src/daemon/conversation-skill-tools.ts +5 -1
- package/src/daemon/conversation-slash.ts +2 -2
- package/src/daemon/conversation-store.ts +8 -1
- package/src/daemon/conversation-tool-setup.ts +37 -0
- package/src/daemon/conversation.ts +0 -4
- package/src/daemon/handlers/channel-config-result.ts +30 -0
- package/src/daemon/handlers/config-discord-channel.ts +301 -0
- package/src/daemon/handlers/config-ingress.ts +9 -0
- package/src/daemon/handlers/config-slack-channel.ts +13 -15
- package/src/daemon/handlers/config-telegram.ts +2 -5
- package/src/daemon/handlers/conversations.ts +0 -2
- package/src/daemon/handlers/skills.ts +25 -7
- package/src/daemon/lifecycle.ts +3 -1
- package/src/daemon/message-types/messages.ts +0 -2
- package/src/daemon/tool-setup-types.ts +5 -3
- package/src/daemon/trust-context-types.test.ts +33 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +58 -4
- package/src/live-voice/__tests__/live-voice-managed-speech-fallback.test.ts +8 -3
- package/src/live-voice/__tests__/live-voice-session-manager.test.ts +73 -11
- package/src/live-voice/__tests__/live-voice-session-preflight.test.ts +148 -2
- package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +64 -0
- package/src/live-voice/__tests__/live-voice-stt.test.ts +3 -0
- package/src/live-voice/__tests__/live-voice-text-turn.test.ts +391 -0
- package/src/live-voice/__tests__/protocol.test.ts +224 -0
- package/src/live-voice/live-voice-credential-preflight.ts +5 -1
- package/src/live-voice/live-voice-session-manager.ts +39 -2
- package/src/live-voice/live-voice-session.ts +224 -9
- package/src/live-voice/protocol.ts +187 -1
- package/src/messaging/providers/discord/api.ts +2 -1
- package/src/messaging/providers/discord/withdraw.ts +62 -0
- package/src/messaging/read-provider-metadata.ts +32 -0
- package/src/monitoring/__tests__/file-descriptors.test.ts +32 -0
- package/src/monitoring/__tests__/process-memory.test.ts +34 -1
- package/src/monitoring/db-integrity-sample.ts +1 -0
- package/src/monitoring/file-descriptors.ts +39 -6
- package/src/monitoring/process-memory.ts +37 -5
- package/src/monitoring/resource-sampler.ts +25 -8
- package/src/notifications/__tests__/activity-failed-dedupe.test.ts +161 -0
- package/src/notifications/__tests__/background-failure-signal.test.ts +107 -0
- package/src/notifications/__tests__/connected-channels.test.ts +12 -0
- package/src/notifications/__tests__/copy-composer.test.ts +83 -0
- package/src/notifications/__tests__/destination-resolver.test.ts +38 -0
- package/src/notifications/activity-failed-dedupe.ts +95 -0
- package/src/notifications/adapters/discord.ts +132 -0
- package/src/notifications/adapters/shared.ts +16 -0
- package/src/notifications/adapters/slack.ts +6 -1
- package/src/notifications/adapters/telegram.ts +5 -8
- package/src/notifications/approval-card-data.ts +94 -1
- package/src/notifications/background-failure-signal.ts +116 -0
- package/src/notifications/broadcaster.ts +3 -9
- package/src/notifications/copy-composer.ts +41 -8
- package/src/notifications/decision-engine.ts +55 -0
- package/src/notifications/destination-resolver.ts +45 -5
- package/src/notifications/emit-signal.ts +19 -5
- package/src/notifications/guardian-question-mode.ts +57 -0
- package/src/notifications/types.ts +2 -8
- package/src/oauth/__tests__/seed-providers-managed.test.ts +18 -0
- package/src/oauth/connect-orchestrator.ts +16 -0
- package/src/oauth/seed-providers.ts +262 -0
- package/src/permissions/checker.test.ts +21 -0
- package/src/permissions/checker.ts +6 -0
- package/src/permissions/gateway-threshold-reader.test.ts +184 -0
- package/src/permissions/gateway-threshold-reader.ts +144 -10
- package/src/permissions/prompter.ts +0 -6
- package/src/permissions/types.ts +5 -4
- package/src/persistence/__tests__/bulk-write-gate.test.ts +0 -38
- package/src/persistence/bulk-write-gate.ts +9 -10
- package/src/persistence/conversation-crud.ts +88 -134
- package/src/persistence/conversation-title-service.ts +43 -29
- package/src/persistence/embeddings/embedding-local.ts +14 -85
- package/src/persistence/embeddings/embedding-runtime-manager.ts +1 -0
- package/src/persistence/lifecycle-events-store.test.ts +50 -0
- package/src/persistence/lifecycle-events-store.ts +75 -5
- package/src/platform/platform-patch-queue.test.ts +368 -0
- package/src/platform/platform-patch-queue.ts +190 -0
- package/src/platform/sync-avatar.test.ts +527 -0
- package/src/platform/sync-avatar.ts +209 -0
- package/src/platform/sync-identity.test.ts +75 -0
- package/src/platform/sync-identity.ts +17 -110
- package/src/plugin-api/model-profiles.test.ts +2 -2
- package/src/plugin-api/model-profiles.ts +7 -7
- package/src/plugin-api/transcription-session.ts +1 -1
- package/src/plugins/defaults/compaction/window-manager.ts +4 -0
- package/src/plugins/defaults/image-recovery/recover.ts +2 -2
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +133 -34
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +21 -31
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +13 -9
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +31 -4
- package/src/plugins/defaults/memory/memory-retrospective-constants.ts +0 -11
- package/src/plugins/defaults/memory/memory-retrospective-fork-boundary.ts +27 -13
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +183 -113
- package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +13 -13
- package/src/plugins/defaults/memory/src/memory-v2-routes.ts +2 -2
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +1 -1
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +4 -2
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +9 -42
- package/src/plugins/defaults/memory/v2/rerank-local.ts +1 -0
- package/src/plugins/defaults/memory/worker.ts +7 -0
- package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
- package/src/providers/__tests__/retry-callsite.test.ts +55 -0
- package/src/providers/__tests__/vellum-connection-routing.test.ts +21 -3
- package/src/providers/connection-resolution.ts +2 -17
- package/src/providers/fallback-breaker.ts +389 -0
- package/src/providers/inference/__tests__/adapter-factory-opencode.test.ts +92 -0
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +16 -0
- package/src/providers/inference/__tests__/base-url-security.test.ts +12 -0
- package/src/providers/inference/adapter-factory.ts +246 -3
- package/src/providers/inference/auth.ts +15 -14
- package/src/providers/inference/chatgpt-subscription-auth.ts +70 -0
- package/src/providers/inference/chatgpt-subscription-credentials.ts +84 -0
- package/src/providers/inference/resolve-auth.ts +1 -1
- package/src/providers/media-resolve.ts +2 -3
- package/src/providers/model-catalog.ts +100 -0
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +22 -0
- package/src/providers/openai/__tests__/chat-template-flatten.test.ts +198 -0
- package/src/providers/openai/api-error-normalization.ts +13 -0
- package/src/providers/openai/chat-completions-provider.ts +84 -0
- package/src/providers/opencode/client.test.ts +150 -0
- package/src/providers/opencode/client.ts +61 -0
- package/src/providers/platform-proxy/constants.ts +5 -0
- package/src/providers/provider-resolvability.ts +20 -0
- package/src/providers/provider-secret-catalog.ts +3 -2
- package/src/providers/retry.ts +956 -68
- package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +42 -0
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +136 -0
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +120 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +316 -12
- package/src/providers/speech-to-text/deepgram-flux-frames.ts +83 -3
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +132 -4
- package/src/providers/speech-to-text/provider-catalog.ts +191 -3
- package/src/providers/speech-to-text/resolve.ts +132 -24
- package/src/providers/speech-to-text/vellum-managed-flux-realtime.ts +296 -0
- package/src/providers/types.ts +39 -0
- package/src/providers/usage-tracking.ts +12 -1
- package/src/providers/vellum/client.ts +29 -0
- package/src/providers/vellum-model-routing.test.ts +2 -0
- package/src/providers/vellum-model-routing.ts +3 -3
- package/src/runtime/AGENTS.md +1 -2
- package/src/runtime/__tests__/agent-wake.test.ts +105 -3
- package/src/runtime/__tests__/background-job-runner.test.ts +89 -3
- package/src/runtime/agent-wake.ts +44 -5
- package/src/runtime/background-job-runner.ts +68 -50
- package/src/runtime/channel-readiness-service.ts +0 -39
- package/src/runtime/http-server.ts +12 -4
- package/src/runtime/routes/__tests__/avatar-state-routes.test.ts +18 -18
- package/src/runtime/routes/__tests__/backup-routes.test.ts +9 -5
- package/src/runtime/routes/__tests__/chatgpt-subscription-auth-routes.test.ts +372 -0
- package/src/runtime/routes/__tests__/contact-routes.test.ts +7 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +96 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +37 -0
- package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +2 -1
- package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +87 -0
- package/src/runtime/routes/acp-claude-auth-routes.ts +25 -54
- package/src/runtime/routes/approval-routes.ts +0 -12
- package/src/runtime/routes/avatar-routes.ts +17 -30
- package/src/runtime/routes/backup-routes.ts +2 -2
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +296 -113
- package/src/runtime/routes/contact-prompt-routes.ts +65 -6
- package/src/runtime/routes/contact-routes.ts +20 -15
- package/src/runtime/routes/conversation-query-routes.ts +98 -10
- package/src/runtime/routes/conversation-routes.ts +2 -2
- package/src/runtime/routes/credential-routes.ts +2 -1
- package/src/runtime/routes/guardian-approval-interception.ts +12 -9
- package/src/runtime/routes/inbound-message-handler.ts +47 -48
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +18 -13
- package/src/runtime/routes/inbound-stages/admission-policy.ts +2 -2
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +20 -2
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +9 -8
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -1
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-profile-session-handler.ts +2 -2
- package/src/runtime/routes/inference-profiles-routes.ts +12 -7
- package/src/runtime/routes/inference-provider-connection-routes.ts +2 -2
- package/src/runtime/routes/inference-send-routes.ts +2 -2
- package/src/runtime/routes/ingress-status-routes.ts +2 -2
- package/src/runtime/routes/integrations/discord.ts +119 -0
- package/src/runtime/routes/llm-call-sites-routes.ts +2 -2
- package/src/runtime/routes/mcp-auth-routes.ts +12 -6
- package/src/runtime/routes/migration-routes.ts +3 -1
- package/src/runtime/routes/oauth-pending-flows.ts +136 -0
- package/src/runtime/routes/secret-routes.ts +7 -0
- package/src/runtime/routes/stt-routes.ts +38 -6
- package/src/runtime/routes/wake-conversation-routes.ts +8 -1
- package/src/runtime/routes/watch-routes.ts +9 -5
- package/src/runtime/sync/resource-sync-events.ts +2 -0
- package/src/runtime/trust-verdict-consumer.ts +3 -3
- package/src/schedule/__tests__/worker-feature-flags.test.ts +58 -0
- package/src/schedule/retry-policy.ts +8 -8
- package/src/schedule/run-script.ts +14 -23
- package/src/schedule/schedule-recovery.ts +0 -1
- package/src/schedule/scheduler.ts +89 -51
- package/src/schedule/worker.ts +57 -0
- package/src/security/__tests__/openai-device-auth.test.ts +355 -0
- package/src/security/ces-rpc-record-backend.ts +123 -0
- package/src/security/oauth2.ts +7 -0
- package/src/security/openai-device-auth.ts +364 -0
- package/src/security/secure-keys.ts +15 -0
- package/src/skills/available-skills.ts +10 -1
- package/src/skills/catalog-install.ts +19 -1
- package/src/skills/inline-command-runner.ts +6 -2
- package/src/skills/platform-compatibility.test.ts +49 -0
- package/src/skills/platform-compatibility.ts +65 -0
- package/src/skills/tool-manifest.ts +20 -0
- package/src/stt/__tests__/roles.test.ts +152 -0
- package/src/stt/daemon-batch-transcriber.ts +4 -1
- package/src/stt/roles.ts +180 -0
- package/src/stt/stt-stream-session.ts +3 -2
- package/src/stt/types.ts +33 -3
- package/src/subagent/manager.ts +12 -0
- package/src/telemetry/__tests__/live-voice-funnel.test.ts +34 -0
- package/src/telemetry/live-voice-funnel.ts +12 -2
- package/src/telemetry/telemetry-wire.generated.ts +6 -0
- package/src/telemetry/tool-audit.ts +83 -30
- package/src/telemetry/tool-executed-events-store.test.ts +0 -1
- package/src/telemetry/turn-outcome.ts +75 -4
- package/src/tools/client-os.ts +115 -0
- package/src/tools/computer-use/definitions.ts +11 -7
- package/src/tools/computer-use/skill-proxy-bridge.ts +2 -2
- package/src/tools/credentials/broker-types.ts +0 -71
- package/src/tools/credentials/broker.ts +6 -201
- package/src/tools/credentials/metadata-store.ts +65 -16
- package/src/tools/credentials/store.ts +2 -0
- package/src/tools/executor.ts +2 -8
- package/src/tools/host-shell.test.ts +75 -0
- package/src/tools/host-shell.ts +24 -0
- package/src/tools/host-terminal/host-shell.ts +15 -44
- package/src/tools/mcp/mcp-tool-factory.ts +44 -9
- package/src/tools/permission-checker.ts +73 -21
- package/src/tools/policy-context.ts +1 -0
- package/src/tools/shared/filesystem/image-read.ts +1 -1
- package/src/tools/skills/find-similar-skills.ts +10 -2
- package/src/tools/skills/load.ts +41 -6
- package/src/tools/skills/sandbox-runner.ts +8 -16
- package/src/tools/skills/skill-tool-factory.ts +1 -0
- package/src/tools/terminal/__tests__/safe-env.test.ts +64 -1
- package/src/tools/terminal/safe-env.ts +39 -9
- package/src/tools/terminal/shell.ts +12 -31
- package/src/tools/tool-approval-handler.ts +64 -10
- package/src/tools/tool-types.ts +10 -0
- package/src/tools/types.ts +19 -3
- package/src/tools/ui-surface/surface-shape-docs.ts +4 -3
- package/src/tools/workflows/manage-workflows.ts +2 -2
- package/src/tools/workflows/run-workflow.test.ts +2 -1
- package/src/util/__tests__/file-use.test.ts +61 -0
- package/src/util/__tests__/process-table.test.ts +66 -0
- package/src/util/__tests__/process-tree.test.ts +46 -0
- package/src/util/errors.ts +1 -0
- package/src/util/file-use.ts +91 -0
- package/src/util/host-process.test.ts +17 -0
- package/src/util/host-process.ts +75 -0
- package/src/util/image-conversion.ts +39 -7
- package/src/util/platform.ts +32 -13
- package/src/util/process-table.ts +249 -0
- package/src/util/process-tree.ts +85 -129
- package/src/util/provider-error-patterns.ts +20 -0
- package/src/util/spawn.ts +9 -2
- package/src/watch/__tests__/watch-retro.test.ts +48 -1
- package/src/watch/watch-retro.ts +46 -4
- package/src/workspace/git-service.ts +4 -14
- package/src/workspace/migrations/147-rename-colliding-backup-profile-names.ts +360 -0
- package/src/workspace/migrations/148-strip-unsupported-fallback-profiles.ts +117 -0
- package/src/workspace/migrations/149-repoint-backup-profile-selections.ts +204 -0
- package/src/workspace/migrations/150-stt-flux-provider-to-model-family.ts +139 -0
- package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +160 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/__tests__/permission-types.test.ts +0 -17
- package/src/channels/gateway-discord-admission.ts +0 -32
- package/src/persistence/fork-message-copy.ts +0 -239
- package/src/plugins/defaults/memory/__tests__/fork-message-copy.test.ts +0 -221
- package/src/security/__tests__/oauth2-device-code.test.ts +0 -431
- package/src/security/oauth2-device-code.ts +0 -312
|
@@ -37,7 +37,7 @@ import { z } from "zod";
|
|
|
37
37
|
import { usesConceptPageMemory } from "../../../../config/memory-v3-gate.js";
|
|
38
38
|
import type { AssistantConfig } from "../../../../config/types.js";
|
|
39
39
|
import { warmGuardianBindings } from "../../../../contacts/guardian-delivery-reader.js";
|
|
40
|
-
import {
|
|
40
|
+
import { emitBackgroundFailureSignal } from "../../../../notifications/background-failure-signal.js";
|
|
41
41
|
import { getDb } from "../../../../persistence/db-connection.js";
|
|
42
42
|
import type { MemoryJob } from "../../../../persistence/jobs-store.js";
|
|
43
43
|
import {
|
|
@@ -199,53 +199,20 @@ export async function memoryV2SweepJob(
|
|
|
199
199
|
} catch (err) {
|
|
200
200
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
201
201
|
log.error({ err }, "memory v2 sweep failed");
|
|
202
|
-
|
|
203
|
-
|
|
202
|
+
// The sweep is a data job with no LLM turn to classify, so its failures
|
|
203
|
+
// report as generic exceptions through the shared background-failure
|
|
204
|
+
// seam, which owns the payload shape and dedupe key.
|
|
205
|
+
emitBackgroundFailureSignal({
|
|
206
|
+
jobName: JOB_NAME,
|
|
207
|
+
sourceChannel: "scheduler",
|
|
208
|
+
sourceContextId: job.id,
|
|
209
|
+
errorKind: "exception",
|
|
204
210
|
errorMessage,
|
|
205
211
|
});
|
|
206
212
|
return 0;
|
|
207
213
|
}
|
|
208
214
|
}
|
|
209
215
|
|
|
210
|
-
/**
|
|
211
|
-
* Emit an `activity.failed` notification for a failed sweep run. Mirrors
|
|
212
|
-
* the shape `runBackgroundJob` produces for its own failures so the home
|
|
213
|
-
* feed and native notifications stay consistent regardless of which code
|
|
214
|
-
* path executed the work. Fire-and-forget — a notification failure must
|
|
215
|
-
* never break sweep operation.
|
|
216
|
-
*/
|
|
217
|
-
function emitSweepActivityFailed(args: {
|
|
218
|
-
jobId: string;
|
|
219
|
-
errorMessage: string;
|
|
220
|
-
}): void {
|
|
221
|
-
const day = new Date().toISOString().slice(0, 10);
|
|
222
|
-
emitNotificationSignal({
|
|
223
|
-
sourceChannel: "scheduler",
|
|
224
|
-
sourceContextId: args.jobId,
|
|
225
|
-
sourceEventName: "activity.failed",
|
|
226
|
-
dedupeKey: `activity-failed:${JOB_NAME}:${day}`,
|
|
227
|
-
contextPayload: {
|
|
228
|
-
jobName: JOB_NAME,
|
|
229
|
-
errorMessage: args.errorMessage,
|
|
230
|
-
errorKind: "exception",
|
|
231
|
-
},
|
|
232
|
-
attentionHints: {
|
|
233
|
-
requiresAction: false,
|
|
234
|
-
urgency: "medium",
|
|
235
|
-
isAsyncBackground: true,
|
|
236
|
-
visibleInSourceNow: false,
|
|
237
|
-
},
|
|
238
|
-
}).catch((emitErr) => {
|
|
239
|
-
log.warn(
|
|
240
|
-
{
|
|
241
|
-
err: emitErr instanceof Error ? emitErr.message : String(emitErr),
|
|
242
|
-
jobId: args.jobId,
|
|
243
|
-
},
|
|
244
|
-
"Failed to emit activity.failed notification for memory v2 sweep",
|
|
245
|
-
);
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
|
|
249
216
|
/**
|
|
250
217
|
* Append each non-empty entry to `memory/buffer.md` + today's archive,
|
|
251
218
|
* using the same format `remember()` produces. Returns the count of entries
|
|
@@ -15,6 +15,7 @@ import { writeFileSync } from "node:fs";
|
|
|
15
15
|
import { getConfig } from "../../../config/loader.js";
|
|
16
16
|
import { isMemoryEnabled } from "../../../config/memory-v3-gate.js";
|
|
17
17
|
import { rehydratePlatformCredentials } from "../../../config/platform-rehydration.js";
|
|
18
|
+
import { startConversationEvictor } from "../../../daemon/conversation-evictor.js";
|
|
18
19
|
import { resetDb } from "../../../persistence/db-connection.js";
|
|
19
20
|
import {
|
|
20
21
|
EMBEDDING_SHUTDOWN_BUDGET_MS,
|
|
@@ -84,6 +85,12 @@ async function main(): Promise<void> {
|
|
|
84
85
|
);
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
// Sweep idle conversations out of the in-memory pool. The daemon starts
|
|
89
|
+
// this at startup; this process hosts conversations too (retrospective and
|
|
90
|
+
// consolidation passes), so without it every pass's conversation is
|
|
91
|
+
// retained for the process lifetime.
|
|
92
|
+
startConversationEvictor();
|
|
93
|
+
|
|
87
94
|
let worker: ReturnType<typeof startMemoryJobsWorkerLoop> | null = null;
|
|
88
95
|
let keepAlive: ReturnType<typeof setInterval> | null = null;
|
|
89
96
|
let disposePidGuard: (() => void) | null = null;
|
|
@@ -38,6 +38,7 @@ describe("API_KEY_PROVIDERS", () => {
|
|
|
38
38
|
expect(API_KEY_PROVIDERS).toContain("anthropic");
|
|
39
39
|
expect(API_KEY_PROVIDERS).toContain("openai");
|
|
40
40
|
expect(API_KEY_PROVIDERS).toContain("gemini");
|
|
41
|
+
expect(API_KEY_PROVIDERS).not.toContain("vellum");
|
|
41
42
|
});
|
|
42
43
|
|
|
43
44
|
test("includes search providers", () => {
|
|
@@ -120,6 +120,61 @@ describe("RetryProvider — callSite resolution", () => {
|
|
|
120
120
|
expect(config.modelIntent).toBeUndefined();
|
|
121
121
|
});
|
|
122
122
|
|
|
123
|
+
test("stamps OpenCode support headers and strips conversationId", async () => {
|
|
124
|
+
setLlmConfig({
|
|
125
|
+
callSites: {
|
|
126
|
+
memoryRetrieval: {
|
|
127
|
+
provider: "opencode",
|
|
128
|
+
model: "mimo-v2.5-free",
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
let seen: SendMessageOptions | undefined;
|
|
134
|
+
const wrapped = new RetryProvider(
|
|
135
|
+
makeProvider("opencode", (options) => {
|
|
136
|
+
seen = options;
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
await wrapped.sendMessage(DUMMY_MESSAGES, {
|
|
141
|
+
config: { callSite: "memoryRetrieval", conversationId: "conv-xyz" },
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const config = seen?.config as Record<string, unknown>;
|
|
145
|
+
const headers = config.requestHeaders as Record<string, string>;
|
|
146
|
+
expect(config.conversationId).toBeUndefined();
|
|
147
|
+
expect(headers["x-opencode-session"]).toBe("conv-xyz");
|
|
148
|
+
expect(headers["x-opencode-request"]).toBeDefined();
|
|
149
|
+
expect(headers).not.toHaveProperty("session_id");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("does not stamp OpenCode headers on openai-compatible", async () => {
|
|
153
|
+
setLlmConfig({
|
|
154
|
+
callSites: {
|
|
155
|
+
memoryRetrieval: {
|
|
156
|
+
provider: "openai-compatible",
|
|
157
|
+
model: "local-model",
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
let seen: SendMessageOptions | undefined;
|
|
163
|
+
const wrapped = new RetryProvider(
|
|
164
|
+
makeProvider("openai-compatible", (options) => {
|
|
165
|
+
seen = options;
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
await wrapped.sendMessage(DUMMY_MESSAGES, {
|
|
170
|
+
config: { callSite: "memoryRetrieval", conversationId: "conv-xyz" },
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const config = seen?.config as Record<string, unknown>;
|
|
174
|
+
expect(config.requestHeaders).toBeUndefined();
|
|
175
|
+
expect(config.conversationId).toBeUndefined();
|
|
176
|
+
});
|
|
177
|
+
|
|
123
178
|
test("strips conversationId before delegating to the inner provider", async () => {
|
|
124
179
|
setLlmConfig({
|
|
125
180
|
callSites: {
|
|
@@ -52,9 +52,10 @@ describe("vellum connection routing", () => {
|
|
|
52
52
|
expect(isVellumManagedConnection({ provider: "vellum" })).toBe(true);
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
test("the vellum sentinel
|
|
56
|
-
// No `provider` override →
|
|
57
|
-
//
|
|
55
|
+
test("the vellum sentinel does not dispatch without an override", () => {
|
|
56
|
+
// No `provider` override → the connection column is the routing
|
|
57
|
+
// sentinel and must not build an adapter, even though Vellum-hosted
|
|
58
|
+
// GPU models share this catalog id.
|
|
58
59
|
const adapter = createAdapterFromConnection(
|
|
59
60
|
vellumConnection,
|
|
60
61
|
resolvedAuth,
|
|
@@ -76,6 +77,23 @@ describe("vellum connection routing", () => {
|
|
|
76
77
|
);
|
|
77
78
|
expect(adapter).not.toBeNull();
|
|
78
79
|
});
|
|
80
|
+
|
|
81
|
+
test("provider override vellum routes GPU models through VellumProvider", () => {
|
|
82
|
+
const adapter = createAdapterFromConnection(
|
|
83
|
+
vellumConnection,
|
|
84
|
+
{
|
|
85
|
+
kind: "header",
|
|
86
|
+
headers: { Authorization: "Bearer test-key" },
|
|
87
|
+
baseUrl: "https://platform.example/v1/runtime-proxy/vellum",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
model: "qwen/qwen3-8b",
|
|
91
|
+
provider: "vellum",
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
expect(adapter).not.toBeNull();
|
|
95
|
+
expect(adapter?.name).toBe("vellum");
|
|
96
|
+
});
|
|
79
97
|
});
|
|
80
98
|
|
|
81
99
|
describe("effectiveConnectionAuth", () => {
|
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
} from "./inference/connections.js";
|
|
53
53
|
import { resolveManagedProxyContext } from "./platform-proxy/context.js";
|
|
54
54
|
import { checkCredentialPresence } from "./provider-availability.js";
|
|
55
|
+
import { dispatchProviderResolvable } from "./provider-resolvability.js";
|
|
55
56
|
import type { ProvidersConfig } from "./registry.js";
|
|
56
57
|
import { resolveProviderFromConnection } from "./registry.js";
|
|
57
58
|
import {
|
|
@@ -67,6 +68,7 @@ import {
|
|
|
67
68
|
} from "./vellum-model-routing.js";
|
|
68
69
|
|
|
69
70
|
export { ConnectionResolutionError, resolveRoutingIdentity };
|
|
71
|
+
export { dispatchProviderResolvable } from "./provider-resolvability.js";
|
|
70
72
|
|
|
71
73
|
const log = getLogger("providers/connection-resolution");
|
|
72
74
|
|
|
@@ -118,23 +120,6 @@ export function writableProfileProviderIssue(provider: string): string | null {
|
|
|
118
120
|
return `Invalid provider "${provider}". Use a known provider or the name of an existing connection.`;
|
|
119
121
|
}
|
|
120
122
|
|
|
121
|
-
/**
|
|
122
|
-
* Selection-time predicate for `ResolveCallSiteOpts.isResolvableProvider`:
|
|
123
|
-
* a provider value dispatches when it is a known vendor/identity or names a
|
|
124
|
-
* connection entry row. Permissive on DB unavailability so a transient blip
|
|
125
|
-
* never heals away a valid entry profile; dispatch soft-fails on its own.
|
|
126
|
-
*/
|
|
127
|
-
export function dispatchProviderResolvable(provider: string): boolean {
|
|
128
|
-
if (VALID_CONNECTION_PROVIDERS.includes(provider)) {
|
|
129
|
-
return true;
|
|
130
|
-
}
|
|
131
|
-
try {
|
|
132
|
-
return getConnection(getDb(), provider) != null;
|
|
133
|
-
} catch {
|
|
134
|
-
return true;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
123
|
/**
|
|
139
124
|
* The vendor a resolved provider value expects its connection row to serve:
|
|
140
125
|
* catalog ids and routing identities pass through, and an entry-name label
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Circuit breaker for managed fallback routes.
|
|
3
|
+
*
|
|
4
|
+
* Once a backup profile has served a request, the primary route is known to be
|
|
5
|
+
* down. Paying the full retry budget on every later request only adds latency
|
|
6
|
+
* to an answer the backup was always going to give, so the breaker remembers
|
|
7
|
+
* the outage and sends the next requests straight to the backup. Recovery is
|
|
8
|
+
* request-driven: after a cooldown, exactly one request probes the primary. A
|
|
9
|
+
* successful probe closes the breaker; a failed one re-trips it with a longer
|
|
10
|
+
* cooldown.
|
|
11
|
+
*
|
|
12
|
+
* Scope
|
|
13
|
+
* -----
|
|
14
|
+
* A remembered outage is keyed by the route it was actually observed on, and
|
|
15
|
+
* the two failure shapes this feature exists for indict different things:
|
|
16
|
+
*
|
|
17
|
+
* - An upstream outage (5xx, 429, transport failure, a broken managed
|
|
18
|
+
* credential) indicts the whole provider, so it is remembered under the
|
|
19
|
+
* upstream name and every model on it skips the primary.
|
|
20
|
+
* - A retired or renamed model (404, `model_not_found`, the managed proxy's
|
|
21
|
+
* preflight 400) indicts one model. Remembering it provider-wide would
|
|
22
|
+
* divert healthy profiles on the same upstream to their backups for the
|
|
23
|
+
* whole cooldown, and would let a probe of some other healthy model close a
|
|
24
|
+
* breaker the retired model still deserves. It is remembered under the
|
|
25
|
+
* upstream plus that model instead.
|
|
26
|
+
*
|
|
27
|
+
* Reads always name the full route, so a request is skipped when either its
|
|
28
|
+
* upstream or its exact model is remembered as down.
|
|
29
|
+
*
|
|
30
|
+
* The cooldown carries plus or minus 20 percent of jitter so a fleet of daemons
|
|
31
|
+
* coming out of the same provider outage does not probe a recovering upstream
|
|
32
|
+
* in the same second.
|
|
33
|
+
*
|
|
34
|
+
* State is intentionally process-local and never persisted: the daemon is a
|
|
35
|
+
* single process, and a restart resets every route to its primary. The worst
|
|
36
|
+
* case for that choice is one request rediscovering an ongoing outage the
|
|
37
|
+
* expensive way, which is exactly what the very first request of an outage
|
|
38
|
+
* already pays.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { getLogger } from "../util/logger.js";
|
|
42
|
+
|
|
43
|
+
const log = getLogger("fallback-breaker");
|
|
44
|
+
|
|
45
|
+
/** Eligible failures inside this window count toward the trip threshold. */
|
|
46
|
+
const FAILURE_WINDOW_MS = 5 * 60_000;
|
|
47
|
+
|
|
48
|
+
/** Eligible failures needed to trip when no backup has served yet. */
|
|
49
|
+
const FAILURES_TO_TRIP = 2;
|
|
50
|
+
|
|
51
|
+
/** Cooldown before the first recovery probe is admitted. */
|
|
52
|
+
const BASE_COOLDOWN_MS = 120_000;
|
|
53
|
+
|
|
54
|
+
/** Ceiling for the doubling cooldown, so a long outage still gets probed. */
|
|
55
|
+
const MAX_COOLDOWN_MS = 10 * 60_000;
|
|
56
|
+
|
|
57
|
+
/** Fraction of the cooldown spent on desynchronizing jitter. */
|
|
58
|
+
const COOLDOWN_JITTER = 0.2;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A route the breaker can remember. `model` narrows the entry to a single
|
|
62
|
+
* model: writes that omit it record an upstream-wide outage, and reads that
|
|
63
|
+
* include it consult both the upstream entry and the model's own.
|
|
64
|
+
*/
|
|
65
|
+
export interface BreakerRoute {
|
|
66
|
+
upstream: string;
|
|
67
|
+
model?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Identifies the breaker state that observed a primary failure. A fallback
|
|
72
|
+
* completion may arrive after a recovery probe, so it must only update the
|
|
73
|
+
* state that was current when the primary failed.
|
|
74
|
+
*/
|
|
75
|
+
export interface BreakerObservation {
|
|
76
|
+
key: string;
|
|
77
|
+
generation: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface BreakerState {
|
|
81
|
+
/** Timestamps of recent eligible failures, pruned to the failure window. */
|
|
82
|
+
failures: number[];
|
|
83
|
+
/** When the current cooldown expires, or null while the breaker is closed. */
|
|
84
|
+
openUntil: number | null;
|
|
85
|
+
/** Consecutive failed recovery probes; each one doubles the cooldown. */
|
|
86
|
+
failedProbes: number;
|
|
87
|
+
/** Whether a probe is currently deciding recovery for this entry. */
|
|
88
|
+
probeInFlight: boolean;
|
|
89
|
+
/** Monotonic identity for this state generation. */
|
|
90
|
+
generation: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const breakers = new Map<string, BreakerState>();
|
|
94
|
+
let nextGeneration = 0;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The single entry a write targets. A model-scoped key can never collide with
|
|
98
|
+
* an upstream name: the separator is a space, which no provider id contains.
|
|
99
|
+
*/
|
|
100
|
+
function keyOf(route: BreakerRoute): string {
|
|
101
|
+
return route.model === undefined
|
|
102
|
+
? route.upstream
|
|
103
|
+
: `${route.upstream} ${route.model}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Every entry that governs this route, upstream-wide first. */
|
|
107
|
+
function coveringKeys(route: BreakerRoute): string[] {
|
|
108
|
+
return route.model === undefined
|
|
109
|
+
? [route.upstream]
|
|
110
|
+
: [route.upstream, keyOf(route)];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function stateFor(key: string): BreakerState {
|
|
114
|
+
const existing = breakers.get(key);
|
|
115
|
+
if (existing !== undefined) {
|
|
116
|
+
return existing;
|
|
117
|
+
}
|
|
118
|
+
const created: BreakerState = {
|
|
119
|
+
failures: [],
|
|
120
|
+
openUntil: null,
|
|
121
|
+
failedProbes: 0,
|
|
122
|
+
probeInFlight: false,
|
|
123
|
+
generation: ++nextGeneration,
|
|
124
|
+
};
|
|
125
|
+
breakers.set(key, created);
|
|
126
|
+
return created;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Cooldown for the next open period. Doubles per consecutive failed probe and
|
|
131
|
+
* is capped at {@link MAX_COOLDOWN_MS} both before and after jitter, so the
|
|
132
|
+
* ceiling holds for the value actually used.
|
|
133
|
+
*/
|
|
134
|
+
function cooldownMs(failedProbes: number): number {
|
|
135
|
+
const base = Math.min(BASE_COOLDOWN_MS * 2 ** failedProbes, MAX_COOLDOWN_MS);
|
|
136
|
+
const jittered = base * (1 + (Math.random() * 2 - 1) * COOLDOWN_JITTER);
|
|
137
|
+
return Math.min(Math.round(jittered), MAX_COOLDOWN_MS);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function trip(
|
|
141
|
+
key: string,
|
|
142
|
+
state: BreakerState,
|
|
143
|
+
now: number,
|
|
144
|
+
reason: string,
|
|
145
|
+
): void {
|
|
146
|
+
const cooldown = cooldownMs(state.failedProbes);
|
|
147
|
+
state.openUntil = now + cooldown;
|
|
148
|
+
state.failures = [];
|
|
149
|
+
log.info(
|
|
150
|
+
{
|
|
151
|
+
route: key,
|
|
152
|
+
cooldownMs: cooldown,
|
|
153
|
+
failedProbes: state.failedProbes,
|
|
154
|
+
reason,
|
|
155
|
+
},
|
|
156
|
+
"Fallback breaker tripped; routing to the backup profile until the cooldown expires",
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Whether the entry is open, whatever the cooldown says. */
|
|
161
|
+
function isOpen(
|
|
162
|
+
state: BreakerState | undefined,
|
|
163
|
+
): state is BreakerState & { openUntil: number } {
|
|
164
|
+
return state !== undefined && state.openUntil !== null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Open entries governing this route, paired with their keys. */
|
|
168
|
+
function openCovering(
|
|
169
|
+
route: BreakerRoute,
|
|
170
|
+
): { key: string; state: BreakerState & { openUntil: number } }[] {
|
|
171
|
+
const open: { key: string; state: BreakerState & { openUntil: number } }[] =
|
|
172
|
+
[];
|
|
173
|
+
for (const key of coveringKeys(route)) {
|
|
174
|
+
const state = breakers.get(key);
|
|
175
|
+
if (isOpen(state)) {
|
|
176
|
+
open.push({ key, state });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return open;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Record an outage-shaped failure of the primary route. Two of them inside
|
|
184
|
+
* {@link FAILURE_WINDOW_MS} trip the breaker; a single blip does not. A failure
|
|
185
|
+
* recorded while the entry is already open changes nothing, since only a
|
|
186
|
+
* recovery probe decides when to close it.
|
|
187
|
+
*
|
|
188
|
+
* The caller decides the scope by what it names: pass the upstream alone for
|
|
189
|
+
* an outage, upstream plus model for a failure that indicts one model.
|
|
190
|
+
*/
|
|
191
|
+
export function recordPrimaryFailure(
|
|
192
|
+
route: BreakerRoute,
|
|
193
|
+
now: number = Date.now(),
|
|
194
|
+
): BreakerObservation {
|
|
195
|
+
const key = keyOf(route);
|
|
196
|
+
const state = stateFor(key);
|
|
197
|
+
if (isOpen(state)) {
|
|
198
|
+
return { key, generation: state.generation };
|
|
199
|
+
}
|
|
200
|
+
state.failures = state.failures.filter((at) => now - at < FAILURE_WINDOW_MS);
|
|
201
|
+
state.failures.push(now);
|
|
202
|
+
if (state.failures.length >= FAILURES_TO_TRIP) {
|
|
203
|
+
trip(key, state, now, "eligible_failures");
|
|
204
|
+
}
|
|
205
|
+
return { key, generation: state.generation };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Record that a backup profile actually served a request the primary could
|
|
210
|
+
* not. This trips the breaker immediately: unlike a bare failure, a completed
|
|
211
|
+
* backup serve proves both that the primary is down and that the backup can
|
|
212
|
+
* carry the traffic, so there is nothing left to learn from the next request
|
|
213
|
+
* repeating the same discovery. Scoped by what the caller names, as above.
|
|
214
|
+
*/
|
|
215
|
+
export function recordFallbackServed(
|
|
216
|
+
route: BreakerRoute,
|
|
217
|
+
now: number = Date.now(),
|
|
218
|
+
observation?: BreakerObservation,
|
|
219
|
+
): void {
|
|
220
|
+
const key = keyOf(route);
|
|
221
|
+
const state = observation === undefined ? stateFor(key) : breakers.get(key);
|
|
222
|
+
if (
|
|
223
|
+
observation !== undefined &&
|
|
224
|
+
(state === undefined ||
|
|
225
|
+
observation.key !== key ||
|
|
226
|
+
observation.generation !== state.generation)
|
|
227
|
+
) {
|
|
228
|
+
log.info(
|
|
229
|
+
{ route: key, observation, currentGeneration: state?.generation },
|
|
230
|
+
"Ignoring a stale fallback serve for the fallback breaker",
|
|
231
|
+
);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (state === undefined || isOpen(state)) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
trip(key, state, now, "fallback_served");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Record a successful primary send, clearing any failure history for the route
|
|
242
|
+
* that served it. Both the upstream entry and this model's own entry are
|
|
243
|
+
* cleared: the upstream answered, and it answered for this model. Another
|
|
244
|
+
* model's remembered outage on the same upstream is left alone.
|
|
245
|
+
*/
|
|
246
|
+
export function recordPrimarySuccess(
|
|
247
|
+
route: BreakerRoute,
|
|
248
|
+
now: number = Date.now(),
|
|
249
|
+
): void {
|
|
250
|
+
for (const key of coveringKeys(route)) {
|
|
251
|
+
const state = breakers.get(key);
|
|
252
|
+
if (state === undefined) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (isOpen(state)) {
|
|
256
|
+
log.info(
|
|
257
|
+
{ route: key, openForMs: Math.max(0, state.openUntil - now) },
|
|
258
|
+
"Primary route served a request; closing the fallback breaker",
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
breakers.delete(key);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Whether this request should skip the primary route and go straight to the
|
|
267
|
+
* backup. True while any entry governing the route is inside its cooldown, and
|
|
268
|
+
* also while a probe is in flight, so concurrent requests keep using the backup
|
|
269
|
+
* instead of piling onto a route a single probe is still testing.
|
|
270
|
+
*/
|
|
271
|
+
export function shouldSkipPrimary(
|
|
272
|
+
route: BreakerRoute,
|
|
273
|
+
now: number = Date.now(),
|
|
274
|
+
): boolean {
|
|
275
|
+
return openCovering(route).some(
|
|
276
|
+
({ state }) => state.probeInFlight || now < state.openUntil,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Claim the single recovery probe for a route whose remembered outages have all
|
|
282
|
+
* cooled down. Returns false for every other caller, including concurrent ones,
|
|
283
|
+
* which keep skipping the primary via {@link shouldSkipPrimary}.
|
|
284
|
+
*/
|
|
285
|
+
export function tryAcquireRecoveryProbe(
|
|
286
|
+
route: BreakerRoute,
|
|
287
|
+
now: number = Date.now(),
|
|
288
|
+
): boolean {
|
|
289
|
+
const open = openCovering(route);
|
|
290
|
+
if (open.length === 0) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
if (open.some(({ state }) => state.probeInFlight || now < state.openUntil)) {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
for (const { state } of open) {
|
|
297
|
+
state.probeInFlight = true;
|
|
298
|
+
}
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* What a recovery probe learned about the route it tested.
|
|
304
|
+
*
|
|
305
|
+
* - `recovered`: the primary served the probe.
|
|
306
|
+
* - `failing`: it did not, and `failedRoute` names what THIS failure indicts,
|
|
307
|
+
* scoped the same way an initial trip is (the whole upstream for an outage,
|
|
308
|
+
* one model for a retirement). Null when the failure names nothing the
|
|
309
|
+
* breaker can remember, in which case the probed entries simply close.
|
|
310
|
+
* - `abandoned`: the probe never reached a verdict, because the caller
|
|
311
|
+
* cancelled the request. No evidence either way.
|
|
312
|
+
*/
|
|
313
|
+
export type ProbeOutcome =
|
|
314
|
+
| { verdict: "recovered" }
|
|
315
|
+
| { verdict: "failing"; failedRoute: BreakerRoute | null }
|
|
316
|
+
| { verdict: "abandoned" };
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Report the outcome of the probe claimed by {@link tryAcquireRecoveryProbe}.
|
|
320
|
+
*
|
|
321
|
+
* A probe that reached a verdict closes every entry it was testing. It is the
|
|
322
|
+
* freshest evidence about those entries and it supersedes whatever opened
|
|
323
|
+
* them: an upstream that answers a probe with a retired-model 404 is an
|
|
324
|
+
* upstream that answers, so continuing to divert its healthy models would be
|
|
325
|
+
* acting on stale evidence. What a failing probe does establish is then
|
|
326
|
+
* recorded anew under `failedRoute`, carrying the escalated cooldown forward,
|
|
327
|
+
* so a route that keeps failing the same way still doubles its wait while a
|
|
328
|
+
* route whose failure changed shape is remembered at its true scope.
|
|
329
|
+
*
|
|
330
|
+
* An `abandoned` probe only hands the claim back. A cancelled request never
|
|
331
|
+
* asked the route anything, so treating it as recovery would delete a breaker
|
|
332
|
+
* that nothing has retested and send the next request through the full retry
|
|
333
|
+
* budget of a route still known to be down. The entry keeps its deadline and
|
|
334
|
+
* its escalation count: the cancellation neither counts as a failed probe nor
|
|
335
|
+
* restarts the wait, it just leaves the probe due again.
|
|
336
|
+
*/
|
|
337
|
+
export function releaseRecoveryProbe(
|
|
338
|
+
route: BreakerRoute,
|
|
339
|
+
outcome: ProbeOutcome,
|
|
340
|
+
now: number = Date.now(),
|
|
341
|
+
): void {
|
|
342
|
+
if (outcome.verdict === "abandoned") {
|
|
343
|
+
for (const key of coveringKeys(route)) {
|
|
344
|
+
const state = breakers.get(key);
|
|
345
|
+
if (state === undefined || !state.probeInFlight) {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
state.probeInFlight = false;
|
|
349
|
+
log.info(
|
|
350
|
+
{ route: key, failedProbes: state.failedProbes },
|
|
351
|
+
"Recovery probe was cancelled before it reached the upstream; the fallback breaker keeps its cooldown",
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
// Consecutive failed probes decide the next cooldown, so the count survives
|
|
357
|
+
// a change of scope: an outage that narrows to one model keeps escalating
|
|
358
|
+
// rather than restarting at the base wait.
|
|
359
|
+
let escalation = 0;
|
|
360
|
+
for (const key of coveringKeys(route)) {
|
|
361
|
+
const state = breakers.get(key);
|
|
362
|
+
if (state === undefined || !state.probeInFlight) {
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
state.probeInFlight = false;
|
|
366
|
+
escalation = Math.max(escalation, state.failedProbes);
|
|
367
|
+
if (outcome.verdict === "recovered") {
|
|
368
|
+
log.info(
|
|
369
|
+
{ route: key, failedProbes: state.failedProbes },
|
|
370
|
+
"Recovery probe succeeded; closing the fallback breaker",
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
breakers.delete(key);
|
|
374
|
+
}
|
|
375
|
+
const failedRoute =
|
|
376
|
+
outcome.verdict === "recovered" ? null : outcome.failedRoute;
|
|
377
|
+
if (failedRoute === null) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const key = keyOf(failedRoute);
|
|
381
|
+
const state = stateFor(key);
|
|
382
|
+
state.failedProbes = Math.max(state.failedProbes, escalation) + 1;
|
|
383
|
+
trip(key, state, now, "recovery_probe_failed");
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Test-only: drop every remembered route so state cannot leak between tests. */
|
|
387
|
+
export function resetFallbackBreaker(): void {
|
|
388
|
+
breakers.clear();
|
|
389
|
+
}
|