@vellumai/assistant 0.10.9 → 0.10.10-dev.202607162206.d08e98e
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 +1 -1
- package/Dockerfile +8 -0
- package/docs/activation-funnel-telemetry.md +13 -7
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/redacted-credential.test.ts +200 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/redacted-credential.ts +226 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/redacted-credential.test.ts +200 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/redacted-credential.ts +226 -0
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/redacted-credential.test.ts +200 -0
- package/node_modules/@vellumai/service-contracts/src/redacted-credential.ts +226 -0
- package/openapi.yaml +530 -107
- package/package.json +1 -1
- package/scripts/generate-openapi.ts +8 -0
- package/src/__tests__/activation-early-marking.test.ts +6 -5
- package/src/__tests__/agent-loop-override-profile.test.ts +22 -25
- package/src/__tests__/agent-wake-override-profile.test.ts +21 -48
- package/src/__tests__/app-builder-tool-scripts.test.ts +0 -1
- package/src/__tests__/app-bundler.test.ts +4 -12
- package/src/__tests__/app-executors.test.ts +2 -49
- package/src/__tests__/app-routes-csp.test.ts +114 -146
- package/src/__tests__/auth-fallback-events-store.test.ts +8 -1
- package/src/__tests__/build-persisted-content.test.ts +96 -0
- package/src/__tests__/bundle-scanner.test.ts +27 -1
- package/src/__tests__/call-controller.test.ts +291 -0
- package/src/__tests__/call-site-routing-connection-auto-resolve.test.ts +165 -0
- package/src/__tests__/chat-credential-redaction.test.ts +1395 -0
- package/src/__tests__/chat-reveal-guard-priming.test.ts +791 -0
- package/src/__tests__/compaction.benchmark.test.ts +2 -1
- package/src/__tests__/compactor-image-manifest-trust.test.ts +50 -0
- package/src/__tests__/config-loader-backfill.test.ts +9 -4
- package/src/__tests__/config-schema-cmd.test.ts +10 -11
- package/src/__tests__/config-schema.test.ts +190 -257
- package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +208 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +5 -19
- package/src/__tests__/conversation-agent-loop.test.ts +22 -9
- package/src/__tests__/conversation-error.test.ts +31 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +110 -0
- package/src/__tests__/conversation-process-callsite.test.ts +12 -19
- package/src/__tests__/conversation-routes-slash-commands.test.ts +7 -21
- package/src/__tests__/conversation-summarize-route.test.ts +36 -44
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +7 -7
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +0 -1
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +0 -1
- package/src/__tests__/conversation-usage.test.ts +4 -12
- package/src/__tests__/credential-routes.test.ts +250 -0
- package/src/__tests__/credential-security-invariants.test.ts +3 -0
- package/src/__tests__/db-migration-rollback.test.ts +22 -0
- package/src/__tests__/empty-response-hook.test.ts +186 -1
- package/src/__tests__/external-plugin-loader.test.ts +11 -5
- package/src/__tests__/heartbeat-service.test.ts +0 -28
- package/src/__tests__/host-shell-tool.test.ts +2 -0
- package/src/__tests__/inactive-tool-error-messages.test.ts +2 -2
- package/src/__tests__/inference-no-mode-boot-e2e.test.ts +52 -8
- package/src/__tests__/internal-telemetry-routes.test.ts +23 -4
- package/src/__tests__/invite-routes-http.test.ts +12 -16
- package/src/__tests__/list-all-apps.test.ts +0 -4
- package/src/__tests__/llm-context-resolution.test.ts +32 -56
- package/src/__tests__/llm-request-log-turn-query.test.ts +109 -0
- package/src/__tests__/llm-resolver-override-or-default.test.ts +3 -52
- package/src/__tests__/llm-resolver.test.ts +342 -602
- package/src/__tests__/llm-schema.test.ts +79 -37
- package/src/__tests__/max-tokens-continue-hook.test.ts +19 -0
- package/src/__tests__/media-stream-output.test.ts +259 -3
- package/src/__tests__/media-stream-server-integration.test.ts +22 -1
- package/src/__tests__/media-stream-stt-session.test.ts +47 -0
- package/src/__tests__/memory-jobs-worker-cleanup-cadence.test.ts +33 -0
- package/src/__tests__/memory-recall-log-store.test.ts +47 -13
- package/src/__tests__/mock-gateway-ipc.ts +46 -1
- package/src/__tests__/mtime-cache.test.ts +61 -0
- package/src/__tests__/navigate-settings-tab.test.ts +2 -0
- package/src/__tests__/normalize-onboarding.test.ts +33 -0
- package/src/__tests__/onboarding-persona-write.test.ts +26 -0
- package/src/__tests__/plugin-api-resolve-credential.test.ts +140 -0
- package/src/__tests__/plugin-app-serve-routes.test.ts +166 -14
- package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +2 -0
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +38 -0
- package/src/__tests__/provider-commit-message-generator.test.ts +27 -20
- package/src/__tests__/provider-connections-backfill.test.ts +138 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +10 -31
- package/src/__tests__/provider-registry-ollama.test.ts +8 -18
- package/src/__tests__/provider-send-message-override-profile.test.ts +23 -23
- package/src/__tests__/provider-usage-tracking.test.ts +9 -19
- package/src/__tests__/prune-old-conversations-job.test.ts +12 -0
- package/src/__tests__/published-app-updater.test.ts +22 -16
- package/src/__tests__/registry.test.ts +5 -16
- package/src/__tests__/retry-openrouter-only-normalization.test.ts +22 -23
- package/src/__tests__/retry-thinking-adaptive-only.test.ts +43 -48
- package/src/__tests__/retry-thinking-tool-choice.test.ts +57 -66
- package/src/__tests__/retry-verbosity-normalization.test.ts +24 -23
- package/src/__tests__/reveal-success-registry.test.ts +123 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +130 -0
- package/src/__tests__/secret-fixtures.ts +9 -0
- package/src/__tests__/server-history-render.test.ts +28 -0
- package/src/__tests__/skills.test.ts +9 -4
- package/src/__tests__/slack-share-routes.test.ts +0 -1
- package/src/__tests__/stt-stream-session.test.ts +6 -5
- package/src/__tests__/subagent-call-site-routing.test.ts +69 -95
- package/src/__tests__/subagent-disposal.test.ts +2 -0
- package/src/__tests__/subagent-fork-notifications.test.ts +2 -0
- package/src/__tests__/subagent-fork-spawn.test.ts +2 -0
- package/src/__tests__/subagent-manager-notify.test.ts +2 -0
- package/src/__tests__/subagent-role-registry.test.ts +37 -0
- package/src/__tests__/subagent-spawn-and-await.test.ts +1 -0
- package/src/__tests__/subagent-terminal-message.test.ts +50 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +78 -5
- package/src/__tests__/surface-completion-nudge-hook.test.ts +19 -0
- package/src/__tests__/telemetry-routes.test.ts +99 -19
- package/src/__tests__/tool-audit.test.ts +34 -4
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
- package/src/__tests__/tool-profiler.test.ts +72 -1
- package/src/__tests__/tool-result-spool.test.ts +49 -4
- package/src/__tests__/tool-side-effects-slack-dm.test.ts +0 -1
- package/src/__tests__/ui-channel-variants.test.ts +108 -0
- package/src/__tests__/ui-shape-teaching.test.ts +255 -0
- package/src/__tests__/usage-attribution.test.ts +18 -41
- package/src/__tests__/user-plugin-loader.test.ts +4 -4
- package/src/__tests__/voice-config-update.test.ts +46 -0
- package/src/__tests__/voice-session-bridge.test.ts +216 -84
- package/src/__tests__/workspace-migration-131-drop-web-fetch-mode.test.ts +120 -0
- package/src/agent/loop.ts +4 -3
- package/src/api/events/open-conversation.test.ts +64 -0
- package/src/api/events/open-conversation.ts +33 -0
- package/src/api/index.ts +6 -0
- package/src/api/responses/conversation-message.ts +5 -0
- package/src/apps/app-store.ts +25 -35
- package/src/bundler/app-bundler.ts +34 -48
- package/src/bundler/app-compiler.ts +39 -4
- package/src/bundler/bundle-scanner.ts +13 -0
- package/src/bundler/manifest.ts +1 -1
- package/src/calls/__tests__/voice-session-bridge.test.ts +47 -0
- package/src/calls/call-constants.ts +5 -0
- package/src/calls/call-controller.ts +100 -32
- package/src/calls/call-transport.ts +9 -0
- package/src/calls/media-stream-output.ts +107 -4
- package/src/calls/media-stream-server.ts +29 -9
- package/src/calls/media-stream-stt-session.ts +7 -1
- package/src/calls/media-turn-detector.ts +11 -1
- package/src/calls/voice-session-bridge.ts +84 -63
- package/src/cli/commands/__tests__/inference-providers.test.ts +270 -33
- package/src/cli/commands/__tests__/notifications.test.ts +24 -3
- package/src/cli/commands/config.help.ts +6 -6
- package/src/cli/commands/credentials.help.ts +13 -0
- package/src/cli/commands/credentials.ts +6 -1
- package/src/cli/commands/email.help.ts +7 -0
- package/src/cli/commands/email.ts +35 -1
- package/src/cli/commands/inference-providers.ts +168 -107
- package/src/cli/commands/inference.help.ts +118 -46
- package/src/cli/commands/memory/index.help.ts +23 -0
- package/src/cli/commands/memory/nodes.ts +146 -0
- package/src/cli/commands/notifications.help.ts +10 -10
- package/src/cli/commands/oauth/connect-surface-guidance.test.ts +40 -0
- package/src/cli/commands/oauth/connect-surface-guidance.ts +54 -0
- package/src/cli/commands/oauth/connect.test.ts +126 -0
- package/src/cli/commands/oauth/connect.ts +29 -6
- package/src/cli/commands/oauth/index.help.ts +7 -1
- package/src/cli/commands/oauth/status.test.ts +69 -3
- package/src/cli/commands/oauth/status.ts +50 -12
- package/src/cli/commands/plugins.help.ts +13 -2
- package/src/cli/commands/plugins.ts +61 -7
- package/src/cli/commands/telemetry.help.ts +13 -0
- package/src/cli/commands/telemetry.ts +45 -5
- package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +8 -2
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +213 -9
- package/src/cli/lib/bundled-marketplace.json +93 -36
- package/src/cli/lib/inspect-plugin.ts +5 -1
- package/src/cli/lib/install-from-github.ts +68 -4
- package/src/cli/lib/plugin-catalog-local.ts +6 -2
- package/src/cli/lib/plugin-fingerprint.ts +3 -3
- package/src/cli/lib/upgrade-plugin.ts +236 -35
- package/src/config/__tests__/default-profile-catalog.test.ts +8 -12
- package/src/config/__tests__/plugin-resident-skill-discovery.test.ts +137 -0
- package/src/config/__tests__/profile-materialization.test.ts +1 -88
- package/src/config/bundled-skills/AGENTS.md +3 -30
- package/src/config/bundled-skills/app-builder/SKILL.md +5 -3
- package/src/config/bundled-skills/app-builder/TOOLS.json +23 -0
- package/src/config/bundled-skills/app-builder/tools/app-open.ts +32 -0
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +1 -1
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +7 -7
- package/src/config/bundled-skills/schedule/SKILL.md +1 -1
- package/src/config/bundled-skills/schedule/TOOLS.json +1 -1
- package/src/config/bundled-skills/settings/TOOLS.json +3 -1
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +2 -0
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +18 -10
- package/src/config/bundled-skills/subagent/SKILL.md +2 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +1 -1
- package/src/config/call-site-defaults.ts +3 -3
- package/src/config/feature-flag-registry.json +24 -39
- package/src/config/llm-resolver.ts +88 -475
- package/src/config/profile-materialization.ts +11 -11
- package/src/config/schema.ts +93 -127
- package/src/config/schemas/__tests__/live-voice.test.ts +24 -6
- package/src/config/schemas/__tests__/stt.test.ts +31 -3
- package/src/config/schemas/live-voice.ts +10 -4
- package/src/config/schemas/llm.ts +52 -15
- package/src/config/schemas/memory-lifecycle.ts +1 -1
- package/src/config/schemas/memory-retrospective.ts +8 -0
- package/src/config/schemas/memory-v2.ts +2 -2
- package/src/config/schemas/memory-v3.ts +1 -1
- package/src/config/schemas/services.ts +6 -3
- package/src/config/schemas/stt.ts +38 -21
- package/src/config/schemas/tts.ts +13 -18
- package/src/config/skills.ts +31 -21
- package/src/context/compactor.ts +44 -7
- package/src/context/post-turn-tool-result-truncation.ts +4 -2
- package/src/context/tool-result-spool.ts +26 -5
- package/src/conversations/__tests__/message-consolidation.test.ts +48 -0
- package/src/conversations/message-consolidation.ts +22 -2
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +5 -12
- package/src/daemon/app-source-watcher.ts +17 -23
- package/src/daemon/chat-credential-redaction.ts +1365 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +627 -34
- package/src/daemon/conversation-agent-loop.ts +71 -31
- package/src/daemon/conversation-error.ts +36 -14
- package/src/daemon/conversation-process.ts +22 -0
- package/src/daemon/conversation-store.ts +35 -0
- package/src/daemon/conversation-surfaces.ts +25 -25
- package/src/daemon/conversation-tool-setup.ts +33 -7
- package/src/daemon/conversation.ts +55 -5
- package/src/daemon/handlers/shared.ts +33 -2
- package/src/daemon/lifecycle.ts +4 -4
- package/src/daemon/message-types/conversations.ts +6 -17
- package/src/daemon/providers-setup.ts +8 -0
- package/src/daemon/tool-setup-types.ts +7 -1
- package/src/daemon/wake-conversation-ops.ts +10 -1
- package/src/hooks/types.ts +9 -0
- package/src/ipc/__tests__/email-ipc.test.ts +90 -0
- package/src/ipc/gateway-client.test.ts +59 -0
- package/src/ipc/gateway-client.ts +70 -27
- package/src/live-voice/__tests__/live-voice-events.test.ts +14 -2
- package/src/live-voice/__tests__/live-voice-integration.test.ts +116 -2
- package/src/live-voice/__tests__/live-voice-vad.test.ts +804 -13
- package/src/live-voice/__tests__/protocol.test.ts +122 -0
- package/src/live-voice/live-voice-session.ts +443 -40
- package/src/live-voice/protocol.ts +143 -1
- package/src/monitoring/__tests__/plugin-source-watch.test.ts +3 -1
- package/src/monitoring/plugin-source-watch.ts +3 -62
- package/src/notifications/README.md +1 -1
- package/src/permissions/checker.ts +8 -4
- package/src/persistence/__tests__/db-init-migrations-ok.test.ts +26 -0
- package/src/persistence/conversation-crud.ts +104 -2
- package/src/persistence/db-init.ts +14 -3
- package/src/persistence/job-handlers/cleanup.ts +15 -7
- package/src/persistence/llm-request-log-store.ts +88 -52
- package/src/persistence/migrations/298-move-memory-jobs-to-memory-db.ts +7 -31
- package/src/persistence/migrations/305-drop-contact-acl-columns.ts +3 -2
- package/src/persistence/migrations/326-move-injection-events-to-memory-db.ts +8 -34
- package/src/persistence/migrations/336-move-memory-v2-activation-logs-to-memory-db.ts +90 -0
- package/src/persistence/migrations/337-move-memory-recall-logs-to-memory-db.ts +114 -0
- package/src/persistence/migrations/338-move-memory-v3-selections-to-memory-db.ts +84 -0
- package/src/persistence/migrations/339-move-activation-sessions-to-memory-db.ts +52 -0
- package/src/persistence/migrations/__tests__/run-migrations.test.ts +155 -0
- package/src/persistence/migrations/helpers/relocation.ts +44 -1
- package/src/persistence/migrations/run-migrations.ts +25 -1
- package/src/persistence/schema/infrastructure.ts +9 -0
- package/src/persistence/schema/memory-core.ts +3 -0
- package/src/persistence/schema/memory-injection.ts +2 -0
- package/src/persistence/steps.ts +36 -0
- package/src/platform/client.test.ts +1 -44
- package/src/platform/client.ts +10 -20
- package/src/platform/consent-cache.test.ts +89 -23
- package/src/platform/consent-cache.ts +71 -33
- package/src/plugin-api/constants.ts +12 -0
- package/src/plugin-api/conversation-turn.ts +37 -14
- package/src/plugin-api/index.ts +11 -1
- package/src/plugin-api/resolve-credential.ts +75 -0
- package/src/plugin-api/vision-support.test.ts +8 -19
- package/src/plugin-api/vision-support.ts +26 -17
- package/src/plugins/collect-source-versions.ts +77 -0
- package/src/plugins/defaults/compaction/compact.ts +6 -0
- package/src/plugins/defaults/compaction/window-manager.ts +9 -0
- package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +18 -24
- package/src/plugins/defaults/empty-response/hooks/user-prompt-submit.ts +38 -0
- package/src/plugins/defaults/empty-response/refusal-quarantine.ts +99 -0
- package/src/plugins/defaults/image-fallback/__tests__/caption-cache-persistence.test.ts +7 -3
- package/src/plugins/defaults/index.ts +8 -1
- package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +4 -1
- package/src/plugins/defaults/memory/__tests__/activation-session-store.test.ts +48 -6
- package/src/plugins/defaults/memory/__tests__/db-memory-attach.test.ts +28 -22
- package/src/plugins/defaults/memory/__tests__/memory-log-stores-degraded.test.ts +148 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +80 -8
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +202 -0
- package/src/plugins/defaults/memory/__tests__/memory-v2-activation-log-store.test.ts +64 -25
- package/src/plugins/defaults/memory/__tests__/memory-v2-concept-frequency.test.ts +24 -11
- package/src/plugins/defaults/memory/__tests__/prompt-override.test.ts +70 -6
- package/src/plugins/defaults/memory/__tests__/table-relocation.test.ts +278 -0
- package/src/plugins/defaults/memory/activation-session-store.ts +27 -20
- package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +2 -9
- package/src/plugins/defaults/memory/context-search/sources/workspace.ts +1 -8
- package/src/plugins/defaults/memory/graph/retriever.test.ts +6 -6
- package/src/plugins/defaults/memory/graph/store.ts +114 -0
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +36 -1
- package/src/plugins/defaults/memory/graph/tools.ts +47 -13
- package/src/plugins/defaults/memory/graph-topology/build-memory-graph.ts +16 -35
- package/src/plugins/defaults/memory/jobs-worker.ts +10 -1
- package/src/plugins/defaults/memory/memory-db.ts +3 -2
- package/src/plugins/defaults/memory/memory-recall-log-store.ts +129 -66
- package/src/plugins/defaults/memory/memory-retrospective-constants.ts +8 -0
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +11 -122
- package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +216 -0
- package/src/plugins/defaults/memory/memory-v2-activation-log-store.ts +84 -46
- package/src/plugins/defaults/memory/memory-v2-concept-frequency.ts +42 -32
- package/src/plugins/defaults/memory/path-containment.ts +21 -0
- package/src/plugins/defaults/memory/prompt-override.ts +51 -13
- package/src/plugins/defaults/memory/tools.test.ts +34 -0
- package/src/plugins/defaults/memory/tools.ts +12 -2
- package/src/plugins/defaults/memory/v2/__tests__/harness-compare.test.ts +19 -15
- package/src/plugins/defaults/memory/v2/__tests__/harness-oracle.test.ts +24 -19
- package/src/plugins/defaults/memory/v2/__tests__/harness-replay-input.test.ts +19 -15
- package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +22 -2
- package/src/plugins/defaults/memory/v2/__tests__/prompts-consolidation.test.ts +5 -3
- package/src/plugins/defaults/memory/v2/harness/oracle.ts +59 -41
- package/src/plugins/defaults/memory/v2/harness/replay-input.ts +29 -25
- package/src/plugins/defaults/memory/v2/migration.ts +46 -16
- package/src/plugins/defaults/memory/v2/prompts/consolidation.ts +4 -0
- package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +17 -15
- package/src/plugins/defaults/memory/v3/__tests__/gate.test.ts +24 -22
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +9 -5
- package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +277 -10
- package/src/plugins/defaults/memory/v3/__tests__/selection-log-store.test.ts +57 -5
- package/src/plugins/defaults/memory/v3/__tests__/shadow-integration.test.ts +9 -7
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +38 -7
- package/src/plugins/defaults/memory/v3/hot-set.test.ts +36 -18
- package/src/plugins/defaults/memory/v3/hot-set.ts +12 -14
- package/src/plugins/defaults/memory/v3/learned-edges.test.ts +47 -27
- package/src/plugins/defaults/memory/v3/learned-edges.ts +12 -14
- package/src/plugins/defaults/memory/v3/orchestrate.ts +139 -22
- package/src/plugins/defaults/memory/v3/prune.test.ts +10 -3
- package/src/plugins/defaults/memory/v3/prune.ts +17 -11
- package/src/plugins/defaults/memory/v3/selection-log-store.ts +23 -11
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +70 -53
- package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +9 -6
- package/src/plugins/external-plugin-loader.ts +15 -15
- package/src/plugins/mtime-cache.ts +47 -0
- package/src/plugins/pipeline.ts +9 -1
- package/src/plugins/plugin-execution-context.ts +44 -0
- package/src/plugins/plugin-tree-walk.ts +31 -24
- package/src/plugins/source-fingerprint.ts +3 -4
- package/src/prompts/normalize-onboarding.ts +12 -0
- package/src/prompts/persona-resolver.ts +8 -0
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +1 -1
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +6 -9
- package/src/providers/__tests__/registry-native-web-search.test.ts +4 -10
- package/src/providers/__tests__/retry-callsite.test.ts +235 -215
- package/src/providers/__tests__/satellite-connection-routing.test.ts +8 -16
- package/src/providers/atlascloud/client.ts +10 -49
- package/src/providers/baseten/client.ts +43 -0
- package/src/providers/call-site-routing.ts +26 -13
- package/src/providers/connection-resolution.ts +9 -11
- package/src/providers/fetch-provider-catalog.ts +4 -2
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +29 -4
- package/src/providers/inference/__tests__/connection-availability-keyless.test.ts +78 -0
- package/src/providers/inference/adapter-factory.ts +27 -6
- package/src/providers/inference/auth.ts +29 -1
- package/src/providers/inference/backfill.ts +61 -10
- package/src/providers/inference/connection-availability.ts +3 -2
- package/src/providers/inference/resolve-auth.ts +9 -1
- package/src/providers/model-catalog.ts +37 -0
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +24 -2
- package/src/providers/openai/api-key-validation.ts +70 -0
- package/src/providers/retry.ts +2 -0
- package/src/providers/types.ts +11 -4
- package/src/providers/vellum-model-routing.test.ts +26 -0
- package/src/providers/vellum-model-routing.ts +30 -0
- package/src/providers/voice-error-copy.ts +47 -0
- package/src/runtime/agent-wake.ts +40 -24
- package/src/runtime/for-chat-mint-registry.ts +118 -0
- package/src/runtime/reveal-nonce.ts +49 -0
- package/src/runtime/reveal-success-registry.ts +306 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +88 -3
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +161 -30
- package/src/runtime/routes/__tests__/migration-vellum-metadata-reconcile.test.ts +7 -0
- package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +15 -0
- package/src/runtime/routes/app-management-routes.ts +152 -45
- package/src/runtime/routes/app-routes.ts +16 -77
- package/src/runtime/routes/canned-message-complete.ts +16 -16
- package/src/runtime/routes/conversation-management-routes.ts +17 -14
- package/src/runtime/routes/conversation-query-routes.ts +54 -6
- package/src/runtime/routes/conversation-routes.ts +48 -2
- package/src/runtime/routes/credential-routes.ts +116 -3
- package/src/runtime/routes/email-routes.ts +17 -1
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +33 -5
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +6 -5
- package/src/runtime/routes/inference-provider-connection-routes.ts +77 -28
- package/src/runtime/routes/inference-send-routes.ts +1 -1
- package/src/runtime/routes/internal-telemetry-routes.ts +7 -6
- package/src/runtime/routes/plugins-routes.ts +37 -6
- package/src/runtime/routes/publish-routes.ts +15 -18
- package/src/runtime/routes/schedule-worker-routes.ts +9 -0
- package/src/runtime/routes/secret-routes.ts +10 -0
- package/src/runtime/routes/telemetry-routes.ts +149 -45
- package/src/schedule/__tests__/schedule-timezone.test.ts +101 -0
- package/src/schedule/__tests__/worker-watchdog.test.ts +209 -0
- package/src/schedule/schedule-store.ts +12 -1
- package/src/schedule/schedule-timezone.ts +63 -0
- package/src/schedule/scheduler.ts +100 -6
- package/src/schedule/worker-control.ts +19 -0
- package/src/security/auth-fallback-events-store.ts +7 -6
- package/src/security/secret-scanner.ts +26 -1
- package/src/services/published-app-updater.ts +6 -11
- package/src/stt/stt-stream-session.ts +36 -16
- package/src/subagent/manager.ts +34 -8
- package/src/telemetry/AGENTS.md +30 -1
- package/src/telemetry/__tests__/config-setting-snapshot.test.ts +18 -0
- package/src/telemetry/__tests__/outbox-test-harness.ts +5 -3
- package/src/telemetry/config-setting-snapshot.ts +44 -10
- package/src/telemetry/telemetry-event-sources.test.ts +124 -30
- package/src/telemetry/telemetry-event-sources.ts +137 -83
- package/src/telemetry/telemetry-events-outbox.test.ts +46 -1
- package/src/telemetry/telemetry-events-outbox.ts +60 -9
- package/src/telemetry/telemetry-wire-source.json +1 -1
- package/src/telemetry/telemetry-wire-validation.ts +39 -2
- package/src/telemetry/telemetry-wire.generated.ts +8 -0
- package/src/telemetry/tool-audit.ts +15 -9
- package/src/telemetry/tool-executed-events-store.test.ts +1 -1
- package/src/telemetry/turn-events-store.ts +20 -0
- package/src/telemetry/types.ts +45 -12
- package/src/telemetry/usage-telemetry-reporter.test.ts +294 -25
- package/src/telemetry/usage-telemetry-reporter.ts +117 -18
- package/src/telemetry/watchdog-direct-emit.test.ts +11 -3
- package/src/telemetry/watchdog-direct-emit.ts +11 -6
- package/src/tools/apps/executors.ts +11 -36
- package/src/tools/executor.ts +14 -1
- package/src/tools/host-terminal/host-shell.ts +6 -2
- package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +1 -1
- package/src/tools/network/__tests__/web-fetch-metadata.test.ts +25 -0
- package/src/tools/network/web-fetch.ts +6 -2
- package/src/tools/skills/sandbox-runner.ts +5 -2
- package/src/tools/subagent/spawn.ts +7 -11
- package/src/tools/terminal/shell.ts +5 -2
- package/src/tools/tool-manifest.ts +0 -2
- package/src/tools/tool-profiler.ts +37 -6
- package/src/tools/ui-surface/channel-variants.ts +101 -0
- package/src/tools/ui-surface/definitions.ts +23 -67
- package/src/tools/ui-surface/surface-shape-docs.ts +229 -0
- package/src/tts/__tests__/provider-adapters.test.ts +18 -0
- package/src/tts/provider-catalog.ts +2 -4
- package/src/tts/providers/deepgram-provider.ts +10 -1
- package/src/types/onboarding-context.ts +8 -0
- package/src/usage/attribution.ts +18 -112
- package/src/{config/bundled-skills/messaging/tools/gmail-mime-helpers.ts → util/mime-type.ts} +4 -1
- package/src/util/provider-error-patterns.ts +7 -1
- package/src/util/worker-process.ts +105 -0
- package/src/watcher/__tests__/telemetry.test.ts +17 -4
- package/src/watcher/telemetry.ts +5 -5
- package/src/workspace/migrations/131-drop-web-fetch-mode.ts +61 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/workspace/provider-commit-message-generator.ts +6 -4
- package/src/__tests__/app-open-proxy.test.ts +0 -67
- package/src/onboarding/onboarding-research-events-store.test.ts +0 -230
- package/src/onboarding/onboarding-research-events-store.ts +0 -104
- package/src/runtime/routes/assets/vellum-design-system.css +0 -2236
- package/src/tools/apps/definitions.ts +0 -73
- package/src/tools/apps/open-proxy.ts +0 -43
|
@@ -15,6 +15,12 @@ import type {
|
|
|
15
15
|
VoiceTurnHandle,
|
|
16
16
|
VoiceTurnOptions,
|
|
17
17
|
} from "../calls/voice-session-bridge.js";
|
|
18
|
+
import {
|
|
19
|
+
getConversationTurnTeardown,
|
|
20
|
+
resolveProcessingWaitMs,
|
|
21
|
+
VOICE_NO_SETUP_FLOWS_RULE,
|
|
22
|
+
waitForPriorTurnTeardown,
|
|
23
|
+
} from "../calls/voice-session-bridge.js";
|
|
18
24
|
import {
|
|
19
25
|
ESCALATION_CONTINUATION_CONTENT,
|
|
20
26
|
ESCALATION_PROFILE,
|
|
@@ -26,6 +32,8 @@ import {
|
|
|
26
32
|
} from "../calls/voice-triage-escalate.js";
|
|
27
33
|
import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
|
|
28
34
|
import { getConfig } from "../config/loader.js";
|
|
35
|
+
import { ABORT_WATCHDOG_MS } from "../daemon/abort-watchdog.js";
|
|
36
|
+
import { findConversation } from "../daemon/conversation-registry.js";
|
|
29
37
|
import type { TrustContext } from "../daemon/trust-context-types.js";
|
|
30
38
|
import { ensureConversationExists } from "../persistence/conversation-crud.js";
|
|
31
39
|
import {
|
|
@@ -40,6 +48,7 @@ import type {
|
|
|
40
48
|
SttStreamServerErrorEvent,
|
|
41
49
|
SttStreamServerEvent,
|
|
42
50
|
} from "../stt/types.js";
|
|
51
|
+
import { getSubagentManager } from "../subagent/index.js";
|
|
43
52
|
import { extractSpeakableSegments } from "../tts/speakable-segments.js";
|
|
44
53
|
import { getLogger } from "../util/logger.js";
|
|
45
54
|
import type {
|
|
@@ -67,6 +76,7 @@ import type {
|
|
|
67
76
|
} from "./live-voice-tts.js";
|
|
68
77
|
import {
|
|
69
78
|
type LiveVoiceClientFrame,
|
|
79
|
+
type LiveVoiceClientUpdateConfigFrame,
|
|
70
80
|
LiveVoiceProtocolErrorCode,
|
|
71
81
|
type LiveVoiceServerFramePayload,
|
|
72
82
|
} from "./protocol.js";
|
|
@@ -93,10 +103,12 @@ const SERVER_VAD_PRE_ROLL_MAX_CHUNKS = 25;
|
|
|
93
103
|
// provider keeps its own, longer, finalize fallback).
|
|
94
104
|
const FINALIZE_GRACE_MS = 1_000;
|
|
95
105
|
// Consecutive speech (ms) required before speech during assistant playback
|
|
96
|
-
// flushes it (speech_started) and cancels the turn, so a cough
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
106
|
+
// flushes it (speech_started) and cancels the turn, so a cough, a filler word,
|
|
107
|
+
// or the assistant's own TTS bleeding through imperfect browser echo
|
|
108
|
+
// cancellation cannot kill a reply mid-sentence. Mirrors the
|
|
109
|
+
// liveVoice.vad.bargeInMinSpeechMs schema default; 0 disables the guard for
|
|
110
|
+
// instant barge-in.
|
|
111
|
+
const DEFAULT_BARGE_IN_MIN_SPEECH_MS = 250;
|
|
100
112
|
// At most this many TTS segment jobs are open (provider stream started,
|
|
101
113
|
// frames not yet fully emitted) per turn: the emitting job plus one
|
|
102
114
|
// prefetching job. The prefetch buffers its chunks in memory until promoted;
|
|
@@ -119,6 +131,18 @@ export type LiveVoiceTtsStreamer = (
|
|
|
119
131
|
options: LiveVoiceTtsOptions,
|
|
120
132
|
) => Promise<LiveVoiceTtsResult>;
|
|
121
133
|
|
|
134
|
+
// Runs an interrupted live-voice turn to completion on a background subagent
|
|
135
|
+
// (true-duplex handoff, gated behind voice-duplex-handoff). The run is silent
|
|
136
|
+
// (no parent notification) until the resurface flow lands, and aborts when
|
|
137
|
+
// `signal` fires. Injected for testability; the factory wires the real
|
|
138
|
+
// SubagentManager-backed implementation.
|
|
139
|
+
export type LiveVoiceBackgroundContinuationSpawner = (args: {
|
|
140
|
+
parentConversationId: string;
|
|
141
|
+
objective: string;
|
|
142
|
+
label: string;
|
|
143
|
+
signal: AbortSignal;
|
|
144
|
+
}) => Promise<void>;
|
|
145
|
+
|
|
122
146
|
export interface LiveVoiceSessionArchiveAudioInput {
|
|
123
147
|
messageId?: string | null;
|
|
124
148
|
sessionId: string;
|
|
@@ -175,6 +199,27 @@ export interface LiveVoiceSessionOptions {
|
|
|
175
199
|
* flush in persistent mode (test hook). Defaults to `FINALIZE_GRACE_MS`.
|
|
176
200
|
*/
|
|
177
201
|
finalizeGraceMs?: number;
|
|
202
|
+
/**
|
|
203
|
+
* Spawns the background continuation for a barged-in turn when the
|
|
204
|
+
* `voice-duplex-handoff` flag is on. The factory wires the real
|
|
205
|
+
* SubagentManager-backed implementation; tests inject a stub.
|
|
206
|
+
*/
|
|
207
|
+
spawnBackgroundContinuation?: LiveVoiceBackgroundContinuationSpawner;
|
|
208
|
+
/**
|
|
209
|
+
* Returns the pending teardown promise for a conversation's most recent
|
|
210
|
+
* turn. The barge-in path awaits it before forking the background
|
|
211
|
+
* continuation so the fork snapshots history only after the interrupted
|
|
212
|
+
* turn's completed tool calls have settled in. The factory wires the
|
|
213
|
+
* bridge's `getConversationTurnTeardown`; tests inject a controllable
|
|
214
|
+
* promise to exercise the ordering.
|
|
215
|
+
*/
|
|
216
|
+
getTurnTeardown?: (conversationId: string) => Promise<void> | undefined;
|
|
217
|
+
/**
|
|
218
|
+
* Overrides the bounded wait for the interrupted turn's teardown before the
|
|
219
|
+
* background continuation forks (test hook). Defaults to the bridge's
|
|
220
|
+
* teardown budget (`resolveProcessingWaitMs`).
|
|
221
|
+
*/
|
|
222
|
+
detachTeardownSettleTimeoutMs?: number;
|
|
178
223
|
}
|
|
179
224
|
|
|
180
225
|
type LiveVoiceUtterancePhase =
|
|
@@ -260,10 +305,15 @@ interface ActiveAssistantTurn {
|
|
|
260
305
|
handle: VoiceTurnHandle | null;
|
|
261
306
|
assistantCompleted: boolean;
|
|
262
307
|
ttsDone: boolean;
|
|
263
|
-
// A tts_audio frame actually went out to the client — the
|
|
264
|
-
//
|
|
308
|
+
// A tts_audio frame actually went out to the client — latches on the first
|
|
309
|
+
// forwarded chunk so the firstTtsAudio metric is marked exactly once per turn.
|
|
265
310
|
ttsAudioStarted: boolean;
|
|
266
311
|
finalized: boolean;
|
|
312
|
+
// When this turn started from a barge-in, the interrupted request's
|
|
313
|
+
// transcript. Appended to the turn's control prompt (both legs) so the model
|
|
314
|
+
// merges it with this turn's utterance instead of treating that utterance as
|
|
315
|
+
// a fresh follow-up. Null for an ordinary (non-barge-in) turn.
|
|
316
|
+
interruptedRequest: string | null;
|
|
267
317
|
// Triage-and-escalate (Voice Mode): the front-door leg emitted [ESCALATE]
|
|
268
318
|
// and the strong "escalated" leg has taken over this same turn. Guards the
|
|
269
319
|
// front-door leg's trailing completion from finalizing the turn, and makes
|
|
@@ -285,6 +335,51 @@ interface ActiveAssistantTurn {
|
|
|
285
335
|
assistantAudioSampleRate?: number;
|
|
286
336
|
}
|
|
287
337
|
|
|
338
|
+
// Base control prompt for every live-voice turn. When a turn starts from a
|
|
339
|
+
// barge-in, the interruption merge note is appended to it (see
|
|
340
|
+
// buildInterruptionMergeNote) so the model reconciles the interrupted request
|
|
341
|
+
// with the new utterance.
|
|
342
|
+
const LIVE_VOICE_CONTROL_PROMPT =
|
|
343
|
+
"You are speaking in a local live voice session. Keep replies brief and conversational. You cannot display cards, forms, or any on-screen UI during the call — convey everything in speech. " +
|
|
344
|
+
VOICE_NO_SETUP_FLOWS_RULE;
|
|
345
|
+
|
|
346
|
+
// System-level guidance appended to a barge-in turn's control prompt so the
|
|
347
|
+
// model treats the new utterance as a continuation of the request it was cut
|
|
348
|
+
// off answering, rather than a fresh follow-up. Reaches the model only; it is
|
|
349
|
+
// not a user message and never renders as a transcript bubble.
|
|
350
|
+
function buildInterruptionMergeNote(interruptedRequest: string): string {
|
|
351
|
+
return `The user interrupted your previous, unfinished reply. Their earlier request was: "${interruptedRequest}". Treat their current message as a continuation of that request and address both together, or stay silent if they only want you to stop.`;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Objective handed to the background subagent that continues a barged-in turn.
|
|
355
|
+
// The subagent forks the live conversation, so it already sees the interrupted
|
|
356
|
+
// turn's completed tool calls in history and resumes from there. The request
|
|
357
|
+
// text is embedded so the continuation still knows what to finish even in the
|
|
358
|
+
// pre-persist window where the interrupted user message has not yet landed in
|
|
359
|
+
// the forked history.
|
|
360
|
+
function buildDuplexContinuationObjective(interruptedRequest: string): string {
|
|
361
|
+
const base =
|
|
362
|
+
"You were in the middle of responding to the user's most recent request when they interrupted you. Finish that response now. Do not repeat any tool calls whose results are already present in the conversation.";
|
|
363
|
+
return interruptedRequest.length > 0
|
|
364
|
+
? `${base} Their request was: "${interruptedRequest}".`
|
|
365
|
+
: base;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Upper bound on how long a barge-in waits for the interrupted turn's teardown
|
|
369
|
+
// to settle before giving up on the continuation. The teardown settles once the
|
|
370
|
+
// aborted turn's agent loop reaches its `finally`, which can wait out BOTH the
|
|
371
|
+
// abort-unwind watchdog and the turn-boundary commit — so bound the wait by the
|
|
372
|
+
// same budget the bridge uses to wait for a prior turn's teardown
|
|
373
|
+
// (resolveProcessingWaitMs). That lets a legitimately slow abort+commit still
|
|
374
|
+
// fork, while a genuinely wedged teardown times out — and on timeout the fork is
|
|
375
|
+
// SKIPPED (not run against stale history); see detachInterruptedTurn.
|
|
376
|
+
function defaultDetachTeardownSettleTimeoutMs(): number {
|
|
377
|
+
return resolveProcessingWaitMs(
|
|
378
|
+
getConfig().workspaceGit?.turnCommitMaxWaitMs ?? 4000,
|
|
379
|
+
ABORT_WATCHDOG_MS,
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
288
383
|
export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
289
384
|
private readonly context: LiveVoiceSessionFactoryContext;
|
|
290
385
|
private readonly resolveTranscriber: LiveVoiceStreamingTranscriberResolver;
|
|
@@ -292,6 +387,22 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
292
387
|
private readonly startVoiceTurn: LiveVoiceTurnStarter | null;
|
|
293
388
|
private readonly streamTtsAudio: LiveVoiceTtsStreamer | null;
|
|
294
389
|
private readonly archiveAudio: LiveVoiceSessionAudioArchiver | null;
|
|
390
|
+
private readonly spawnBackgroundContinuation: LiveVoiceBackgroundContinuationSpawner | null;
|
|
391
|
+
// Reads the interrupted turn's teardown promise so the barge-in path can wait
|
|
392
|
+
// for it to settle before forking the continuation (voice-duplex-handoff).
|
|
393
|
+
private readonly getTurnTeardown:
|
|
394
|
+
| ((conversationId: string) => Promise<void> | undefined)
|
|
395
|
+
| null;
|
|
396
|
+
private readonly detachTeardownSettleTimeoutMs: number;
|
|
397
|
+
// Abort handles for background continuations started when a barge-in detached
|
|
398
|
+
// an interrupted turn (voice-duplex-handoff). Each controller is registered
|
|
399
|
+
// synchronously before its spawn, so interrupt()/close() abort a continuation
|
|
400
|
+
// even if a stop lands while it is still spawning.
|
|
401
|
+
private readonly detachControllers = new Set<AbortController>();
|
|
402
|
+
// Bumped whenever a stop (interrupt/close) fires. A barge-in captures this
|
|
403
|
+
// before its async teardown; if it has changed by the time the detach would
|
|
404
|
+
// spawn, a stop landed during the gap and the continuation is not started.
|
|
405
|
+
private detachStopGeneration = 0;
|
|
295
406
|
private readonly emitMetrics: boolean;
|
|
296
407
|
private readonly metrics: LiveVoiceMetricsCollector;
|
|
297
408
|
private readonly createTurnId: () => string;
|
|
@@ -306,7 +417,9 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
306
417
|
// Energy gate for server-VAD speech classification; undefined defers to
|
|
307
418
|
// DEFAULT_SPEECH_ENERGY_THRESHOLD.
|
|
308
419
|
private readonly speechEnergyThreshold: number | undefined;
|
|
309
|
-
|
|
420
|
+
// Mutable so a mid-session `update_config` frame can retune "interrupt
|
|
421
|
+
// sensitivity" live (see applyConfigUpdate).
|
|
422
|
+
private bargeInMinSpeechMs: number;
|
|
310
423
|
// Sustained-speech barge-in guard, armed at speech onset while the
|
|
311
424
|
// assistant turn is audibly speaking: consecutive speech-chunk duration
|
|
312
425
|
// accumulates until it reaches bargeInMinSpeechMs, then the deferred
|
|
@@ -326,6 +439,11 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
326
439
|
// the chunk's PCM duration; zeroed whenever the client flushes playback
|
|
327
440
|
// (speech_started, turn_cancelled, interrupt, close).
|
|
328
441
|
private assistantPlaybackTailUntilMs = 0;
|
|
442
|
+
// Set when barge-in cancels an in-flight turn: the interrupted request's
|
|
443
|
+
// transcript, carried into the next turn so the model merges the two.
|
|
444
|
+
// Consumed (and cleared) when that turn launches; cleared if the barge-in
|
|
445
|
+
// utterance is discarded, so it can never attach to a later, unrelated turn.
|
|
446
|
+
private pendingInterruptedRequest: string | null = null;
|
|
329
447
|
private readonly maxPendingAudioBytes: number;
|
|
330
448
|
// Set on VAD speech onset; consumed when the first speech chunk is routed
|
|
331
449
|
// to an utterance so the metric lands on the right turn.
|
|
@@ -375,6 +493,12 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
375
493
|
this.startVoiceTurn = options.startVoiceTurn ?? null;
|
|
376
494
|
this.streamTtsAudio = options.streamTtsAudio ?? null;
|
|
377
495
|
this.archiveAudio = options.archiveAudio ?? null;
|
|
496
|
+
this.spawnBackgroundContinuation =
|
|
497
|
+
options.spawnBackgroundContinuation ?? null;
|
|
498
|
+
this.getTurnTeardown = options.getTurnTeardown ?? null;
|
|
499
|
+
this.detachTeardownSettleTimeoutMs =
|
|
500
|
+
options.detachTeardownSettleTimeoutMs ??
|
|
501
|
+
defaultDetachTeardownSettleTimeoutMs();
|
|
378
502
|
this.emitMetrics = options.emitMetrics ?? false;
|
|
379
503
|
this.createTurnId = options.createTurnId ?? randomUUID;
|
|
380
504
|
this.conversationId =
|
|
@@ -386,12 +510,23 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
386
510
|
...(options.metricsClock ? { clock: options.metricsClock } : {}),
|
|
387
511
|
});
|
|
388
512
|
this.speechEnergyThreshold = options.speechEnergyThreshold;
|
|
513
|
+
// Precedence for the two sensitivity knobs: per-session start-frame
|
|
514
|
+
// override (the client's user setting) > daemon `liveVoice.vad` config
|
|
515
|
+
// (seeded into `options` by the factory) > in-code default.
|
|
389
516
|
this.bargeInMinSpeechMs =
|
|
390
|
-
|
|
517
|
+
context.startFrame.bargeInMinSpeechMs ??
|
|
518
|
+
options.bargeInMinSpeechMs ??
|
|
519
|
+
DEFAULT_BARGE_IN_MIN_SPEECH_MS;
|
|
391
520
|
this.finalizeGraceMs = options.finalizeGraceMs ?? FINALIZE_GRACE_MS;
|
|
521
|
+
const turnDetectorConfig: TurnDetectorConfig = {
|
|
522
|
+
...(options.turnDetectorConfig ?? {}),
|
|
523
|
+
...(context.startFrame.silenceThresholdMs !== undefined
|
|
524
|
+
? { silenceThresholdMs: context.startFrame.silenceThresholdMs }
|
|
525
|
+
: {}),
|
|
526
|
+
};
|
|
392
527
|
this.turnDetector =
|
|
393
528
|
context.startFrame.turnDetection === "server_vad"
|
|
394
|
-
? new MediaTurnDetector(
|
|
529
|
+
? new MediaTurnDetector(turnDetectorConfig, {
|
|
395
530
|
onTurnStart: () => this.handleVadSpeechStart(),
|
|
396
531
|
onTurnEnd: (reason) => this.handleVadUtteranceEnd(reason),
|
|
397
532
|
})
|
|
@@ -461,6 +596,25 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
461
596
|
return;
|
|
462
597
|
case "start":
|
|
463
598
|
return;
|
|
599
|
+
case "update_config":
|
|
600
|
+
this.applyConfigUpdate(frame);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Apply a mid-session `update_config` frame: retune the live turn detector's
|
|
607
|
+
* pause ("pause before reply") and/or the barge-in guard ("interrupt
|
|
608
|
+
* sensitivity") without reconnecting. Each field is optional and independent;
|
|
609
|
+
* changes take effect from the next utterance. A no-op on manual (non-
|
|
610
|
+
* server_vad) sessions, which have no turn detector.
|
|
611
|
+
*/
|
|
612
|
+
private applyConfigUpdate(frame: LiveVoiceClientUpdateConfigFrame): void {
|
|
613
|
+
if (frame.silenceThresholdMs !== undefined) {
|
|
614
|
+
this.turnDetector?.setSilenceThresholdMs(frame.silenceThresholdMs);
|
|
615
|
+
}
|
|
616
|
+
if (frame.bargeInMinSpeechMs !== undefined) {
|
|
617
|
+
this.bargeInMinSpeechMs = frame.bargeInMinSpeechMs;
|
|
464
618
|
}
|
|
465
619
|
}
|
|
466
620
|
|
|
@@ -477,6 +631,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
477
631
|
this.state = "closed";
|
|
478
632
|
this.turnDetector?.dispose();
|
|
479
633
|
this.stopSessionTranscriber();
|
|
634
|
+
this.abortDetachedRuns();
|
|
480
635
|
await this.cancelAssistantTurn("session_closed");
|
|
481
636
|
if (shouldEmitSessionEndMetrics) {
|
|
482
637
|
await this.emitSessionEndMetrics();
|
|
@@ -867,12 +1022,14 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
867
1022
|
}
|
|
868
1023
|
|
|
869
1024
|
// VAD speech onset. Contract: speech_started tells the client to flush
|
|
870
|
-
// tail playback immediately; barge-in then cancels
|
|
871
|
-
//
|
|
872
|
-
//
|
|
873
|
-
//
|
|
874
|
-
//
|
|
875
|
-
//
|
|
1025
|
+
// tail playback immediately; barge-in then cancels any in-flight,
|
|
1026
|
+
// non-finalized turn — including a pre-TTS "thinking" turn whose reply is
|
|
1027
|
+
// still being generated, so a user can cut in before the assistant starts
|
|
1028
|
+
// talking (JARVIS-1266). Speaking over a thinking or audibly speaking turn
|
|
1029
|
+
// is deferred behind the same sustained-speech guard, so a cough or noise
|
|
1030
|
+
// blip cannot kill an unspoken reply or clip a spoken one; sustained speech
|
|
1031
|
+
// aborts the turn. Onset while listening keeps the instant speech_started
|
|
1032
|
+
// (turn-taking latency is untouched).
|
|
876
1033
|
private handleVadSpeechStart(): void {
|
|
877
1034
|
if (this.isClosed || this.state === "failed") {
|
|
878
1035
|
return;
|
|
@@ -881,25 +1038,27 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
881
1038
|
this.vadSpeechStartPending = true;
|
|
882
1039
|
|
|
883
1040
|
const turn = this.activeAssistantTurn;
|
|
884
|
-
|
|
885
|
-
|
|
1041
|
+
// Any in-flight, non-finalized turn is interruptible, whether it is still
|
|
1042
|
+
// "thinking" (pre-TTS) or audibly speaking, so a user can cut in before the
|
|
1043
|
+
// assistant starts talking.
|
|
1044
|
+
const bargeableTurn = turn && !turn.finalized ? turn : null;
|
|
886
1045
|
// The client can still be draining audible playback after tts_done
|
|
887
1046
|
// (the turn is already cleared server-side) — that tail deserves the
|
|
888
1047
|
// same guard, or a noise blip clips the reply's last words.
|
|
889
1048
|
const drainingPlayback = Date.now() < this.assistantPlaybackTailUntilMs;
|
|
890
1049
|
|
|
891
|
-
if ((
|
|
1050
|
+
if ((bargeableTurn || drainingPlayback) && this.bargeInMinSpeechMs > 0) {
|
|
892
1051
|
// Onset audio keeps flowing into the cycle/pre-roll while the guard
|
|
893
1052
|
// accumulates (trackBargeInGuard), so no speech is lost either way.
|
|
894
|
-
this.pendingBargeIn = { turn:
|
|
1053
|
+
this.pendingBargeIn = { turn: bargeableTurn, speechMs: 0 };
|
|
895
1054
|
return;
|
|
896
1055
|
}
|
|
897
1056
|
|
|
898
1057
|
this.pendingBargeIn = null;
|
|
899
1058
|
this.assistantPlaybackTailUntilMs = 0;
|
|
900
1059
|
void this.sendFrame({ type: "speech_started" });
|
|
901
|
-
if (
|
|
902
|
-
this.bargeIn(
|
|
1060
|
+
if (bargeableTurn) {
|
|
1061
|
+
this.bargeIn(bargeableTurn);
|
|
903
1062
|
}
|
|
904
1063
|
}
|
|
905
1064
|
|
|
@@ -938,8 +1097,25 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
938
1097
|
// do not collide with it in the collector. turn_cancelled flushes
|
|
939
1098
|
// client playback, so the drain estimate resets with it.
|
|
940
1099
|
this.assistantPlaybackTailUntilMs = 0;
|
|
1100
|
+
// Carry the interrupted request into the next turn so it merges with the
|
|
1101
|
+
// barge-in utterance rather than being answered as a fresh follow-up.
|
|
1102
|
+
const interruptedRequest = turn.utterance.finalTranscriptSegments
|
|
1103
|
+
.join(" ")
|
|
1104
|
+
.trim();
|
|
1105
|
+
this.pendingInterruptedRequest =
|
|
1106
|
+
interruptedRequest.length > 0 ? interruptedRequest : null;
|
|
941
1107
|
turn.abortController.abort();
|
|
942
1108
|
this.metrics.markBargeIn(turn.turnId);
|
|
1109
|
+
// Capture the interrupted turn's teardown promise synchronously, before the
|
|
1110
|
+
// barge-in utterance's own startVoiceTurn overwrites the bridge's
|
|
1111
|
+
// per-conversation entry (that utterance is not transcribed yet, so its turn
|
|
1112
|
+
// has not started — this read is race-free). The detach awaits it so the
|
|
1113
|
+
// fork snapshots history only after this turn's completed tool calls have
|
|
1114
|
+
// settled in (see detachInterruptedTurn).
|
|
1115
|
+
const teardownWait = this.getTurnTeardown?.(this.conversationId);
|
|
1116
|
+
// Snapshot the stop generation before the async teardown: a stop that lands
|
|
1117
|
+
// during it must cancel the pending detach (checked in detachInterruptedTurn).
|
|
1118
|
+
const stopGeneration = this.detachStopGeneration;
|
|
943
1119
|
void (async () => {
|
|
944
1120
|
await this.finishMetricsTurn(
|
|
945
1121
|
turn.utterance,
|
|
@@ -949,9 +1125,118 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
949
1125
|
);
|
|
950
1126
|
await this.sendFrame({ type: "turn_cancelled", turnId: turn.turnId });
|
|
951
1127
|
await this.cancelAssistantTurn("barge_in");
|
|
1128
|
+
// Keep the interrupted turn's work alive on a background subagent
|
|
1129
|
+
// (voice-duplex-handoff); the detach waits for its teardown to settle the
|
|
1130
|
+
// partial into history before forking.
|
|
1131
|
+
this.detachInterruptedTurn(turn, stopGeneration, teardownWait);
|
|
952
1132
|
})().catch(() => {});
|
|
953
1133
|
}
|
|
954
1134
|
|
|
1135
|
+
// True-duplex handoff (voice-duplex-handoff): keep a barged-in turn's work
|
|
1136
|
+
// alive by continuing it on a background subagent instead of discarding it.
|
|
1137
|
+
// Waits for the interrupted turn's bridge teardown (captured at barge-in) to
|
|
1138
|
+
// settle before forking, so its partial — including any completed tool calls —
|
|
1139
|
+
// is already in the conversation the subagent forks from and a side-effecting
|
|
1140
|
+
// continuation cannot repeat a call the interrupted turn already ran.
|
|
1141
|
+
// Resurfacing the subagent's result is a follow-up; for now it runs silently
|
|
1142
|
+
// and a later stop/interrupt aborts it.
|
|
1143
|
+
private detachInterruptedTurn(
|
|
1144
|
+
turn: ActiveAssistantTurn,
|
|
1145
|
+
stopGeneration: number,
|
|
1146
|
+
teardownWait: Promise<void> | undefined,
|
|
1147
|
+
): void {
|
|
1148
|
+
const spawn = this.spawnBackgroundContinuation;
|
|
1149
|
+
if (
|
|
1150
|
+
!spawn ||
|
|
1151
|
+
this.isClosed ||
|
|
1152
|
+
// The model already finished generating (barge-in during TTS playback of a
|
|
1153
|
+
// complete reply): there is nothing to continue, so a continuation would
|
|
1154
|
+
// just re-do a finished answer.
|
|
1155
|
+
turn.assistantCompleted ||
|
|
1156
|
+
// A stop (interrupt/close) landed during the barge-in teardown: honor it
|
|
1157
|
+
// and do not start the continuation.
|
|
1158
|
+
this.detachStopGeneration !== stopGeneration ||
|
|
1159
|
+
!isAssistantFeatureFlagEnabled("voice-duplex-handoff", getConfig())
|
|
1160
|
+
) {
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
// Embed the interrupted request in the objective so the continuation knows
|
|
1164
|
+
// what to finish even if the forked history predates the user message being
|
|
1165
|
+
// persisted (barge-in can fire while the turn is still acquiring the lock).
|
|
1166
|
+
const interruptedRequest = turn.utterance.finalTranscriptSegments
|
|
1167
|
+
.join(" ")
|
|
1168
|
+
.trim();
|
|
1169
|
+
// Register the abort handle synchronously so a stop that lands while the
|
|
1170
|
+
// continuation is still spawning still aborts it (abortDetachedRuns fires
|
|
1171
|
+
// controller.abort(), which the spawn's signal wiring honors).
|
|
1172
|
+
const controller = new AbortController();
|
|
1173
|
+
this.detachControllers.add(controller);
|
|
1174
|
+
void (async () => {
|
|
1175
|
+
try {
|
|
1176
|
+
// Wait for the interrupted turn's teardown to settle its partial into
|
|
1177
|
+
// conversation history before the fork snapshots it. This is
|
|
1178
|
+
// turn-scoped — it waits for THIS turn only (captured at barge-in), not
|
|
1179
|
+
// the conversation's overall idle state, so the interrupting turn's own
|
|
1180
|
+
// work still proceeds in parallel (conversation.waitForIdle would block
|
|
1181
|
+
// on it and defeat the background handoff).
|
|
1182
|
+
if (teardownWait) {
|
|
1183
|
+
let settled = false;
|
|
1184
|
+
try {
|
|
1185
|
+
settled = await waitForPriorTurnTeardown(
|
|
1186
|
+
teardownWait,
|
|
1187
|
+
this.detachTeardownSettleTimeoutMs,
|
|
1188
|
+
controller.signal,
|
|
1189
|
+
);
|
|
1190
|
+
} catch {
|
|
1191
|
+
// Aborted mid-wait (stop/interrupt); handled by the skip below.
|
|
1192
|
+
}
|
|
1193
|
+
// Fork only once the teardown has settled. On timeout (false) or
|
|
1194
|
+
// abort (throw) we cannot guarantee the fork would see the interrupted
|
|
1195
|
+
// turn's completed tool calls, so skip the continuation rather than
|
|
1196
|
+
// snapshot stale history and risk repeating a side effect — the bridge
|
|
1197
|
+
// refuses the next turn on an unsettled teardown for the same reason.
|
|
1198
|
+
// The continuation is best-effort, so a rare dropped one is the safe
|
|
1199
|
+
// trade.
|
|
1200
|
+
if (!settled) {
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
if (controller.signal.aborted || this.isClosed) {
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
await spawn({
|
|
1208
|
+
parentConversationId: this.conversationId,
|
|
1209
|
+
objective: buildDuplexContinuationObjective(interruptedRequest),
|
|
1210
|
+
label: `voice-continue-${turn.turnId}`,
|
|
1211
|
+
signal: controller.signal,
|
|
1212
|
+
});
|
|
1213
|
+
} catch (err) {
|
|
1214
|
+
// A stop/interrupt aborts via the signal; that rejection is expected.
|
|
1215
|
+
if (!controller.signal.aborted) {
|
|
1216
|
+
log.warn(
|
|
1217
|
+
{ err, turnId: turn.turnId },
|
|
1218
|
+
"Voice duplex handoff continuation failed",
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
} finally {
|
|
1222
|
+
this.detachControllers.delete(controller);
|
|
1223
|
+
}
|
|
1224
|
+
})();
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// Abort every background continuation this session started and drop its
|
|
1228
|
+
// handle. A client interrupt or session close is a hard stop for detached
|
|
1229
|
+
// work; the continuation's own `.finally` removes it from the set too.
|
|
1230
|
+
private abortDetachedRuns(): void {
|
|
1231
|
+
// Bump the generation so a barge-in whose async teardown is still in flight
|
|
1232
|
+
// (its detach not yet spawned) sees the stop and skips the continuation.
|
|
1233
|
+
this.detachStopGeneration += 1;
|
|
1234
|
+
for (const controller of this.detachControllers) {
|
|
1235
|
+
controller.abort();
|
|
1236
|
+
}
|
|
1237
|
+
this.detachControllers.clear();
|
|
1238
|
+
}
|
|
1239
|
+
|
|
955
1240
|
// VAD closed the utterance — the analog of ptt_release: emit
|
|
956
1241
|
// utterance_end, then run the standard release path.
|
|
957
1242
|
private handleVadUtteranceEnd(reason: "silence" | "max-duration"): void {
|
|
@@ -1384,6 +1669,12 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1384
1669
|
this.assistantPlaybackTailUntilMs = 0;
|
|
1385
1670
|
this.takeVadPreRoll();
|
|
1386
1671
|
this.vadPendingTurnEnd = null;
|
|
1672
|
+
// A client interrupt is a hard reset: any barge-in merge context waiting for
|
|
1673
|
+
// the next turn is now stale (the interrupted utterance may be discarded
|
|
1674
|
+
// without ever reaching finalizePendingUtterance).
|
|
1675
|
+
this.pendingInterruptedRequest = null;
|
|
1676
|
+
// ...and it hard-stops any detached background continuations.
|
|
1677
|
+
this.abortDetachedRuns();
|
|
1387
1678
|
const utterance = this.currentUtterance;
|
|
1388
1679
|
this.stopSessionTranscriber();
|
|
1389
1680
|
if (utterance) {
|
|
@@ -1456,6 +1747,24 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1456
1747
|
return;
|
|
1457
1748
|
}
|
|
1458
1749
|
|
|
1750
|
+
// Consume any pending barge-in merge context for this turn (one barge-in
|
|
1751
|
+
// feeds exactly the next launched turn).
|
|
1752
|
+
const interruptedRequest = this.pendingInterruptedRequest;
|
|
1753
|
+
this.pendingInterruptedRequest = null;
|
|
1754
|
+
await this.launchAssistantTurn(utterance, content, { interruptedRequest });
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
// Build the ActiveAssistantTurn for a released utterance and drive its model
|
|
1758
|
+
// leg.
|
|
1759
|
+
private async launchAssistantTurn(
|
|
1760
|
+
utterance: UtteranceCycle,
|
|
1761
|
+
content: string,
|
|
1762
|
+
opts?: {
|
|
1763
|
+
// Set on a barge-in follow-up turn: the interrupted request's transcript,
|
|
1764
|
+
// appended to the turn's control prompt so the model merges the two.
|
|
1765
|
+
interruptedRequest?: string | null;
|
|
1766
|
+
},
|
|
1767
|
+
): Promise<void> {
|
|
1459
1768
|
utterance.assistantTurnStarted = true;
|
|
1460
1769
|
const token = Symbol("live-voice-assistant-turn");
|
|
1461
1770
|
const turnId = this.ensureTurnId(utterance);
|
|
@@ -1471,6 +1780,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1471
1780
|
ttsDone: false,
|
|
1472
1781
|
ttsAudioStarted: false,
|
|
1473
1782
|
finalized: false,
|
|
1783
|
+
interruptedRequest: opts?.interruptedRequest ?? null,
|
|
1474
1784
|
escalationHandedOff: false,
|
|
1475
1785
|
ttsBuffer: "",
|
|
1476
1786
|
ttsSegmentEnqueued: false,
|
|
@@ -1551,7 +1861,14 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1551
1861
|
return;
|
|
1552
1862
|
}
|
|
1553
1863
|
this.markFirstAssistantDelta(utterance, turnId);
|
|
1554
|
-
|
|
1864
|
+
// Same send-time abort gate as the default-leg delta path: a front-door
|
|
1865
|
+
// delta queued behind a backed-up outbound frame must not be written
|
|
1866
|
+
// once barge-in aborts the turn. Escalation aborts the front-door handle,
|
|
1867
|
+
// not this turn's controller, so legitimate front-door text still sends.
|
|
1868
|
+
void this.sendFrame(
|
|
1869
|
+
{ type: "assistant_text_delta", text: chunk },
|
|
1870
|
+
() => !activeTurn.abortController.signal.aborted && !this.isClosed,
|
|
1871
|
+
);
|
|
1555
1872
|
this.bufferAssistantTextForTts(token, chunk);
|
|
1556
1873
|
};
|
|
1557
1874
|
|
|
@@ -1596,9 +1913,11 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1596
1913
|
assistantMessageChannel: "vellum",
|
|
1597
1914
|
userMessageInterface: "macos",
|
|
1598
1915
|
assistantMessageInterface: "macos",
|
|
1599
|
-
voiceControlPrompt:
|
|
1600
|
-
|
|
1601
|
-
|
|
1916
|
+
voiceControlPrompt: activeTurn.interruptedRequest
|
|
1917
|
+
? `${LIVE_VOICE_CONTROL_PROMPT}\n\n${buildInterruptionMergeNote(
|
|
1918
|
+
activeTurn.interruptedRequest,
|
|
1919
|
+
)}`
|
|
1920
|
+
: LIVE_VOICE_CONTROL_PROMPT,
|
|
1602
1921
|
content: leg.content,
|
|
1603
1922
|
isInbound: true,
|
|
1604
1923
|
signal: activeTurn.abortController.signal,
|
|
@@ -1622,10 +1941,21 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1622
1941
|
return;
|
|
1623
1942
|
}
|
|
1624
1943
|
this.markFirstAssistantDelta(utterance, turnId);
|
|
1625
|
-
void this.sendFrame(
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1944
|
+
void this.sendFrame(
|
|
1945
|
+
{
|
|
1946
|
+
type: "assistant_text_delta",
|
|
1947
|
+
text: msg.text,
|
|
1948
|
+
},
|
|
1949
|
+
// Re-check at send time (mirrors the tts_audio path): a delta
|
|
1950
|
+
// already queued behind a backed-up outbound frame must not be
|
|
1951
|
+
// written once barge-in has aborted the turn, or the cancelled
|
|
1952
|
+
// reply's text leaks ahead of turn_cancelled. Key off this turn's
|
|
1953
|
+
// own abort signal — a normal message_complete finalizes and
|
|
1954
|
+
// clears activeAssistantTurn while trailing deltas may still be
|
|
1955
|
+
// draining, so an activeAssistantTurn-based guard would drop them.
|
|
1956
|
+
() =>
|
|
1957
|
+
!activeTurn.abortController.signal.aborted && !this.isClosed,
|
|
1958
|
+
);
|
|
1629
1959
|
this.bufferAssistantTextForTts(token, msg.text);
|
|
1630
1960
|
},
|
|
1631
1961
|
message_complete: (msg) => {
|
|
@@ -1798,7 +2128,14 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1798
2128
|
private isActiveAssistantTurn(token: symbol): boolean {
|
|
1799
2129
|
const activeTurn = this.activeAssistantTurn;
|
|
1800
2130
|
return (
|
|
1801
|
-
activeTurn?.token === token &&
|
|
2131
|
+
activeTurn?.token === token &&
|
|
2132
|
+
!activeTurn.finalized &&
|
|
2133
|
+
// Barge-in aborts synchronously but finalizes through an async
|
|
2134
|
+
// cancelAssistantTurn chain; the abort makes the turn dead at once so a
|
|
2135
|
+
// rejected startVoiceTurn or a trailing onError in that window does not
|
|
2136
|
+
// treat it as live (and emit a stray error frame or double-finalize).
|
|
2137
|
+
!activeTurn.abortController.signal.aborted &&
|
|
2138
|
+
!this.isClosed
|
|
1802
2139
|
);
|
|
1803
2140
|
}
|
|
1804
2141
|
|
|
@@ -1808,6 +2145,10 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1808
2145
|
activeTurn?.token === token &&
|
|
1809
2146
|
!activeTurn.assistantCompleted &&
|
|
1810
2147
|
!activeTurn.finalized &&
|
|
2148
|
+
// Fence a late first assistant_text_delta once barge-in aborts a pre-TTS
|
|
2149
|
+
// turn, before its async teardown finalizes — mirrors isForwardingTts so
|
|
2150
|
+
// no cancelled-turn text leaks after turn_cancelled.
|
|
2151
|
+
!activeTurn.abortController.signal.aborted &&
|
|
1811
2152
|
!this.isClosed
|
|
1812
2153
|
);
|
|
1813
2154
|
}
|
|
@@ -2072,9 +2413,13 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
2072
2413
|
if (activeTurn?.token !== token) {
|
|
2073
2414
|
return;
|
|
2074
2415
|
}
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
)
|
|
2416
|
+
// Only retain the assistant TTS audio when it will be archived (see
|
|
2417
|
+
// collectUserAudio); the mime/sample-rate are cheap and left unconditional.
|
|
2418
|
+
if (this.archiveAudio) {
|
|
2419
|
+
activeTurn.assistantAudioChunks.push(
|
|
2420
|
+
Buffer.from(chunk.dataBase64, "base64"),
|
|
2421
|
+
);
|
|
2422
|
+
}
|
|
2078
2423
|
activeTurn.assistantAudioMimeType = chunk.contentType;
|
|
2079
2424
|
activeTurn.assistantAudioSampleRate = chunk.sampleRate;
|
|
2080
2425
|
job.frames = job.frames.then(async () => {
|
|
@@ -2087,10 +2432,10 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
2087
2432
|
},
|
|
2088
2433
|
() => this.isForwardingTts(token),
|
|
2089
2434
|
);
|
|
2090
|
-
//
|
|
2091
|
-
//
|
|
2092
|
-
//
|
|
2093
|
-
// from
|
|
2435
|
+
// Skip a frame that wasn't actually written — a backed-up outbound
|
|
2436
|
+
// queue hasn't reached the client, so it must not extend the
|
|
2437
|
+
// playback-tail estimate or latch first-audio state. Token match keeps
|
|
2438
|
+
// a stale turn's late send from latching a newer turn.
|
|
2094
2439
|
if (!sent) {
|
|
2095
2440
|
return;
|
|
2096
2441
|
}
|
|
@@ -2114,7 +2459,12 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
2114
2459
|
}
|
|
2115
2460
|
|
|
2116
2461
|
private collectUserAudio(utterance: UtteranceCycle, chunk: Buffer): void {
|
|
2117
|
-
|
|
2462
|
+
// Only retain the raw audio when it will actually be archived — otherwise
|
|
2463
|
+
// buffering a whole turn's PCM is dead memory. The first-audio metric is
|
|
2464
|
+
// independent of archiving, so it stays unconditional.
|
|
2465
|
+
if (this.archiveAudio) {
|
|
2466
|
+
utterance.userAudioChunks.push(Buffer.from(chunk));
|
|
2467
|
+
}
|
|
2118
2468
|
this.markUtteranceMetric(utterance, "firstAudioAtMs", (turnId) =>
|
|
2119
2469
|
this.metrics.markFirstAudio(turnId),
|
|
2120
2470
|
);
|
|
@@ -2229,6 +2579,13 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
2229
2579
|
utterance: UtteranceCycle,
|
|
2230
2580
|
reason: string,
|
|
2231
2581
|
): Promise<void> {
|
|
2582
|
+
// An utterance that finalizes here never became a turn (empty transcript,
|
|
2583
|
+
// client interrupt, transcriber close, error), so it ends the window a
|
|
2584
|
+
// barge-in's merge context was waiting to attach to. Drop that context so
|
|
2585
|
+
// it can't leak into a later, unrelated turn. The barged turn itself
|
|
2586
|
+
// finalizes through finalizeAssistantTurn, not here, so this never clears a
|
|
2587
|
+
// request that the barge-in follow-up turn is still about to consume.
|
|
2588
|
+
this.pendingInterruptedRequest = null;
|
|
2232
2589
|
utterance.completed = true;
|
|
2233
2590
|
const turnId = utterance.turnId;
|
|
2234
2591
|
if (!turnId) {
|
|
@@ -2509,7 +2866,8 @@ export function createLiveVoiceSession(
|
|
|
2509
2866
|
// unchanged. Optional-chained
|
|
2510
2867
|
// because hand-built test configs may predate the liveVoice namespace;
|
|
2511
2868
|
// absent config falls through to the in-code defaults.
|
|
2512
|
-
const
|
|
2869
|
+
const liveVoiceConfig = getConfig().liveVoice;
|
|
2870
|
+
const vadConfig = liveVoiceConfig?.vad;
|
|
2513
2871
|
return new LiveVoiceSession(context, {
|
|
2514
2872
|
...options,
|
|
2515
2873
|
turnDetectorConfig:
|
|
@@ -2533,14 +2891,59 @@ export function createLiveVoiceSession(
|
|
|
2533
2891
|
options.streamTtsAudio === undefined
|
|
2534
2892
|
? defaultStreamLiveVoiceTtsAudio
|
|
2535
2893
|
: options.streamTtsAudio,
|
|
2894
|
+
spawnBackgroundContinuation:
|
|
2895
|
+
options.spawnBackgroundContinuation ?? defaultSpawnBackgroundContinuation,
|
|
2896
|
+
getTurnTeardown: options.getTurnTeardown ?? getConversationTurnTeardown,
|
|
2897
|
+
// Off by default (see the `liveVoice.archiveAudio` schema): voice turns
|
|
2898
|
+
// persist only their transcribed text, so the recorded audio never lands
|
|
2899
|
+
// as an attachment on the conversation messages. Enable via config for
|
|
2900
|
+
// playback/debugging. An explicit option (incl. `null`) always wins — the
|
|
2901
|
+
// test seam and any future caller override.
|
|
2536
2902
|
archiveAudio:
|
|
2537
2903
|
options.archiveAudio === undefined
|
|
2538
|
-
?
|
|
2904
|
+
? liveVoiceConfig?.archiveAudio
|
|
2905
|
+
? defaultArchiveLiveVoiceAudio
|
|
2906
|
+
: null
|
|
2539
2907
|
: options.archiveAudio,
|
|
2540
2908
|
emitMetrics: options.emitMetrics ?? true,
|
|
2541
2909
|
});
|
|
2542
2910
|
}
|
|
2543
2911
|
|
|
2912
|
+
// Forks the live voice conversation into a background subagent that continues
|
|
2913
|
+
// the interrupted turn. The fork inherits the conversation's current messages
|
|
2914
|
+
// (which include the interrupted turn's completed tool calls after teardown),
|
|
2915
|
+
// so it resumes without repeating them. Uses spawnAndAwait (synchronous mode)
|
|
2916
|
+
// so the terminal parent-notification is skipped — the continuation stays
|
|
2917
|
+
// silent until the resurface flow lands, and this call ignores its result. The
|
|
2918
|
+
// `signal` aborts the child on a stop/interrupt.
|
|
2919
|
+
async function defaultSpawnBackgroundContinuation(args: {
|
|
2920
|
+
parentConversationId: string;
|
|
2921
|
+
objective: string;
|
|
2922
|
+
label: string;
|
|
2923
|
+
signal: AbortSignal;
|
|
2924
|
+
}): Promise<void> {
|
|
2925
|
+
const parentConversation = findConversation(args.parentConversationId);
|
|
2926
|
+
if (!parentConversation) {
|
|
2927
|
+
throw new Error(
|
|
2928
|
+
`Cannot detach interrupted voice turn: conversation ${args.parentConversationId} is not resident.`,
|
|
2929
|
+
);
|
|
2930
|
+
}
|
|
2931
|
+
await getSubagentManager().spawnAndAwait(
|
|
2932
|
+
{
|
|
2933
|
+
parentConversationId: args.parentConversationId,
|
|
2934
|
+
label: args.label,
|
|
2935
|
+
objective: args.objective,
|
|
2936
|
+
fork: true,
|
|
2937
|
+
sendResultToUser: false,
|
|
2938
|
+
parentMessages: [...parentConversation.messages],
|
|
2939
|
+
parentSystemPrompt: parentConversation.getCurrentSystemPrompt(),
|
|
2940
|
+
},
|
|
2941
|
+
// No client-facing events: the continuation is silent until resurface.
|
|
2942
|
+
() => {},
|
|
2943
|
+
{ signal: args.signal },
|
|
2944
|
+
);
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2544
2947
|
async function defaultResolveStreamingTranscriber(
|
|
2545
2948
|
options: ResolveStreamingTranscriberOptions,
|
|
2546
2949
|
): Promise<StreamingTranscriber | null> {
|