@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
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { recordTelemetryEvent } from "../telemetry/telemetry-events-outbox.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
LifecycleTelemetryEvent,
|
|
4
|
+
TelemetryEventBase,
|
|
5
|
+
} from "../telemetry/types.js";
|
|
3
6
|
import { APP_VERSION } from "../version.js";
|
|
4
7
|
|
|
5
8
|
export interface LifecycleEvent {
|
|
@@ -8,6 +11,68 @@ export interface LifecycleEvent {
|
|
|
8
11
|
createdAt: number;
|
|
9
12
|
}
|
|
10
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Optional typed attributes a lifecycle event carries alongside its name.
|
|
16
|
+
*
|
|
17
|
+
* Every one is nullable and optional on the wire, so a caller with nothing to
|
|
18
|
+
* say omits it and a platform that predates the field ignores it. They exist
|
|
19
|
+
* so grouping dimensions (risk, access preset, surface) are columns rather
|
|
20
|
+
* than substrings of `event_name`, which the server caps at 64 chars.
|
|
21
|
+
*/
|
|
22
|
+
export interface LifecycleEventAttributes {
|
|
23
|
+
/**
|
|
24
|
+
* Tool the event is about. The stable join key between a permission prompt
|
|
25
|
+
* and its decision: `event_name` embeds a truncated tool name, this does not.
|
|
26
|
+
*/
|
|
27
|
+
toolName?: string;
|
|
28
|
+
/** Classified risk of the invocation ("low" | "medium" | "high"). */
|
|
29
|
+
riskLevel?: string;
|
|
30
|
+
/** Auto-approve threshold (access preset) in effect for the invocation. */
|
|
31
|
+
riskThreshold?: string;
|
|
32
|
+
/** Channel the event's turn runs on ("vellum", "slack", "telegram", ...). */
|
|
33
|
+
surface?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Parent conversation. Rides the same `share_analytics` gate as the
|
|
36
|
+
* conversation ids on `turn` / `llm_usage` / `tool_executed`, and scopes the
|
|
37
|
+
* pending outbox row so deleting the conversation deletes the row with it.
|
|
38
|
+
*/
|
|
39
|
+
conversationId?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Lifecycle wire payload minus the fields `recordTelemetryEvent` stamps. */
|
|
43
|
+
type LifecycleFields = Omit<LifecycleTelemetryEvent, keyof TelemetryEventBase>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Assemble the lifecycle wire fields, omitting attributes the caller left
|
|
47
|
+
* unset so a plain `app_open` payload stays exactly as small as it was.
|
|
48
|
+
*
|
|
49
|
+
* An empty attribute is omitted rather than sent: the wire schema requires at
|
|
50
|
+
* least one character, and a single failing field makes the server drop the
|
|
51
|
+
* whole event silently.
|
|
52
|
+
*/
|
|
53
|
+
function buildLifecycleFields(
|
|
54
|
+
eventName: string,
|
|
55
|
+
attributes: LifecycleEventAttributes,
|
|
56
|
+
): LifecycleFields {
|
|
57
|
+
const fields: LifecycleFields = { event_name: eventName };
|
|
58
|
+
if (attributes.toolName) {
|
|
59
|
+
fields.tool_name = attributes.toolName;
|
|
60
|
+
}
|
|
61
|
+
if (attributes.riskLevel) {
|
|
62
|
+
fields.risk_level = attributes.riskLevel;
|
|
63
|
+
}
|
|
64
|
+
if (attributes.riskThreshold) {
|
|
65
|
+
fields.risk_threshold = attributes.riskThreshold;
|
|
66
|
+
}
|
|
67
|
+
if (attributes.surface) {
|
|
68
|
+
fields.surface = attributes.surface;
|
|
69
|
+
}
|
|
70
|
+
if (attributes.conversationId) {
|
|
71
|
+
fields.conversation_id = attributes.conversationId;
|
|
72
|
+
}
|
|
73
|
+
return fields;
|
|
74
|
+
}
|
|
75
|
+
|
|
11
76
|
/**
|
|
12
77
|
* Wire shape of one lifecycle event, for callers that insert outbox rows via
|
|
13
78
|
* raw SQL (conversation-crud's clearAll audit path). Mirrors the shape
|
|
@@ -32,9 +97,14 @@ export function buildLifecycleTelemetryEvent(
|
|
|
32
97
|
* Record a lifecycle event (e.g. app_open, hatch) into the `telemetry_events`
|
|
33
98
|
* outbox. Consent gating and degraded-mode `null` are `recordTelemetryEvent`'s.
|
|
34
99
|
*/
|
|
35
|
-
export function recordLifecycleEvent(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
100
|
+
export function recordLifecycleEvent(
|
|
101
|
+
eventName: string,
|
|
102
|
+
attributes: LifecycleEventAttributes = {},
|
|
103
|
+
): LifecycleEvent | null {
|
|
104
|
+
const recorded = recordTelemetryEvent(
|
|
105
|
+
"lifecycle",
|
|
106
|
+
buildLifecycleFields(eventName, attributes),
|
|
107
|
+
{ conversationId: attributes.conversationId ?? null },
|
|
108
|
+
);
|
|
39
109
|
return recorded ? { ...recorded, eventName } : null;
|
|
40
110
|
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import {
|
|
2
|
+
afterEach,
|
|
3
|
+
beforeEach,
|
|
4
|
+
describe,
|
|
5
|
+
expect,
|
|
6
|
+
mock,
|
|
7
|
+
setSystemTime,
|
|
8
|
+
test,
|
|
9
|
+
} from "bun:test";
|
|
10
|
+
|
|
11
|
+
let mockClient: {
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
platformAssistantId: string;
|
|
14
|
+
fetch: (path: string, init: RequestInit) => Promise<Response>;
|
|
15
|
+
} | null;
|
|
16
|
+
|
|
17
|
+
mock.module("./client.js", () => ({
|
|
18
|
+
VellumPlatformClient: { create: async () => mockClient },
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
import { getLogger } from "../util/logger.js";
|
|
22
|
+
import {
|
|
23
|
+
createPlatformPatchQueue,
|
|
24
|
+
type SyncedKey,
|
|
25
|
+
} from "./platform-patch-queue.js";
|
|
26
|
+
|
|
27
|
+
interface Patch {
|
|
28
|
+
path: string;
|
|
29
|
+
body: { value: string };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let patches: Patch[];
|
|
33
|
+
let respond: () => Response;
|
|
34
|
+
let builds: number;
|
|
35
|
+
let queues: Array<{ dispose: () => void }>;
|
|
36
|
+
|
|
37
|
+
function makeClient(assistantId = "asst-1", baseUrl = "https://platform.a") {
|
|
38
|
+
return {
|
|
39
|
+
baseUrl,
|
|
40
|
+
platformAssistantId: assistantId,
|
|
41
|
+
fetch: async (path: string, init: RequestInit) => {
|
|
42
|
+
patches.push({ path, body: JSON.parse(init.body as string) });
|
|
43
|
+
return respond();
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function makeQueue(
|
|
49
|
+
opts: {
|
|
50
|
+
loadSyncedKey?: () => SyncedKey | null;
|
|
51
|
+
saveSyncedKey?: (synced: SyncedKey) => void;
|
|
52
|
+
maxAgeMs?: number;
|
|
53
|
+
retryDelaysMs?: number[];
|
|
54
|
+
} = {},
|
|
55
|
+
) {
|
|
56
|
+
const queue = createPlatformPatchQueue<string | undefined>({
|
|
57
|
+
log: getLogger("test"),
|
|
58
|
+
label: "value",
|
|
59
|
+
buildPayload: (value) => {
|
|
60
|
+
builds += 1;
|
|
61
|
+
return value === undefined ? undefined : { key: value, body: { value } };
|
|
62
|
+
},
|
|
63
|
+
...opts,
|
|
64
|
+
});
|
|
65
|
+
queues.push(queue);
|
|
66
|
+
return queue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function settle(): Promise<void> {
|
|
70
|
+
for (let i = 0; i < 10; i++) {
|
|
71
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe("createPlatformPatchQueue", () => {
|
|
76
|
+
beforeEach(() => {
|
|
77
|
+
patches = [];
|
|
78
|
+
builds = 0;
|
|
79
|
+
respond = () => new Response("{}", { status: 200 });
|
|
80
|
+
mockClient = makeClient();
|
|
81
|
+
queues = [];
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
for (const queue of queues) {
|
|
86
|
+
queue.dispose();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("PATCHes once and dedups an unchanged key", async () => {
|
|
91
|
+
const queue = makeQueue();
|
|
92
|
+
queue.enqueue("a");
|
|
93
|
+
await settle();
|
|
94
|
+
queue.enqueue("a");
|
|
95
|
+
await settle();
|
|
96
|
+
|
|
97
|
+
expect(patches).toEqual([
|
|
98
|
+
{ path: "/v1/assistants/asst-1/", body: { value: "a" } },
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("re-registering to another assistant id or base URL re-sends", async () => {
|
|
103
|
+
const queue = makeQueue();
|
|
104
|
+
queue.enqueue("a");
|
|
105
|
+
await settle();
|
|
106
|
+
mockClient = makeClient("asst-2");
|
|
107
|
+
queue.enqueue("a");
|
|
108
|
+
await settle();
|
|
109
|
+
mockClient = makeClient("asst-2", "https://platform.b");
|
|
110
|
+
queue.enqueue("a");
|
|
111
|
+
await settle();
|
|
112
|
+
queue.enqueue("a");
|
|
113
|
+
await settle();
|
|
114
|
+
|
|
115
|
+
expect(patches.map((p) => p.path)).toEqual([
|
|
116
|
+
"/v1/assistants/asst-1/",
|
|
117
|
+
"/v1/assistants/asst-2/",
|
|
118
|
+
"/v1/assistants/asst-2/",
|
|
119
|
+
]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("rapid enqueues collapse into one PATCH carrying the newest payload", async () => {
|
|
123
|
+
const queue = makeQueue();
|
|
124
|
+
queue.enqueue("a");
|
|
125
|
+
queue.enqueue("b");
|
|
126
|
+
queue.enqueue("c");
|
|
127
|
+
await settle();
|
|
128
|
+
|
|
129
|
+
expect(patches.map((p) => p.body.value)).toEqual(["c"]);
|
|
130
|
+
expect(builds).toBe(1);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("an undefined payload, missing client, or missing assistant id skips", async () => {
|
|
134
|
+
const queue = makeQueue();
|
|
135
|
+
queue.enqueue(undefined);
|
|
136
|
+
await settle();
|
|
137
|
+
mockClient = null;
|
|
138
|
+
queue.enqueue("a");
|
|
139
|
+
await settle();
|
|
140
|
+
mockClient = makeClient("");
|
|
141
|
+
queue.enqueue("a");
|
|
142
|
+
await settle();
|
|
143
|
+
|
|
144
|
+
expect(patches).toHaveLength(0);
|
|
145
|
+
expect(builds).toBe(1);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("a failed PATCH does not dedup the next attempt", async () => {
|
|
149
|
+
const queue = makeQueue();
|
|
150
|
+
respond = () => new Response("nope", { status: 500 });
|
|
151
|
+
queue.enqueue("a");
|
|
152
|
+
await settle();
|
|
153
|
+
respond = () => new Response("{}", { status: 200 });
|
|
154
|
+
queue.enqueue("a");
|
|
155
|
+
await settle();
|
|
156
|
+
queue.enqueue("a");
|
|
157
|
+
await settle();
|
|
158
|
+
|
|
159
|
+
expect(patches).toHaveLength(2);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("a thrown fetch is swallowed and retried on the next enqueue", async () => {
|
|
163
|
+
const queue = makeQueue();
|
|
164
|
+
mockClient = {
|
|
165
|
+
...makeClient(),
|
|
166
|
+
fetch: async () => {
|
|
167
|
+
throw new Error("boom");
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
queue.enqueue("a");
|
|
171
|
+
await settle();
|
|
172
|
+
mockClient = makeClient();
|
|
173
|
+
queue.enqueue("a");
|
|
174
|
+
await settle();
|
|
175
|
+
|
|
176
|
+
expect(patches).toHaveLength(1);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("seeds the dedup key from loadSyncedKey and saves each success", async () => {
|
|
180
|
+
const saved: SyncedKey[] = [];
|
|
181
|
+
const queue = makeQueue({
|
|
182
|
+
loadSyncedKey: () => ({
|
|
183
|
+
key: "https://platform.a|asst-1|a",
|
|
184
|
+
syncedAt: Date.now(),
|
|
185
|
+
}),
|
|
186
|
+
saveSyncedKey: (synced) => saved.push(synced),
|
|
187
|
+
});
|
|
188
|
+
queue.enqueue("a");
|
|
189
|
+
await settle();
|
|
190
|
+
queue.enqueue("b");
|
|
191
|
+
await settle();
|
|
192
|
+
|
|
193
|
+
expect(patches.map((p) => p.body.value)).toEqual(["b"]);
|
|
194
|
+
expect(saved).toEqual([
|
|
195
|
+
{ key: "https://platform.a|asst-1|b", syncedAt: expect.any(Number) },
|
|
196
|
+
]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("a matching key older than maxAgeMs re-sends in-process", async () => {
|
|
200
|
+
const saved: SyncedKey[] = [];
|
|
201
|
+
const queue = makeQueue({
|
|
202
|
+
maxAgeMs: 1000,
|
|
203
|
+
saveSyncedKey: (synced) => saved.push(synced),
|
|
204
|
+
});
|
|
205
|
+
const start = Date.now();
|
|
206
|
+
setSystemTime(new Date(start));
|
|
207
|
+
try {
|
|
208
|
+
queue.enqueue("a");
|
|
209
|
+
await settle();
|
|
210
|
+
setSystemTime(new Date(start + 999));
|
|
211
|
+
queue.enqueue("a");
|
|
212
|
+
await settle();
|
|
213
|
+
setSystemTime(new Date(start + 1000));
|
|
214
|
+
queue.enqueue("a");
|
|
215
|
+
await settle();
|
|
216
|
+
queue.enqueue("a");
|
|
217
|
+
await settle();
|
|
218
|
+
} finally {
|
|
219
|
+
setSystemTime();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
expect(patches).toHaveLength(2);
|
|
223
|
+
expect(saved.map((s) => s.syncedAt)).toEqual([start, start + 1000]);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("a seeded key older than maxAgeMs re-sends", async () => {
|
|
227
|
+
const queue = makeQueue({
|
|
228
|
+
maxAgeMs: 1000,
|
|
229
|
+
loadSyncedKey: () => ({
|
|
230
|
+
key: "https://platform.a|asst-1|a",
|
|
231
|
+
syncedAt: Date.now() - 1000,
|
|
232
|
+
}),
|
|
233
|
+
});
|
|
234
|
+
queue.enqueue("a");
|
|
235
|
+
await settle();
|
|
236
|
+
|
|
237
|
+
expect(patches).toHaveLength(1);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("re-sends at expiry with no enqueue call, and dispose cancels that", async () => {
|
|
241
|
+
const queue = makeQueue({ maxAgeMs: 300 });
|
|
242
|
+
queue.enqueue("a");
|
|
243
|
+
await settle();
|
|
244
|
+
expect(patches).toHaveLength(1);
|
|
245
|
+
|
|
246
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
247
|
+
await settle();
|
|
248
|
+
expect(patches).toHaveLength(2);
|
|
249
|
+
expect(builds).toBe(2);
|
|
250
|
+
|
|
251
|
+
queue.dispose();
|
|
252
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
253
|
+
await settle();
|
|
254
|
+
expect(patches).toHaveLength(2);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("a seeded, still-fresh key arms the expiry re-send", async () => {
|
|
258
|
+
const queue = makeQueue({
|
|
259
|
+
maxAgeMs: 300,
|
|
260
|
+
loadSyncedKey: () => ({
|
|
261
|
+
key: "https://platform.a|asst-1|a",
|
|
262
|
+
syncedAt: Date.now(),
|
|
263
|
+
}),
|
|
264
|
+
});
|
|
265
|
+
queue.enqueue("a");
|
|
266
|
+
await settle();
|
|
267
|
+
expect(patches).toHaveLength(0);
|
|
268
|
+
|
|
269
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
270
|
+
await settle();
|
|
271
|
+
expect(patches).toHaveLength(1);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("a failed PATCH retries on backoff and dedups after the retry succeeds", async () => {
|
|
275
|
+
const queue = makeQueue({ retryDelaysMs: [100, 100, 100] });
|
|
276
|
+
respond = () => new Response("nope", { status: 500 });
|
|
277
|
+
queue.enqueue("a");
|
|
278
|
+
await settle();
|
|
279
|
+
expect(patches).toHaveLength(1);
|
|
280
|
+
|
|
281
|
+
respond = () => new Response("{}", { status: 200 });
|
|
282
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
283
|
+
await settle();
|
|
284
|
+
expect(patches).toHaveLength(2);
|
|
285
|
+
|
|
286
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
287
|
+
await settle();
|
|
288
|
+
queue.enqueue("a");
|
|
289
|
+
await settle();
|
|
290
|
+
expect(patches).toHaveLength(2);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("a thrown fetch retries on backoff", async () => {
|
|
294
|
+
const queue = makeQueue({ retryDelaysMs: [100] });
|
|
295
|
+
mockClient = {
|
|
296
|
+
...makeClient(),
|
|
297
|
+
fetch: async () => {
|
|
298
|
+
throw new Error("boom");
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
queue.enqueue("a");
|
|
302
|
+
await settle();
|
|
303
|
+
mockClient = makeClient();
|
|
304
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
305
|
+
await settle();
|
|
306
|
+
|
|
307
|
+
expect(patches).toHaveLength(1);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("retries stop once the backoff schedule is exhausted", async () => {
|
|
311
|
+
const queue = makeQueue({ retryDelaysMs: [50, 50] });
|
|
312
|
+
respond = () => new Response("nope", { status: 500 });
|
|
313
|
+
queue.enqueue("a");
|
|
314
|
+
await settle();
|
|
315
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
316
|
+
await settle();
|
|
317
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
318
|
+
await settle();
|
|
319
|
+
expect(patches).toHaveLength(3);
|
|
320
|
+
|
|
321
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
322
|
+
await settle();
|
|
323
|
+
expect(patches).toHaveLength(3);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("a new enqueue during backoff supersedes the retry and resets the bound", async () => {
|
|
327
|
+
const queue = makeQueue({ retryDelaysMs: [100] });
|
|
328
|
+
respond = () => new Response("nope", { status: 500 });
|
|
329
|
+
queue.enqueue("a");
|
|
330
|
+
await settle();
|
|
331
|
+
queue.enqueue("b");
|
|
332
|
+
await settle();
|
|
333
|
+
expect(patches.map((p) => p.body.value)).toEqual(["a", "b"]);
|
|
334
|
+
|
|
335
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
336
|
+
await settle();
|
|
337
|
+
expect(patches.map((p) => p.body.value)).toEqual(["a", "b", "b"]);
|
|
338
|
+
|
|
339
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
340
|
+
await settle();
|
|
341
|
+
expect(patches).toHaveLength(3);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("dispose cancels a pending retry", async () => {
|
|
345
|
+
const queue = makeQueue({ retryDelaysMs: [300] });
|
|
346
|
+
respond = () => new Response("nope", { status: 500 });
|
|
347
|
+
queue.enqueue("a");
|
|
348
|
+
await settle();
|
|
349
|
+
queue.dispose();
|
|
350
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
351
|
+
await settle();
|
|
352
|
+
|
|
353
|
+
expect(patches).toHaveLength(1);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("without maxAgeMs a matching key never expires", async () => {
|
|
357
|
+
const queue = makeQueue({
|
|
358
|
+
loadSyncedKey: () => ({
|
|
359
|
+
key: "https://platform.a|asst-1|a",
|
|
360
|
+
syncedAt: 0,
|
|
361
|
+
}),
|
|
362
|
+
});
|
|
363
|
+
queue.enqueue("a");
|
|
364
|
+
await settle();
|
|
365
|
+
|
|
366
|
+
expect(patches).toHaveLength(0);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialized, deduplicated PATCH queue for the platform Assistant record.
|
|
3
|
+
*
|
|
4
|
+
* Each `enqueue` bumps a sequence number and chains onto the previous request,
|
|
5
|
+
* so rapid changes collapse into one PATCH carrying the newest payload and a
|
|
6
|
+
* stale in-flight response cannot overwrite a newer value. The dedup key is
|
|
7
|
+
* scoped to the platform destination (base URL + assistant id), so
|
|
8
|
+
* re-registering elsewhere re-sends an unchanged payload. With `maxAgeMs`, a
|
|
9
|
+
* key only dedups while its last success is younger than that, and an unref'd
|
|
10
|
+
* timer re-enqueues the last input at expiry so an idle long-lived process
|
|
11
|
+
* re-sends without any caller. Best-effort: failures are logged, never
|
|
12
|
+
* thrown, and leave the dedup key untouched; a failed request re-enqueues
|
|
13
|
+
* itself on a bounded backoff (superseded by any new enqueue).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { getLogger } from "../util/logger.js";
|
|
17
|
+
import { VellumPlatformClient } from "./client.js";
|
|
18
|
+
|
|
19
|
+
type Logger = ReturnType<typeof getLogger>;
|
|
20
|
+
|
|
21
|
+
type PatchBody = Record<string, unknown>;
|
|
22
|
+
|
|
23
|
+
export interface PatchPayload {
|
|
24
|
+
/** Identifies the payload content; equal keys are not re-sent. */
|
|
25
|
+
key: string;
|
|
26
|
+
/** A function runs only after the key passes dedup; undefined skips. */
|
|
27
|
+
body: PatchBody | (() => Promise<PatchBody | undefined>);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SyncedKey {
|
|
31
|
+
/** Destination-scoped dedup key of the last successful PATCH. */
|
|
32
|
+
key: string;
|
|
33
|
+
/** Epoch ms of that PATCH. */
|
|
34
|
+
syncedAt: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface PlatformPatchQueueOptions<T> {
|
|
38
|
+
log: Logger;
|
|
39
|
+
/** Names the synced thing in log lines, e.g. "avatar". */
|
|
40
|
+
label: string;
|
|
41
|
+
/** Returns undefined to skip the PATCH entirely. */
|
|
42
|
+
buildPayload: (
|
|
43
|
+
input: T,
|
|
44
|
+
) => Promise<PatchPayload | undefined> | PatchPayload | undefined;
|
|
45
|
+
/** Seeds the dedup key on first use (e.g. from disk). */
|
|
46
|
+
loadSyncedKey?: () => SyncedKey | null;
|
|
47
|
+
/** Called after each successful PATCH. */
|
|
48
|
+
saveSyncedKey?: (synced: SyncedKey) => void;
|
|
49
|
+
/** A matching key older than this re-sends, via a timer; omit to never expire. */
|
|
50
|
+
maxAgeMs?: number;
|
|
51
|
+
/** Backoff after each failed attempt; retries stop once exhausted. */
|
|
52
|
+
retryDelaysMs?: number[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const DEFAULT_RETRY_DELAYS_MS = [30_000, 120_000, 600_000];
|
|
56
|
+
|
|
57
|
+
export interface PlatformPatchQueue<T> {
|
|
58
|
+
enqueue: (input: T) => void;
|
|
59
|
+
/** Cancels the expiry and retry timers; queued requests still run. */
|
|
60
|
+
dispose: () => void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function createPlatformPatchQueue<T = void>(
|
|
64
|
+
options: PlatformPatchQueueOptions<T>,
|
|
65
|
+
): PlatformPatchQueue<T> {
|
|
66
|
+
const {
|
|
67
|
+
log,
|
|
68
|
+
label,
|
|
69
|
+
buildPayload,
|
|
70
|
+
loadSyncedKey,
|
|
71
|
+
saveSyncedKey,
|
|
72
|
+
maxAgeMs,
|
|
73
|
+
retryDelaysMs = DEFAULT_RETRY_DELAYS_MS,
|
|
74
|
+
} = options;
|
|
75
|
+
let lastSynced: SyncedKey | null | undefined;
|
|
76
|
+
let seq = 0;
|
|
77
|
+
let pending: Promise<void> = Promise.resolve();
|
|
78
|
+
let expiryTimer: ReturnType<typeof setTimeout> | undefined;
|
|
79
|
+
let retryTimer: ReturnType<typeof setTimeout> | undefined;
|
|
80
|
+
|
|
81
|
+
function armExpiry(input: T): void {
|
|
82
|
+
if (maxAgeMs === undefined || !lastSynced) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
clearTimeout(expiryTimer);
|
|
86
|
+
const delay = Math.max(0, lastSynced.syncedAt + maxAgeMs - Date.now());
|
|
87
|
+
expiryTimer = setTimeout(() => enqueue(input), delay);
|
|
88
|
+
expiryTimer.unref?.();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function armRetry(input: T, requestSeq: number, attempt: number): void {
|
|
92
|
+
const delay = retryDelaysMs[attempt];
|
|
93
|
+
if (requestSeq !== seq || delay === undefined) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
clearTimeout(retryTimer);
|
|
97
|
+
retryTimer = setTimeout(() => schedule(input, attempt + 1), delay);
|
|
98
|
+
retryTimer.unref?.();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function run(
|
|
102
|
+
input: T,
|
|
103
|
+
requestSeq: number,
|
|
104
|
+
attempt: number,
|
|
105
|
+
): Promise<void> {
|
|
106
|
+
try {
|
|
107
|
+
if (requestSeq !== seq) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const client = await VellumPlatformClient.create();
|
|
111
|
+
const assistantId = client?.platformAssistantId;
|
|
112
|
+
if (!client || !assistantId) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const payload = await buildPayload(input);
|
|
117
|
+
if (!payload || requestSeq !== seq) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const key = `${client.baseUrl}|${assistantId}|${payload.key}`;
|
|
121
|
+
lastSynced ??= loadSyncedKey?.() ?? null;
|
|
122
|
+
if (
|
|
123
|
+
lastSynced?.key === key &&
|
|
124
|
+
(maxAgeMs === undefined || Date.now() - lastSynced.syncedAt < maxAgeMs)
|
|
125
|
+
) {
|
|
126
|
+
armExpiry(input);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const body =
|
|
130
|
+
typeof payload.body === "function"
|
|
131
|
+
? await payload.body()
|
|
132
|
+
: payload.body;
|
|
133
|
+
if (!body || requestSeq !== seq) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const resp = await client.fetch(
|
|
138
|
+
`/v1/assistants/${encodeURIComponent(assistantId)}/`,
|
|
139
|
+
{
|
|
140
|
+
method: "PATCH",
|
|
141
|
+
headers: { "Content-Type": "application/json" },
|
|
142
|
+
body: JSON.stringify(body),
|
|
143
|
+
signal: AbortSignal.timeout(15_000),
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
if (resp.ok) {
|
|
148
|
+
lastSynced = { key, syncedAt: Date.now() };
|
|
149
|
+
saveSyncedKey?.(lastSynced);
|
|
150
|
+
armExpiry(input);
|
|
151
|
+
log.info(
|
|
152
|
+
{ key: payload.key, assistantId },
|
|
153
|
+
`Synced ${label} to platform`,
|
|
154
|
+
);
|
|
155
|
+
} else {
|
|
156
|
+
log.warn(
|
|
157
|
+
{
|
|
158
|
+
status: resp.status,
|
|
159
|
+
body: await resp.text(),
|
|
160
|
+
assistantId,
|
|
161
|
+
attempt,
|
|
162
|
+
},
|
|
163
|
+
`Failed to sync ${label} to platform`,
|
|
164
|
+
);
|
|
165
|
+
armRetry(input, requestSeq, attempt);
|
|
166
|
+
}
|
|
167
|
+
} catch (err) {
|
|
168
|
+
log.warn({ err, attempt }, `Error syncing ${label} to platform`);
|
|
169
|
+
armRetry(input, requestSeq, attempt);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function schedule(input: T, attempt: number): void {
|
|
174
|
+
const mySeq = ++seq;
|
|
175
|
+
pending = pending.then(() => run(input, mySeq, attempt)).catch(() => {});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function enqueue(input: T): void {
|
|
179
|
+
clearTimeout(retryTimer);
|
|
180
|
+
schedule(input, 0);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
enqueue,
|
|
185
|
+
dispose(): void {
|
|
186
|
+
clearTimeout(expiryTimer);
|
|
187
|
+
clearTimeout(retryTimer);
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|