@vellumai/assistant 0.12.1-staging.1 → 0.12.1
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/AGENTS.md +0 -4
- package/ARCHITECTURE.md +12 -11
- package/docs/architecture/turn-actor.md +8 -15
- package/openapi.yaml +1 -291
- package/package.json +1 -1
- package/src/__tests__/always-loaded-tools-guard.test.ts +10 -18
- package/src/__tests__/app-compiler.test.ts +0 -6
- package/src/__tests__/app-control-tool-schemas.test.ts +2 -5
- package/src/__tests__/app-dir-path-guard.test.ts +3 -1
- package/src/__tests__/app-source-watcher.test.ts +218 -0
- package/src/{calls/__tests__ → __tests__}/call-controller.test.ts +82 -503
- package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +2 -5
- package/src/__tests__/computer-use-tools.test.ts +5 -17
- package/src/__tests__/config-schema-cmd.test.ts +2 -2
- package/src/__tests__/config-schema.test.ts +29 -28
- package/src/__tests__/config-watcher-skill-reseed.test.ts +359 -0
- package/src/__tests__/config-watcher.test.ts +8 -23
- package/src/__tests__/connection-model-compat.test.ts +2 -2
- package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
- package/src/__tests__/conversation-pairing.test.ts +0 -26
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +10 -4
- package/src/__tests__/conversation-queue.test.ts +207 -48
- package/src/__tests__/conversation-retry-discard.test.ts +1 -54
- package/src/__tests__/conversation-skill-tools.test.ts +0 -27
- package/src/__tests__/conversation-surfaces-app-control.test.ts +3 -8
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +0 -67
- package/src/__tests__/conversation-title-service.test.ts +10 -14
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +5 -3
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +0 -51
- package/src/__tests__/credential-record-retire.test.ts +1 -171
- package/src/__tests__/credential-routes.test.ts +3 -212
- package/src/__tests__/credential-security-invariants.test.ts +1 -2
- package/src/__tests__/cu-unified-flow.test.ts +2 -8
- package/src/__tests__/drain-kick-guard.test.ts +2 -0
- package/src/__tests__/drain-requeue-on-contention.test.ts +6 -0
- package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
- package/src/__tests__/host-bash-proxy.test.ts +45 -26
- package/src/__tests__/host-browser-proxy.test.ts +17 -6
- package/src/__tests__/host-file-proxy-targeted.test.ts +0 -23
- package/src/__tests__/host-file-proxy.test.ts +23 -11
- package/src/__tests__/host-transfer-proxy-targeted.test.ts +9 -30
- package/src/__tests__/llm-catalog-parity.test.ts +0 -25
- package/src/__tests__/mcp-auth-routes.test.ts +9 -6
- package/src/__tests__/mcp-config-secret-boundary.test.ts +136 -78
- package/src/__tests__/mcp-health-check.test.ts +11 -8
- package/src/__tests__/mcp-list-plugin-servers.test.ts +10 -8
- package/src/{calls/__tests__ → __tests__}/media-stream-audio-transcode.test.ts +1 -1
- package/src/{calls/__tests__ → __tests__}/media-stream-output.test.ts +8 -37
- package/src/{calls/__tests__ → __tests__}/media-stream-parser.test.ts +1 -1
- package/src/{calls/__tests__ → __tests__}/media-stream-server-integration.test.ts +57 -150
- package/src/{calls/__tests__ → __tests__}/media-stream-stt-session.test.ts +5 -5
- package/src/{calls/__tests__ → __tests__}/media-turn-detector.test.ts +1 -1
- package/src/__tests__/messages-after-tiebreaker.test.ts +0 -91
- package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
- package/src/__tests__/messaging-send-tool.test.ts +548 -355
- package/src/__tests__/model-intents.test.ts +0 -3
- package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
- package/src/__tests__/platform-client-verify-credential.test.ts +0 -2
- package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -13
- package/src/__tests__/script-proxy-certs.test.ts +0 -35
- package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
- package/src/__tests__/skill-feature-flags.test.ts +2 -2
- package/src/__tests__/speaker-identification.test.ts +56 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +3 -19
- package/src/__tests__/subagent-tools.test.ts +2 -3
- package/src/__tests__/telephony-tts-guard.test.ts +3 -38
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +0 -4
- package/src/{calls/__tests__/voice-session-bridge-turn-flow.test.ts → __tests__/voice-session-bridge.test.ts} +12 -20
- package/src/__tests__/watch-retro-tool-availability.test.ts +17 -8
- package/src/__tests__/workspace-git-service.test.ts +0 -26
- package/src/acp/__tests__/agent-process.test.ts +4 -258
- package/src/acp/__tests__/auth-required.test.ts +0 -96
- package/src/acp/__tests__/client-handler.test.ts +2 -90
- package/src/acp/__tests__/session-manager-persistence.test.ts +1 -5
- package/src/acp/__tests__/session-manager-resume.test.ts +6 -270
- package/src/acp/__tests__/session-manager-usage.test.ts +2 -4
- package/src/acp/__tests__/session-manager.test.ts +4 -895
- package/src/acp/agent-process.test.ts +1 -4
- package/src/acp/agent-process.ts +9 -94
- package/src/acp/auth-required.ts +1 -50
- package/src/acp/auto-install.test.ts +7 -9
- package/src/acp/client-handler.ts +3 -16
- package/src/acp/resolve-agent.test.ts +6 -220
- package/src/acp/resolve-agent.ts +23 -88
- package/src/acp/session-manager.test.ts +3 -196
- package/src/acp/session-manager.ts +18 -441
- package/src/acp/types.ts +5 -50
- package/src/api/events/app-files-changed.ts +3 -4
- package/src/api/index.ts +0 -6
- package/src/apps/app-store.ts +1 -1
- package/src/bundler/app-compiler.ts +0 -15
- package/src/calls/__tests__/voice-control-protocol.test.ts +25 -58
- package/src/calls/__tests__/voice-session-bridge.test.ts +1 -37
- package/src/calls/__tests__/voice-triage-escalate.test.ts +0 -105
- package/src/calls/call-constants.ts +4 -24
- package/src/calls/call-controller.ts +199 -499
- package/src/calls/call-transport.ts +0 -16
- package/src/calls/media-stream-output.ts +0 -32
- package/src/calls/media-stream-server.ts +30 -82
- package/src/calls/media-stream-stt-session.ts +0 -15
- package/src/calls/resolve-call-tts-provider.ts +2 -33
- package/src/calls/speaker-identification.ts +233 -0
- package/src/calls/tts-call-strategy.ts +5 -9
- package/src/calls/types.ts +0 -2
- package/src/calls/voice-control-protocol.ts +21 -53
- package/src/calls/voice-session-bridge.ts +46 -59
- package/src/calls/voice-triage-escalate.ts +56 -175
- package/src/cli/commands/apps.help.ts +13 -67
- package/src/cli/commands/apps.ts +0 -183
- package/src/cli/commands/channels/index.help.ts +2 -70
- package/src/cli/commands/channels/index.ts +3 -16
- package/src/cli/commands/credentials.help.ts +2 -6
- package/src/cli/commands/credentials.ts +2 -2
- package/src/cli/commands/mcp.help.ts +8 -8
- package/src/cli/commands/mcp.ts +1 -1
- package/src/cli/commands/roadmap.help.ts +1 -5
- package/src/cli/commands/roadmap.ts +1 -1
- package/src/cli/lib/bundled-marketplace.json +90 -103
- package/src/config/__tests__/default-profile-catalog.test.ts +0 -22
- package/src/config/__tests__/memory-retrospective-schema.test.ts +1 -15
- package/src/config/__tests__/platform-rehydration.test.ts +0 -93
- package/src/config/acp-defaults.test.ts +3 -13
- package/src/config/acp-defaults.ts +6 -23
- package/src/config/acp-schema.ts +2 -30
- package/src/config/bundled-skills/acp/SKILL.md +14 -22
- package/src/config/bundled-skills/acp/TOOLS.json +1 -5
- package/src/config/bundled-skills/app-builder/SKILL.md +9 -12
- package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
- package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +139 -67
- package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
- package/src/config/bundled-skills/screen-annotation/SKILL.md +1 -8
- package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +1 -4
- package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
- package/src/config/default-profile-catalog.ts +1 -1
- package/src/config/env.ts +2 -2
- package/src/config/feature-flag-registry.json +12 -8
- package/src/config/memory-v3-gate.test.ts +0 -31
- package/src/config/memory-v3-gate.ts +16 -20
- package/src/config/platform-rehydration.ts +21 -75
- package/src/config/schema.ts +2 -2
- package/src/config/schemas/__tests__/live-voice.test.ts +158 -8
- package/src/config/schemas/calls.ts +28 -1
- package/src/config/schemas/live-voice.ts +173 -1
- package/src/config/schemas/mcp.ts +1 -1
- package/src/config/schemas/memory-retrospective.ts +0 -10
- package/src/config/schemas/skills.ts +14 -0
- package/src/credential-execution/process-manager.test.ts +19 -18
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +121 -66
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +1 -4
- package/src/daemon/app-source-watcher.ts +218 -0
- package/src/daemon/config-watcher.ts +194 -13
- package/src/daemon/conversation-process.ts +64 -35
- package/src/daemon/conversation-skill-tools.ts +31 -13
- package/src/daemon/conversation-store.ts +1 -1
- package/src/daemon/conversation-surfaces.ts +8 -10
- package/src/daemon/conversation-tool-setup.ts +43 -37
- package/src/daemon/conversation.ts +3 -0
- package/src/daemon/host-bash-proxy.ts +12 -8
- package/src/daemon/host-browser-proxy.ts +1 -15
- package/src/daemon/host-file-proxy.ts +10 -8
- package/src/daemon/host-transfer-proxy.ts +18 -14
- package/src/daemon/lifecycle.ts +5 -0
- package/src/daemon/mcp-reload-service.ts +5 -5
- package/src/daemon/message-types/sync.ts +0 -1
- package/src/daemon/providers-setup.ts +1 -1
- package/src/daemon/shutdown-handlers.ts +9 -1
- package/src/daemon/skill-memory-refresh.ts +0 -64
- package/src/daemon/tool-side-effects.ts +18 -7
- package/src/hooks/types.ts +0 -33
- package/src/inbound/platform-callback-registration.ts +5 -4
- package/src/live-voice/__tests__/live-voice-metrics.test.ts +3 -0
- package/src/live-voice/__tests__/live-voice-progress.test.ts +15 -15
- package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
- package/src/{calls → live-voice}/__tests__/progress-narration.test.ts +4 -4
- package/src/{calls → live-voice}/__tests__/progress-phrases.test.ts +1 -1
- package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +69 -13
- package/src/live-voice/live-voice-session.ts +693 -213
- package/src/{calls → live-voice}/progress-narration.ts +2 -2
- package/src/live-voice/protocol.ts +2 -0
- package/src/mcp/__tests__/effective-config.test.ts +2 -2
- package/src/mcp/effective-config.ts +3 -3
- package/src/mcp/mcp-header-store.ts +45 -29
- package/src/mcp/startup.ts +4 -8
- package/src/messaging/provider.ts +3 -10
- package/src/messaging/providers/callback-routing.ts +0 -18
- package/src/messaging/providers/channel-transport.ts +0 -56
- package/src/messaging/providers/discord/transport.ts +0 -17
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +17 -6
- package/src/messaging/providers/slack/adapter.ts +44 -7
- package/src/messaging/providers/slack/transport.ts +0 -16
- package/src/messaging/providers/telegram-bot/adapter.ts +48 -1
- package/src/messaging/providers/telegram-bot/client.ts +26 -0
- package/src/messaging/providers/telegram-bot/transport.ts +0 -20
- package/src/messaging/providers/whatsapp/adapter.ts +48 -1
- package/src/messaging/providers/whatsapp/client.ts +29 -0
- package/src/messaging/providers/whatsapp/transport.ts +0 -18
- package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +0 -8
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +0 -55
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +3 -153
- package/src/notifications/assistant-reply-producer.ts +3 -8
- package/src/notifications/conversation-pairing.ts +16 -36
- package/src/notifications/delivered-post-record.ts +1 -11
- package/src/notifications/home-feed-side-effect.ts +7 -55
- package/src/outbound-proxy/certs.ts +5 -3
- package/src/permissions/checker.test.ts +6 -11
- package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
- package/src/persistence/conversation-crud.ts +63 -71
- package/src/persistence/conversation-lineage.ts +4 -12
- package/src/persistence/conversation-title-service.ts +26 -9
- package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
- package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
- package/src/persistence/message-reads.ts +0 -30
- package/src/persistence/migrations/157-invite-contact-id.ts +6 -18
- package/src/persistence/schema/memory-core.ts +0 -5
- package/src/platform/client.test.ts +0 -2
- package/src/platform/client.ts +10 -2
- package/src/plugin-api/constants.ts +0 -2
- package/src/plugin-api/index.ts +0 -1
- package/src/plugin-api/types.ts +0 -2
- package/src/plugins/defaults/index.ts +0 -2
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +0 -46
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +5 -9
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +10 -20
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +0 -219
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +0 -31
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +0 -63
- package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +0 -4
- package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -4
- package/src/plugins/defaults/memory/hooks/init.ts +0 -10
- package/src/plugins/defaults/memory/hooks/shutdown.ts +0 -2
- package/src/plugins/defaults/memory/jobs-worker.ts +0 -13
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +60 -34
- package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +5 -3
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +3 -12
- package/src/plugins/defaults/memory/memory-retrospective-state.ts +71 -312
- package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +1 -2
- package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +3 -5
- package/src/plugins/defaults/memory/startup.ts +1 -6
- package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +9 -7
- package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
- package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +0 -89
- package/src/plugins/defaults/memory/worker.ts +0 -6
- package/src/plugins/mcp-servers.ts +75 -15
- package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
- package/src/providers/gemini/client.ts +23 -9
- package/src/providers/model-catalog.ts +0 -34
- package/src/providers/model-intents.ts +5 -10
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +18 -146
- package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +1 -83
- package/src/providers/openai/chat-completions-provider.ts +78 -192
- package/src/providers/openai/codex-models.ts +4 -0
- package/src/runtime/AGENTS.md +0 -2
- package/src/runtime/auth/same-actor.ts +3 -38
- package/src/runtime/http-server.ts +151 -160
- package/src/runtime/routes/__tests__/acp-routes.test.ts +4 -159
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +1 -108
- package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +12 -16
- package/src/runtime/routes/acp-routes.test.ts +0 -15
- package/src/runtime/routes/acp-routes.ts +9 -72
- package/src/runtime/routes/app-management-routes.ts +0 -73
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/conversation-list-routes.ts +3 -14
- package/src/runtime/routes/conversation-query-routes.ts +0 -40
- package/src/runtime/routes/credential-routes.ts +10 -19
- package/src/runtime/routes/diagnostics-routes.ts +3 -55
- package/src/runtime/routes/index.ts +0 -4
- package/src/runtime/routes/log-export-routes.ts +0 -2
- package/src/runtime/routes/mcp-auth-routes.ts +47 -39
- package/src/runtime/routes/migration-routes.ts +21 -8
- package/src/runtime/routes/platform-routes.ts +15 -18
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +0 -28
- package/src/runtime/services/conversation-serializer.ts +3 -16
- package/src/runtime/sync/resource-sync-events.ts +1 -2
- package/src/schedule/worker.ts +1 -1
- package/src/security/ces-rpc-record-backend.ts +0 -7
- package/src/subagent/consult-context.ts +25 -12
- package/src/tools/acp/list-agents.test.ts +1 -3
- package/src/tools/acp/spawn.test.ts +11 -146
- package/src/tools/acp/spawn.ts +3 -21
- package/src/tools/acp/status.ts +3 -40
- package/src/tools/app-control/skill-proxy-bridge.ts +1 -2
- package/src/tools/capability-offer.test.ts +2 -22
- package/src/tools/capability-offer.ts +1 -16
- package/src/tools/computer-use/definitions.ts +2 -4
- package/src/tools/computer-use/skill-proxy-bridge.ts +1 -2
- package/src/tools/credentials/metadata-store.ts +0 -100
- package/src/tools/host-filesystem/edit.test.ts +1 -2
- package/src/tools/host-filesystem/edit.ts +2 -2
- package/src/tools/host-filesystem/read.test.ts +1 -2
- package/src/tools/host-filesystem/read.ts +2 -2
- package/src/tools/host-filesystem/transfer.test.ts +1 -2
- package/src/tools/host-filesystem/transfer.ts +2 -2
- package/src/tools/host-filesystem/write.test.ts +1 -2
- package/src/tools/host-filesystem/write.ts +2 -2
- package/src/tools/host-terminal/host-shell.test.ts +0 -16
- package/src/tools/host-terminal/host-shell.ts +20 -4
- package/src/tools/policy-context.ts +7 -6
- package/src/tools/subagent/spawn.ts +8 -5
- package/src/tools/watch/watch-retro-report.ts +8 -0
- package/src/util/__tests__/language-subtag.test.ts +1 -23
- package/src/util/fs-watcher-error.ts +36 -0
- package/src/util/language-subtag.ts +0 -16
- package/src/util/platform.ts +0 -8
- package/src/workspace/git-service.ts +54 -5
- package/src/workspace/migrations/registry.ts +0 -6
- package/scripts/smoke-desktop-window-theme.ts +0 -186
- package/src/__tests__/git-porcelain.test.ts +0 -47
- package/src/__tests__/helpers/set-workspace-mcp.ts +0 -11
- package/src/__tests__/message-deleted-hook-dispatch.test.ts +0 -119
- package/src/__tests__/resolve-app.test.ts +0 -135
- package/src/__tests__/source-fingerprint.test.ts +0 -184
- package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +0 -336
- package/src/acp/__tests__/helpers/acp-model-option.ts +0 -51
- package/src/acp/model-config.test.ts +0 -166
- package/src/acp/model-config.ts +0 -138
- package/src/api/events/acp-session-model-update.test.ts +0 -88
- package/src/api/events/acp-session-model-update.ts +0 -46
- package/src/apps/resolve-app.ts +0 -81
- package/src/apps/source-fingerprint.ts +0 -266
- package/src/calls/__tests__/barge-in-guard.test.ts +0 -81
- package/src/calls/__tests__/voice-leg-coordinator.test.ts +0 -229
- package/src/calls/__tests__/voice-progress-cadence.test.ts +0 -274
- package/src/calls/barge-in-guard.ts +0 -118
- package/src/calls/voice-leg-coordinator.ts +0 -189
- package/src/calls/voice-progress-cadence.ts +0 -423
- package/src/cli/commands/__tests__/apps.test.ts +0 -245
- package/src/cli/commands/__tests__/roadmap.help.test.ts +0 -19
- package/src/cli/commands/channels/__tests__/send.test.ts +0 -218
- package/src/cli/commands/channels/send.ts +0 -128
- package/src/config/__tests__/platform-identity.test.ts +0 -117
- package/src/config/acp-agent-write.ts +0 -43
- package/src/config/acp-schema.test.ts +0 -81
- package/src/config/platform-identity.ts +0 -144
- package/src/config/schemas/__tests__/voice.test.ts +0 -148
- package/src/config/schemas/voice.ts +0 -150
- package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +0 -133
- package/src/daemon/app-change-notify.ts +0 -45
- package/src/desktop/__tests__/fake-desktop.ts +0 -119
- package/src/desktop/desktop-chrome-policy.test.ts +0 -71
- package/src/desktop/desktop-chrome-policy.ts +0 -42
- package/src/desktop/desktop-dependencies.test.ts +0 -102
- package/src/desktop/desktop-dependencies.ts +0 -306
- package/src/desktop/desktop-feature.test.ts +0 -18
- package/src/desktop/desktop-feature.ts +0 -19
- package/src/desktop/desktop-panel-config.test.ts +0 -135
- package/src/desktop/desktop-panel-config.ts +0 -149
- package/src/desktop/desktop-session-manager.test.ts +0 -627
- package/src/desktop/desktop-session-manager.ts +0 -814
- package/src/desktop/desktop-stream-bridge.test.ts +0 -395
- package/src/desktop/desktop-stream-bridge.ts +0 -214
- package/src/desktop/desktop-wallpaper.test.ts +0 -191
- package/src/desktop/desktop-wallpaper.ts +0 -113
- package/src/desktop/desktop-window-theme.ts +0 -170
- package/src/i18n/AGENTS.md +0 -22
- package/src/i18n/__tests__/i18n.test.ts +0 -108
- package/src/i18n/index.ts +0 -16
- package/src/i18n/locales.ts +0 -93
- package/src/i18n/messages.ts +0 -61
- package/src/i18n/title.ts +0 -25
- package/src/mcp/__tests__/workspace-mcp-config.test.ts +0 -135
- package/src/mcp/spec-schema.ts +0 -111
- package/src/mcp/workspace-mcp-config.ts +0 -188
- package/src/messaging/providers/__tests__/proactive-address.test.ts +0 -95
- package/src/persistence/conversation-title-placeholders.ts +0 -23
- package/src/persistence/message-cursor.ts +0 -78
- package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +0 -149
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +0 -118
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +0 -83
- package/src/plugins/defaults/memory/hooks/message-deleted.ts +0 -21
- package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +0 -126
- package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +0 -79
- package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +0 -28
- package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +0 -9
- package/src/providers/gemini-thought-signature.test.ts +0 -63
- package/src/providers/gemini-thought-signature.ts +0 -44
- package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +0 -613
- package/src/providers/openai/__tests__/google-thought-signature.test.ts +0 -235
- package/src/providers/openai/google-thought-signature.ts +0 -320
- package/src/runtime/__tests__/channel-send.test.ts +0 -443
- package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +0 -89
- package/src/runtime/__tests__/live-voice-upgrade.test.ts +0 -58
- package/src/runtime/__tests__/runtime-ws-test-utils.ts +0 -72
- package/src/runtime/__tests__/watch-stream-upgrade.test.ts +0 -59
- package/src/runtime/channel-send.ts +0 -316
- package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +0 -205
- package/src/runtime/routes/__tests__/channel-send-routes.test.ts +0 -127
- package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +0 -107
- package/src/runtime/routes/channel-send-routes.ts +0 -85
- package/src/runtime/routes/desktop-setup-routes.test.ts +0 -74
- package/src/runtime/routes/desktop-setup-routes.ts +0 -35
- package/src/security/__tests__/ces-http-record-backend.test.ts +0 -137
- package/src/security/ces-http-record-backend.ts +0 -120
- package/src/skills/__tests__/workspace-skill-md-signature.test.ts +0 -60
- package/src/skills/workspace-skill-md-signature.ts +0 -50
- package/src/tools/acp/status.test.ts +0 -94
- package/src/util/ca-bundle.ts +0 -13
- package/src/workspace/git-porcelain.ts +0 -66
- package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +0 -258
- package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +0 -78
- package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +0 -214
- package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +0 -102
- package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +0 -199
- /package/src/{calls → live-voice}/progress-phrases.ts +0 -0
|
@@ -1,38 +1,19 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
type BargeInGuard,
|
|
6
|
-
createBargeInGuard,
|
|
7
|
-
DEFAULT_BARGE_IN_MIN_SPEECH_MS,
|
|
8
|
-
} from "../calls/barge-in-guard.js";
|
|
9
4
|
import {
|
|
10
5
|
MediaTurnDetector,
|
|
11
6
|
type TurnDetectorConfig,
|
|
12
7
|
} from "../calls/media-turn-detector.js";
|
|
13
|
-
import {
|
|
14
|
-
createVoiceProgressNarrator,
|
|
15
|
-
type VoiceProgressNarrator,
|
|
16
|
-
} from "../calls/progress-narration.js";
|
|
17
|
-
import {
|
|
18
|
-
APPROVAL_PENDING_PHRASE_BY_LANGUAGE,
|
|
19
|
-
approvalPendingPhraseFor,
|
|
20
|
-
} from "../calls/progress-phrases.js";
|
|
21
8
|
import {
|
|
22
9
|
SPOKEN_REPLY_LENGTH_RULE,
|
|
23
10
|
SPOKEN_REPLY_PLAIN_TEXT_RULE,
|
|
24
11
|
} from "../calls/spoken-reply-rules.js";
|
|
25
12
|
import { sanitizeForTts } from "../calls/tts-text-sanitizer.js";
|
|
26
|
-
import { createControlMarkerHoldback } from "../calls/voice-control-protocol.js";
|
|
27
|
-
import {
|
|
28
|
-
createFrontDoorLegCoordinator,
|
|
29
|
-
type FrontDoorLegCoordinator,
|
|
30
|
-
type SpokenEscalationBridge,
|
|
31
|
-
} from "../calls/voice-leg-coordinator.js";
|
|
32
13
|
import {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
} from "../calls/voice-
|
|
14
|
+
isIncompleteControlMarkerTail,
|
|
15
|
+
stripInternalSpeechMarkers,
|
|
16
|
+
} from "../calls/voice-control-protocol.js";
|
|
36
17
|
import type {
|
|
37
18
|
VoiceTurnHandle,
|
|
38
19
|
VoiceTurnOptions,
|
|
@@ -42,16 +23,24 @@ import {
|
|
|
42
23
|
resolveProcessingWaitMs,
|
|
43
24
|
waitForPriorTurnTeardown,
|
|
44
25
|
} from "../calls/voice-session-bridge.js";
|
|
45
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
capEscalationBridge,
|
|
28
|
+
classifyFrontDoorLeading,
|
|
29
|
+
ESCALATE_VERDICT_TOKEN,
|
|
30
|
+
ESCALATION_CONTINUATION_CONTENT,
|
|
31
|
+
FALLBACK_ESCALATION_BRIDGE_BY_LANGUAGE,
|
|
32
|
+
fallbackEscalationBridgeFor,
|
|
33
|
+
isEscalationBridgeComplete,
|
|
34
|
+
MIN_SPOKEN_BRIDGE_CHARS,
|
|
35
|
+
type VoiceRoutingLeg,
|
|
36
|
+
} from "../calls/voice-triage-escalate.js";
|
|
46
37
|
import { getConfig } from "../config/loader.js";
|
|
47
38
|
import {
|
|
48
39
|
type LiveVoiceFluxConfig,
|
|
49
40
|
LiveVoiceFluxConfigSchema,
|
|
41
|
+
type LiveVoiceFrontModelConfig,
|
|
42
|
+
LiveVoiceFrontModelConfigSchema,
|
|
50
43
|
} from "../config/schemas/live-voice.js";
|
|
51
|
-
import {
|
|
52
|
-
type VoiceFrontModelConfig,
|
|
53
|
-
VoiceFrontModelConfigSchema,
|
|
54
|
-
} from "../config/schemas/voice.js";
|
|
55
44
|
import { ABORT_WATCHDOG_MS } from "../daemon/abort-watchdog.js";
|
|
56
45
|
import { isRefusedInReadOnlyPass } from "../daemon/conversation-tool-setup.js";
|
|
57
46
|
import type { TrustContext } from "../daemon/trust-context-types.js";
|
|
@@ -104,7 +93,7 @@ import {
|
|
|
104
93
|
} from "../tts/reasoning-tag-filter.js";
|
|
105
94
|
import { extractSpeakableSegments } from "../tts/speakable-segments.js";
|
|
106
95
|
import { createAbortReason } from "../util/abort-reasons.js";
|
|
107
|
-
import {
|
|
96
|
+
import { hasLocalizedEntry } from "../util/language-subtag.js";
|
|
108
97
|
import { getLogger } from "../util/logger.js";
|
|
109
98
|
import {
|
|
110
99
|
activityLabelForTool,
|
|
@@ -147,6 +136,17 @@ import type {
|
|
|
147
136
|
LiveVoiceTtsOptions,
|
|
148
137
|
LiveVoiceTtsResult,
|
|
149
138
|
} from "./live-voice-tts.js";
|
|
139
|
+
import {
|
|
140
|
+
createVoiceProgressNarrator,
|
|
141
|
+
type VoiceProgressNarrator,
|
|
142
|
+
type VoiceProgressTextInput,
|
|
143
|
+
} from "./progress-narration.js";
|
|
144
|
+
import {
|
|
145
|
+
APPROVAL_PENDING_PHRASE_BY_LANGUAGE,
|
|
146
|
+
approvalPendingPhraseFor,
|
|
147
|
+
pickProgressPhrase,
|
|
148
|
+
PROGRESS_FALLBACK_PHRASES_BY_LANGUAGE,
|
|
149
|
+
} from "./progress-phrases.js";
|
|
150
150
|
import {
|
|
151
151
|
type LiveVoiceClientAttachFrameFrame,
|
|
152
152
|
type LiveVoiceClientAttachImageFrame,
|
|
@@ -200,6 +200,7 @@ const FINALIZE_GRACE_MS = 1_000;
|
|
|
200
200
|
// upstream of the provider, which hears only the microphone: it cannot tell
|
|
201
201
|
// our own TTS bleeding through imperfect echo cancellation from the caller,
|
|
202
202
|
// and would report a user turn nobody took.
|
|
203
|
+
const DEFAULT_BARGE_IN_MIN_SPEECH_MS = 250;
|
|
203
204
|
// The playback echo gate learns microphone energy while assistant audio is
|
|
204
205
|
// expected at the speaker. Input must rise above the learned level by this
|
|
205
206
|
// margin to count as user speech.
|
|
@@ -235,6 +236,24 @@ const NOISE_FLOOR_MAX_BASE_MULTIPLE = 4;
|
|
|
235
236
|
// tracks the effective trailing-silence threshold (the detector keeps its own
|
|
236
237
|
// copy private) so the endpoint decider can report the pause length.
|
|
237
238
|
const DEFAULT_SILENCE_THRESHOLD_MS = 800;
|
|
239
|
+
// Longest continuous sub-threshold gap the sustained-speech barge-in run
|
|
240
|
+
// tolerates without resetting. A gap this short is a syllable boundary, or the
|
|
241
|
+
// choppy energy the browser's half-duplex echo canceller produces while the
|
|
242
|
+
// assistant is still playing (it ducks the user's near-end voice, so post-AEC
|
|
243
|
+
// user speech arrives as intermittent above-gate chunks) — so the run keeps
|
|
244
|
+
// accumulating across it and a barge-in during playback still lands. Only a
|
|
245
|
+
// longer continuous silence (a real end of speech, or an isolated cough) resets
|
|
246
|
+
// the run.
|
|
247
|
+
const BARGE_IN_GAP_TOLERANCE_MS = 200;
|
|
248
|
+
// Ceiling on cumulative sub-threshold time across a whole barge-in run, as a
|
|
249
|
+
// multiple of bargeInMinSpeechMs. Per-gap tolerance alone lets sparse isolated
|
|
250
|
+
// blips (e.g. a 10 ms echo spike every 200 ms) each clear the consecutive-gap
|
|
251
|
+
// timer while retaining prior speech, so they would sum to the guard over
|
|
252
|
+
// several seconds and fire a barge-in with no sustained user speech. Capping the
|
|
253
|
+
// run's total tolerated silence imposes a minimum above-gate duty cycle
|
|
254
|
+
// (1 / (1 + ratio) ≈ 20%): once the run is mostly silence it resets, so genuine
|
|
255
|
+
// choppy speech still lands but periodic noise cannot accumulate into one.
|
|
256
|
+
const BARGE_IN_MAX_TOLERATED_SILENCE_RATIO = 4;
|
|
238
257
|
// Slack added to the configured end-of-turn timeout before the session stops
|
|
239
258
|
// waiting for an event that is not coming and falls the utterance back onto
|
|
240
259
|
// the silence-boundary path. A turn-detecting provider force-ends its own turn
|
|
@@ -388,11 +407,11 @@ export interface LiveVoiceSessionOptions {
|
|
|
388
407
|
finalizeGraceMs?: number;
|
|
389
408
|
/**
|
|
390
409
|
* Voice front-door endpointing and progress narration tuning. The
|
|
391
|
-
* production factory seeds this from `
|
|
410
|
+
* production factory seeds this from `liveVoice.frontModel` config when
|
|
392
411
|
* unset; absent fields fall back to the schema defaults (the constructor
|
|
393
412
|
* schema-parses the partial into a complete config).
|
|
394
413
|
*/
|
|
395
|
-
frontModelConfig?: Partial<
|
|
414
|
+
frontModelConfig?: Partial<LiveVoiceFrontModelConfig>;
|
|
396
415
|
/**
|
|
397
416
|
* Deepgram Flux turn-detection tuning. The production factory seeds this
|
|
398
417
|
* from `liveVoice.flux` config when unset; absent fields fall back to the
|
|
@@ -402,7 +421,7 @@ export interface LiveVoiceSessionOptions {
|
|
|
402
421
|
fluxConfig?: Partial<LiveVoiceFluxConfig>;
|
|
403
422
|
/**
|
|
404
423
|
* Progress narration service. The production factory constructs it from
|
|
405
|
-
* `
|
|
424
|
+
* `liveVoice.frontModel.progress` config when unset. `null` disables
|
|
406
425
|
* generated narration, so progress updates use their static fallback.
|
|
407
426
|
*/
|
|
408
427
|
progressNarrator?: VoiceProgressNarrator | null;
|
|
@@ -585,6 +604,70 @@ interface TtsSegmentJob {
|
|
|
585
604
|
frames: Promise<void>;
|
|
586
605
|
}
|
|
587
606
|
|
|
607
|
+
// One tool operation observed on a turn, fed by the bridge's structured tool
|
|
608
|
+
// callbacks. `completedAtMs`/`isError`/`resultPreview` land with tool_result.
|
|
609
|
+
interface TurnProgressOp {
|
|
610
|
+
toolName: string;
|
|
611
|
+
toolUseId?: string;
|
|
612
|
+
startedAtMs: number;
|
|
613
|
+
completedAtMs?: number;
|
|
614
|
+
isError?: boolean;
|
|
615
|
+
resultPreview?: string;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Per-turn tool-activity log and narration cadence state for spoken progress
|
|
619
|
+
// updates (liveVoice.frontModel.progress).
|
|
620
|
+
interface TurnProgressState {
|
|
621
|
+
// Tool operations this turn, in start order.
|
|
622
|
+
ops: TurnProgressOp[];
|
|
623
|
+
// Ops accumulated toward the next ops-triggered narration. Counted once per
|
|
624
|
+
// op, on start (not completion), so a burst of slow tools still trips the
|
|
625
|
+
// threshold while they run.
|
|
626
|
+
opsSinceNarration: number;
|
|
627
|
+
// Bumped by every observable change to the turn's tool activity — an op
|
|
628
|
+
// starting or finishing. The idle trigger compares it against
|
|
629
|
+
// `narratedEpoch` so a tick with nothing new to report stays silent.
|
|
630
|
+
stateEpoch: number;
|
|
631
|
+
// The `stateEpoch` the last spoken narration described: the activity the
|
|
632
|
+
// user has already been told about.
|
|
633
|
+
narratedEpoch: number;
|
|
634
|
+
// Narrations actually spoken this turn — the metrics count and the
|
|
635
|
+
// decider's 1-based updateIndex. Rate, not count, bounds narration:
|
|
636
|
+
// idleIntervalMs/minGapMs cap the cadence and the session duration cap
|
|
637
|
+
// bounds the turn.
|
|
638
|
+
updatesSpoken: number;
|
|
639
|
+
// When the last spoken floor-holder (ack or narration) enqueued — gates the
|
|
640
|
+
// progress.minGapMs spacing guard. Null until something speaks.
|
|
641
|
+
lastFloorHolderAtMs: number | null;
|
|
642
|
+
// When the turn's last TTS segment finished emitting (turn launch until
|
|
643
|
+
// anything speaks). Together with the session-level playback-tail estimate
|
|
644
|
+
// this anchors the dead-air countdown: idle time is measured from when the
|
|
645
|
+
// user last heard something, not from when the turn started.
|
|
646
|
+
lastAudibleAtMs: number;
|
|
647
|
+
// Self-re-arming dead-air narration timer; null once cleared or fired.
|
|
648
|
+
idleTimer: ReturnType<typeof setTimeout> | null;
|
|
649
|
+
// A narration generation is awaiting the decider; serializes narrations.
|
|
650
|
+
narrationInFlight: boolean;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Newest incomplete op, optionally restricted to a tool name (the
|
|
654
|
+
// tool_result fallback match when no toolUseId correlates).
|
|
655
|
+
function findLastIncompleteOp(
|
|
656
|
+
ops: TurnProgressOp[],
|
|
657
|
+
toolName?: string,
|
|
658
|
+
): TurnProgressOp | undefined {
|
|
659
|
+
for (let i = ops.length - 1; i >= 0; i -= 1) {
|
|
660
|
+
const op = ops[i];
|
|
661
|
+
if (
|
|
662
|
+
op.completedAtMs === undefined &&
|
|
663
|
+
(toolName === undefined || op.toolName === toolName)
|
|
664
|
+
) {
|
|
665
|
+
return op;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return undefined;
|
|
669
|
+
}
|
|
670
|
+
|
|
588
671
|
interface ActiveAssistantTurn {
|
|
589
672
|
token: symbol;
|
|
590
673
|
turnId: string;
|
|
@@ -600,9 +683,7 @@ interface ActiveAssistantTurn {
|
|
|
600
683
|
handle: VoiceTurnHandle | null;
|
|
601
684
|
// When the turn launched, for narration's turnElapsedMs.
|
|
602
685
|
launchedAtMs: number;
|
|
603
|
-
|
|
604
|
-
// (voice.frontModel.progress), shared with the phone driver.
|
|
605
|
-
progress: ProgressCadence;
|
|
686
|
+
progress: TurnProgressState;
|
|
606
687
|
assistantCompleted: boolean;
|
|
607
688
|
ttsDone: boolean;
|
|
608
689
|
// Latched when the turn puts something on screen (a ui tool ran); consumed
|
|
@@ -722,9 +803,7 @@ interface ActiveAssistantTurn {
|
|
|
722
803
|
// Guards the
|
|
723
804
|
// front-door leg's trailing completion from finalizing the turn, and makes
|
|
724
805
|
// the hand-off idempotent.
|
|
725
|
-
|
|
726
|
-
// escalated leg. Null until the front-door leg starts.
|
|
727
|
-
frontDoor: FrontDoorLegCoordinator | null;
|
|
806
|
+
escalationHandedOff: boolean;
|
|
728
807
|
ttsBuffer: string;
|
|
729
808
|
// What the caller actually hears this turn, summed over the model's own
|
|
730
809
|
// segments (acks and progress narration do not count). Logged at tts_done
|
|
@@ -750,6 +829,55 @@ interface ActiveAssistantTurn {
|
|
|
750
829
|
assistantAudioSampleRate?: number;
|
|
751
830
|
}
|
|
752
831
|
|
|
832
|
+
/**
|
|
833
|
+
* Control-marker hygiene for one model leg's delta stream, shared by the
|
|
834
|
+
* front-door answer stage and the default/escalated leg. The returned flush
|
|
835
|
+
* forwards the stripped (stripInternalSpeechMarkers) prefix of `raw` that has
|
|
836
|
+
* not been emitted yet and cannot contain a still-streaming control marker:
|
|
837
|
+
* the flush stops at the first "[" whose tail is an incomplete marker
|
|
838
|
+
* (isIncompleteControlMarkerTail) and holds from there until a later delta
|
|
839
|
+
* completes or disproves it; `force` (leg completion) emits the held tail so
|
|
840
|
+
* real text that merely resembles a marker prefix is not dropped. The scan
|
|
841
|
+
* runs forward from the emitted boundary — not from the last "[" — so
|
|
842
|
+
* brackets INSIDE a streaming marker body (a JSON array or "]"-bearing string
|
|
843
|
+
* in ASK_GUARDIAN_APPROVAL) can neither mask the marker's start nor pass as
|
|
844
|
+
* its terminator.
|
|
845
|
+
*
|
|
846
|
+
* **Markers are stripped, never acted on.** Nothing the model can say
|
|
847
|
+
* minimizes the room any more: that is decided by whether a ui tool ran (see
|
|
848
|
+
* the `tool_use_start` handler), so the reveal cannot depend on the model
|
|
849
|
+
* remembering a token, and a reply whose content happens to contain "[-1]" (an
|
|
850
|
+
* array literal, a temperature) cannot move the room either. No prompt teaches
|
|
851
|
+
* a marker, so this stripping is defense against a model that emits one
|
|
852
|
+
* regardless: an unspoken, unpersisted "[-1]" is the correct handling of a
|
|
853
|
+
* token that now means nothing.
|
|
854
|
+
*/
|
|
855
|
+
function createControlMarkerHoldback(
|
|
856
|
+
turn: ActiveAssistantTurn,
|
|
857
|
+
emit: (chunk: string) => void,
|
|
858
|
+
): (raw: string, opts?: { force?: boolean }) => void {
|
|
859
|
+
let emitted = 0;
|
|
860
|
+
return (raw, opts) => {
|
|
861
|
+
let safeEnd = raw.length;
|
|
862
|
+
if (opts?.force !== true) {
|
|
863
|
+
for (
|
|
864
|
+
let i = raw.indexOf("[", emitted);
|
|
865
|
+
i !== -1;
|
|
866
|
+
i = raw.indexOf("[", i + 1)
|
|
867
|
+
) {
|
|
868
|
+
if (isIncompleteControlMarkerTail(raw.slice(i))) {
|
|
869
|
+
safeEnd = i;
|
|
870
|
+
break;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
if (safeEnd > emitted) {
|
|
875
|
+
emit(stripInternalSpeechMarkers(raw.slice(emitted, safeEnd)));
|
|
876
|
+
emitted = safeEnd;
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
|
|
753
881
|
// Base control prompt for every live-voice turn. Opens with the spoken-reply
|
|
754
882
|
// rules shared with the phone path (spoken-reply-rules.ts): this is the only
|
|
755
883
|
// place the model learns that its text is spoken, since the system prompt has
|
|
@@ -1161,17 +1289,22 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1161
1289
|
// assistant turn is audibly speaking: above-gate speech-chunk duration
|
|
1162
1290
|
// accumulates until it reaches bargeInMinSpeechMs, then the deferred
|
|
1163
1291
|
// speech_started + barge-in fire (at most once per onset). Brief sub-threshold
|
|
1164
|
-
// gaps are tolerated (see
|
|
1292
|
+
// gaps are tolerated (see BARGE_IN_GAP_TOLERANCE_MS); the run resets on a
|
|
1165
1293
|
// single longer continuous silence, or once cumulative tolerated silence
|
|
1166
|
-
// exceeds the duty-cycle ceiling (see
|
|
1294
|
+
// exceeds the duty-cycle ceiling (see BARGE_IN_MAX_TOLERATED_SILENCE_RATIO).
|
|
1167
1295
|
// The detector's utterance end discards the guard.
|
|
1168
1296
|
private pendingBargeIn: {
|
|
1169
1297
|
// Null when guarding only the post-tts_done drain window (the turn is
|
|
1170
1298
|
// already finalized but the client is still playing its tail).
|
|
1171
1299
|
turn: ActiveAssistantTurn | null;
|
|
1172
|
-
|
|
1173
|
-
//
|
|
1174
|
-
|
|
1300
|
+
speechMs: number;
|
|
1301
|
+
// Consecutive sub-threshold (non-speech) time since the last speech chunk;
|
|
1302
|
+
// resets speechMs once it exceeds BARGE_IN_GAP_TOLERANCE_MS.
|
|
1303
|
+
silenceMs: number;
|
|
1304
|
+
// Cumulative sub-threshold time over the whole run (not reset by speech
|
|
1305
|
+
// chunks); resets speechMs once it exceeds the duty-cycle ceiling so sparse
|
|
1306
|
+
// periodic blips cannot sum into a barge-in.
|
|
1307
|
+
toleratedSilenceMs: number;
|
|
1175
1308
|
} | null = null;
|
|
1176
1309
|
// Estimated wall-clock ms until the client finishes draining the
|
|
1177
1310
|
// assistant audio sent so far. The server clears the turn right after
|
|
@@ -1267,9 +1400,9 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1267
1400
|
// never emits an answer.
|
|
1268
1401
|
private readonly progressNarrator: VoiceProgressNarrator | null;
|
|
1269
1402
|
// Complete front-model tunables: the constructor schema-parses the partial
|
|
1270
|
-
// option once, so every field carries its `
|
|
1403
|
+
// option once, so every field carries its `liveVoice.frontModel` schema
|
|
1271
1404
|
// default when unset.
|
|
1272
|
-
private readonly frontModelConfig:
|
|
1405
|
+
private readonly frontModelConfig: LiveVoiceFrontModelConfig;
|
|
1273
1406
|
// Complete Flux tunables (the constructor schema-parses the partial option
|
|
1274
1407
|
// once, so every field carries its `liveVoice.flux` schema default).
|
|
1275
1408
|
private readonly fluxConfig: LiveVoiceFluxConfig;
|
|
@@ -1369,7 +1502,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1369
1502
|
options.echoDrainSlackMs ?? DEFAULT_ECHO_DRAIN_SLACK_MS;
|
|
1370
1503
|
this.finalizeGraceMs = options.finalizeGraceMs ?? FINALIZE_GRACE_MS;
|
|
1371
1504
|
this.progressNarrator = options.progressNarrator ?? null;
|
|
1372
|
-
this.frontModelConfig =
|
|
1505
|
+
this.frontModelConfig = LiveVoiceFrontModelConfigSchema.parse(
|
|
1373
1506
|
options.frontModelConfig ?? {},
|
|
1374
1507
|
);
|
|
1375
1508
|
this.fluxConfig = LiveVoiceFluxConfigSchema.parse(options.fluxConfig ?? {});
|
|
@@ -2355,7 +2488,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
2355
2488
|
|
|
2356
2489
|
if (this.echoWindowTotalAudioMs === 0) {
|
|
2357
2490
|
this.echoWindowGuardCarryover =
|
|
2358
|
-
this.pendingBargeIn !== null && this.pendingBargeIn.
|
|
2491
|
+
this.pendingBargeIn !== null && this.pendingBargeIn.speechMs > 0;
|
|
2359
2492
|
} else if (this.pendingBargeIn === null) {
|
|
2360
2493
|
this.echoWindowGuardCarryover = false;
|
|
2361
2494
|
}
|
|
@@ -2400,7 +2533,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
2400
2533
|
);
|
|
2401
2534
|
if (meanAmplitude > speechThreshold) {
|
|
2402
2535
|
const guardHasSpeech =
|
|
2403
|
-
this.pendingBargeIn !== null && this.pendingBargeIn.
|
|
2536
|
+
this.pendingBargeIn !== null && this.pendingBargeIn.speechMs > 0;
|
|
2404
2537
|
if (!guardHasSpeech && this.echoMatchesAssistant(chunk)) {
|
|
2405
2538
|
this.updateEchoEnergy(meanAmplitude, chunkMs);
|
|
2406
2539
|
return [{ chunk, classification: "echo" }];
|
|
@@ -2698,7 +2831,9 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
2698
2831
|
// accumulates (trackBargeInGuard), so no speech is lost either way.
|
|
2699
2832
|
this.pendingBargeIn = {
|
|
2700
2833
|
turn: bargeableTurn,
|
|
2701
|
-
|
|
2834
|
+
speechMs: 0,
|
|
2835
|
+
silenceMs: 0,
|
|
2836
|
+
toleratedSilenceMs: 0,
|
|
2702
2837
|
};
|
|
2703
2838
|
return;
|
|
2704
2839
|
}
|
|
@@ -2722,42 +2857,71 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
2722
2857
|
|
|
2723
2858
|
// Advance the sustained-speech barge-in guard by one server-VAD chunk.
|
|
2724
2859
|
// Speech accumulates toward bargeInMinSpeechMs, short true-silence gaps are
|
|
2725
|
-
// tolerated, and classified playback echo resets the run immediately
|
|
2726
|
-
//
|
|
2727
|
-
// run carried over, so the classifier re-learns the playback level.
|
|
2860
|
+
// tolerated, and classified playback echo resets the run immediately.
|
|
2861
|
+
// Longer or mostly silent runs reset through the existing gap limits.
|
|
2728
2862
|
private trackBargeInGuard(
|
|
2729
2863
|
classification: VadEnergyClassification,
|
|
2730
2864
|
chunk: Buffer,
|
|
2731
2865
|
): void {
|
|
2732
|
-
const
|
|
2733
|
-
if (!
|
|
2866
|
+
const guard = this.pendingBargeIn;
|
|
2867
|
+
if (!guard) {
|
|
2734
2868
|
return;
|
|
2735
2869
|
}
|
|
2736
2870
|
const chunkMs = pcm16DurationMs(
|
|
2737
2871
|
chunk.byteLength,
|
|
2738
2872
|
this.context.startFrame.audio.sampleRate,
|
|
2739
2873
|
);
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2874
|
+
if (classification === "echo") {
|
|
2875
|
+
this.resetBargeInGuardRun();
|
|
2876
|
+
return;
|
|
2877
|
+
}
|
|
2878
|
+
if (classification === "silence") {
|
|
2879
|
+
guard.silenceMs += chunkMs;
|
|
2880
|
+
guard.toleratedSilenceMs += chunkMs;
|
|
2881
|
+
// Strictly greater on the per-gap check: a gap of exactly
|
|
2882
|
+
// BARGE_IN_GAP_TOLERANCE_MS is still tolerated. The web client batches PCM
|
|
2883
|
+
// into 50 ms frames, so a run of ducked frames lands on the boundary
|
|
2884
|
+
// exactly (e.g. four frames = 200 ms). The run also resets once its total
|
|
2885
|
+
// tolerated silence outweighs the speech by the duty-cycle ceiling, so
|
|
2886
|
+
// sparse periodic blips can never sum to the guard.
|
|
2887
|
+
if (
|
|
2888
|
+
guard.silenceMs > BARGE_IN_GAP_TOLERANCE_MS ||
|
|
2889
|
+
guard.toleratedSilenceMs >
|
|
2890
|
+
this.bargeInMinSpeechMs * BARGE_IN_MAX_TOLERATED_SILENCE_RATIO
|
|
2891
|
+
) {
|
|
2892
|
+
this.resetBargeInGuardRun();
|
|
2746
2893
|
}
|
|
2747
2894
|
return;
|
|
2748
2895
|
}
|
|
2749
|
-
|
|
2896
|
+
guard.silenceMs = 0;
|
|
2897
|
+
guard.speechMs += chunkMs;
|
|
2898
|
+
if (guard.speechMs < this.bargeInMinSpeechMs) {
|
|
2750
2899
|
return;
|
|
2751
2900
|
}
|
|
2752
2901
|
this.pendingBargeIn = null;
|
|
2753
2902
|
this.assistantPlaybackTailUntilMs = 0;
|
|
2754
2903
|
this.sendSpeechStarted();
|
|
2755
|
-
const { turn } =
|
|
2904
|
+
const { turn } = guard;
|
|
2756
2905
|
if (turn && turn === this.activeAssistantTurn && !turn.finalized) {
|
|
2757
2906
|
this.bargeIn(turn);
|
|
2758
2907
|
}
|
|
2759
2908
|
}
|
|
2760
2909
|
|
|
2910
|
+
private resetBargeInGuardRun(): void {
|
|
2911
|
+
const guard = this.pendingBargeIn;
|
|
2912
|
+
if (!guard) {
|
|
2913
|
+
return;
|
|
2914
|
+
}
|
|
2915
|
+
guard.speechMs = 0;
|
|
2916
|
+
guard.silenceMs = 0;
|
|
2917
|
+
guard.toleratedSilenceMs = 0;
|
|
2918
|
+
if (this.echoWindowGuardCarryover) {
|
|
2919
|
+
this.echoWindowGuardCarryover = false;
|
|
2920
|
+
this.echoEnergyEma = 0;
|
|
2921
|
+
this.echoProbeChunks = [];
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2761
2925
|
private bargeIn(turn: ActiveAssistantTurn): void {
|
|
2762
2926
|
// Abort synchronously so no tts_audio frame can follow turn_cancelled,
|
|
2763
2927
|
// and settle the cancelled turn's metrics so the next utterance's marks
|
|
@@ -3397,15 +3561,11 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
3397
3561
|
// down. One line, not narration: the turn is not working, it is waiting,
|
|
3398
3562
|
// and it says which, in the turn's spoken language, like every other
|
|
3399
3563
|
// filler phrase.
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
)
|
|
3406
|
-
) {
|
|
3407
|
-
turn.progress.noteFloorHolder();
|
|
3408
|
-
}
|
|
3564
|
+
this.enqueueFillerPhrase(
|
|
3565
|
+
turn,
|
|
3566
|
+
approvalPendingPhraseFor(turn.language),
|
|
3567
|
+
this.fixedPhraseLanguage(turn, APPROVAL_PENDING_PHRASE_BY_LANGUAGE),
|
|
3568
|
+
);
|
|
3409
3569
|
}
|
|
3410
3570
|
|
|
3411
3571
|
/** Clear the wait once a decision lands, so the turn narrates normally again. */
|
|
@@ -4012,7 +4172,13 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
4012
4172
|
if (!alreadyReleased) {
|
|
4013
4173
|
void this.releaseUtterance();
|
|
4014
4174
|
}
|
|
4015
|
-
|
|
4175
|
+
if (
|
|
4176
|
+
this.frontModelConfig.progress.enabled &&
|
|
4177
|
+
this.streamTtsAudio &&
|
|
4178
|
+
this.progressNarrator
|
|
4179
|
+
) {
|
|
4180
|
+
this.armProgressIdleTimer(turn);
|
|
4181
|
+
}
|
|
4016
4182
|
return true;
|
|
4017
4183
|
}
|
|
4018
4184
|
|
|
@@ -5003,39 +5169,20 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5003
5169
|
abortController,
|
|
5004
5170
|
handle: null,
|
|
5005
5171
|
launchedAtMs: Date.now(),
|
|
5006
|
-
progress:
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
//
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
// longest dead air in the system. Nothing is in flight while a
|
|
5021
|
-
// decision is pending, so every phrase narration has would be a lie
|
|
5022
|
-
// about who the call is waiting on: the turn says so once, when it
|
|
5023
|
-
// starts waiting, and is quiet after that.
|
|
5024
|
-
canNarrate: () =>
|
|
5025
|
-
this.isActiveAssistantTurn(token) &&
|
|
5026
|
-
!activeTurn.assistantCompleted &&
|
|
5027
|
-
activeTurn.pendingApproval === null,
|
|
5028
|
-
isAudioIdle: () => this.turnAudioIdle(activeTurn),
|
|
5029
|
-
playbackTailUntilMs: () => this.assistantPlaybackTailUntilMs,
|
|
5030
|
-
transcriptSoFar: () =>
|
|
5031
|
-
activeTurn.utterance.finalTranscriptSegments.join(" ").trim(),
|
|
5032
|
-
language: () => activeTurn.language,
|
|
5033
|
-
deltaEpoch: () => activeTurn.deltaEpoch,
|
|
5034
|
-
speak: (phrase, language) =>
|
|
5035
|
-
this.enqueueFillerPhrase(activeTurn, phrase, language),
|
|
5036
|
-
onNarrationSpoken: () => this.markProgressSpoken(activeTurn),
|
|
5037
|
-
},
|
|
5038
|
-
}),
|
|
5172
|
+
progress: {
|
|
5173
|
+
ops: [],
|
|
5174
|
+
opsSinceNarration: 0,
|
|
5175
|
+
// Equal epochs at launch: a turn that has done nothing observable yet
|
|
5176
|
+
// has nothing to narrate, so the idle trigger waits for tool activity
|
|
5177
|
+
// or the maxSilenceMs heartbeat.
|
|
5178
|
+
stateEpoch: 0,
|
|
5179
|
+
narratedEpoch: 0,
|
|
5180
|
+
updatesSpoken: 0,
|
|
5181
|
+
lastFloorHolderAtMs: null,
|
|
5182
|
+
lastAudibleAtMs: Date.now(),
|
|
5183
|
+
idleTimer: null,
|
|
5184
|
+
narrationInFlight: false,
|
|
5185
|
+
},
|
|
5039
5186
|
assistantCompleted: false,
|
|
5040
5187
|
ttsDone: false,
|
|
5041
5188
|
minimizeRequested: false,
|
|
@@ -5063,7 +5210,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5063
5210
|
continuationDelivery: opts?.continuationDelivery ?? null,
|
|
5064
5211
|
hiddenPrompt: opts?.hiddenPrompt === true,
|
|
5065
5212
|
deltaEpoch: 0,
|
|
5066
|
-
|
|
5213
|
+
escalationHandedOff: false,
|
|
5067
5214
|
ttsBuffer: "",
|
|
5068
5215
|
spokenSegments: 0,
|
|
5069
5216
|
spokenChars: 0,
|
|
@@ -5121,8 +5268,13 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5121
5268
|
// cadence, wherever in the turn it occurs; without TTS or a narrator there
|
|
5122
5269
|
// is nothing to speak (the idle trigger's static fallback still needs a
|
|
5123
5270
|
// generation attempt to fall back from).
|
|
5124
|
-
if (
|
|
5125
|
-
|
|
5271
|
+
if (
|
|
5272
|
+
this.frontModelConfig.progress.enabled &&
|
|
5273
|
+
this.streamTtsAudio &&
|
|
5274
|
+
this.progressNarrator &&
|
|
5275
|
+
!opts?.speculative
|
|
5276
|
+
) {
|
|
5277
|
+
this.armProgressIdleTimer(activeTurn);
|
|
5126
5278
|
}
|
|
5127
5279
|
|
|
5128
5280
|
// Front-door leg: a fast model fronts every turn (the `voiceFrontDoor`
|
|
@@ -5185,15 +5337,17 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5185
5337
|
}
|
|
5186
5338
|
const { token, utterance, turnId } = activeTurn;
|
|
5187
5339
|
|
|
5188
|
-
// `rawText` accumulates this leg's
|
|
5189
|
-
//
|
|
5190
|
-
//
|
|
5191
|
-
//
|
|
5192
|
-
//
|
|
5193
|
-
//
|
|
5194
|
-
//
|
|
5195
|
-
// spoken.
|
|
5340
|
+
// `rawText` accumulates this leg's full stream. A front-door leg starts
|
|
5341
|
+
// in `deciding` until its leading tokens classify as hold / escalate /
|
|
5342
|
+
// answer: an answer flushes through the shared marker holdback, while an
|
|
5343
|
+
// escalation buffers the post-verdict stream into `bridgeRaw` until the
|
|
5344
|
+
// bridge is complete, then hands off. A default/escalated leg flushes
|
|
5345
|
+
// every delta through the same holdback, so a stray control marker from
|
|
5346
|
+
// the main model is stripped instead of spoken.
|
|
5196
5347
|
let rawText = "";
|
|
5348
|
+
let frontDoorStage: "deciding" | "answer" | "bridging" | "handedOff" =
|
|
5349
|
+
"deciding";
|
|
5350
|
+
let bridgeRaw = "";
|
|
5197
5351
|
|
|
5198
5352
|
const emitLegText = (chunk: string): void => {
|
|
5199
5353
|
if (chunk.length === 0) {
|
|
@@ -5216,54 +5370,20 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5216
5370
|
this.bufferAssistantTextForTts(token, chunk);
|
|
5217
5371
|
};
|
|
5218
5372
|
|
|
5219
|
-
const flushLegText = createControlMarkerHoldback(emitLegText);
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
// hand-off settles the verdict.
|
|
5234
|
-
clear: () => this.clearFillerTimers(activeTurn),
|
|
5235
|
-
noteFloorHolder: () => activeTurn.progress.noteFloorHolder(),
|
|
5236
|
-
arm: () => activeTurn.progress.arm(),
|
|
5237
|
-
},
|
|
5238
|
-
// Hold discards a speculative turn (mid-thought pause, keep
|
|
5239
|
-
// listening); escalate and answer both commit it: utterance
|
|
5240
|
-
// release, thinking frame, and timers all happen inside
|
|
5241
|
-
// commitSpeculativeTurn.
|
|
5242
|
-
onHold: () => {
|
|
5243
|
-
void this.holdSpeculativeTurn(activeTurn);
|
|
5244
|
-
},
|
|
5245
|
-
commit: () =>
|
|
5246
|
-
!activeTurn.speculativePending ||
|
|
5247
|
-
this.commitSpeculativeTurn(activeTurn),
|
|
5248
|
-
onAnswerText: (text) => {
|
|
5249
|
-
rawText += text;
|
|
5250
|
-
flushLegText(rawText);
|
|
5251
|
-
},
|
|
5252
|
-
abortLeg: () => {
|
|
5253
|
-
activeTurn.handle?.abort();
|
|
5254
|
-
activeTurn.handle = null;
|
|
5255
|
-
},
|
|
5256
|
-
speakBridge: (bridge) =>
|
|
5257
|
-
this.speakEscalationBridge(activeTurn, bridge),
|
|
5258
|
-
startEscalatedLeg: (escalated) => {
|
|
5259
|
-
void this.startAssistantLeg(activeTurn, escalated);
|
|
5260
|
-
},
|
|
5261
|
-
},
|
|
5262
|
-
})
|
|
5263
|
-
: null;
|
|
5264
|
-
if (coordinator !== null) {
|
|
5265
|
-
activeTurn.frontDoor = coordinator;
|
|
5266
|
-
}
|
|
5373
|
+
const flushLegText = createControlMarkerHoldback(activeTurn, emitLegText);
|
|
5374
|
+
|
|
5375
|
+
// Hand off once enough of the post-verdict stream has arrived to cap
|
|
5376
|
+
// the bridge (sentence terminator or hard cap). Until then nothing is
|
|
5377
|
+
// spoken — the bridge goes out in one piece at hand-off, so the audio,
|
|
5378
|
+
// the persisted row, and the phrase quoted to the escalated leg are all
|
|
5379
|
+
// the same capped text.
|
|
5380
|
+
const maybeHandOffBridge = (): void => {
|
|
5381
|
+
if (!isEscalationBridgeComplete(bridgeRaw)) {
|
|
5382
|
+
return;
|
|
5383
|
+
}
|
|
5384
|
+
frontDoorStage = "handedOff";
|
|
5385
|
+
this.escalateTurn(activeTurn, capEscalationBridge(bridgeRaw));
|
|
5386
|
+
};
|
|
5267
5387
|
|
|
5268
5388
|
try {
|
|
5269
5389
|
// Latched before the await, not after: this flag only decides whether the
|
|
@@ -5350,13 +5470,53 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5350
5470
|
if (!this.isForwardingAssistantText(token)) {
|
|
5351
5471
|
return;
|
|
5352
5472
|
}
|
|
5353
|
-
if (
|
|
5473
|
+
if (leg.frontDoor) {
|
|
5474
|
+
rawText += msg.text;
|
|
5475
|
+
if (frontDoorStage === "handedOff") {
|
|
5476
|
+
return;
|
|
5477
|
+
}
|
|
5478
|
+
if (frontDoorStage === "bridging") {
|
|
5479
|
+
bridgeRaw += msg.text;
|
|
5480
|
+
maybeHandOffBridge();
|
|
5481
|
+
return;
|
|
5482
|
+
}
|
|
5354
5483
|
// Verdict-first: the leg's leading tokens decide the turn's
|
|
5355
|
-
// fate
|
|
5356
|
-
//
|
|
5357
|
-
//
|
|
5358
|
-
//
|
|
5359
|
-
|
|
5484
|
+
// fate. Hold discards a speculative turn (mid-thought pause,
|
|
5485
|
+
// keep listening); escalate and answer both commit it —
|
|
5486
|
+
// utterance release, thinking frame, and timers all happen
|
|
5487
|
+
// inside commitSpeculativeTurn. The hold branch is only
|
|
5488
|
+
// classifiable while the leg is speculative (its decision
|
|
5489
|
+
// rule is the only one that teaches the hold token).
|
|
5490
|
+
if (frontDoorStage === "deciding") {
|
|
5491
|
+
const verdict = classifyFrontDoorLeading(
|
|
5492
|
+
rawText.trimStart(),
|
|
5493
|
+
activeTurn.speculativePending &&
|
|
5494
|
+
activeTurn.speculativeHoldAllowed,
|
|
5495
|
+
);
|
|
5496
|
+
if (verdict === "pending") {
|
|
5497
|
+
return;
|
|
5498
|
+
}
|
|
5499
|
+
if (verdict === "hold") {
|
|
5500
|
+
void this.holdSpeculativeTurn(activeTurn);
|
|
5501
|
+
return;
|
|
5502
|
+
}
|
|
5503
|
+
if (
|
|
5504
|
+
activeTurn.speculativePending &&
|
|
5505
|
+
!this.commitSpeculativeTurn(activeTurn)
|
|
5506
|
+
) {
|
|
5507
|
+
return;
|
|
5508
|
+
}
|
|
5509
|
+
if (verdict === "escalate") {
|
|
5510
|
+
frontDoorStage = "bridging";
|
|
5511
|
+
bridgeRaw = rawText
|
|
5512
|
+
.trimStart()
|
|
5513
|
+
.slice(ESCALATE_VERDICT_TOKEN.length);
|
|
5514
|
+
maybeHandOffBridge();
|
|
5515
|
+
return;
|
|
5516
|
+
}
|
|
5517
|
+
frontDoorStage = "answer";
|
|
5518
|
+
}
|
|
5519
|
+
flushLegText(rawText);
|
|
5360
5520
|
return;
|
|
5361
5521
|
}
|
|
5362
5522
|
// Defensive: speculative legs are always front-door today, but a
|
|
@@ -5392,20 +5552,27 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5392
5552
|
if (current.speculativePending) {
|
|
5393
5553
|
this.commitSpeculativeTurn(current);
|
|
5394
5554
|
}
|
|
5395
|
-
// A front-door leg that stopped mid-bridge
|
|
5396
|
-
//
|
|
5397
|
-
//
|
|
5398
|
-
//
|
|
5399
|
-
//
|
|
5400
|
-
//
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5555
|
+
// A front-door leg that stopped mid-bridge (a bare escalate
|
|
5556
|
+
// verdict, or a holding phrase with no sentence terminator)
|
|
5557
|
+
// hands off now with whatever arrived; the canned fallback
|
|
5558
|
+
// covers an empty bridge. A cancellation mid-bridge falls
|
|
5559
|
+
// through to normal cancelled finalization instead — a dead
|
|
5560
|
+
// turn must not spawn an escalated leg.
|
|
5561
|
+
if (
|
|
5562
|
+
leg.frontDoor &&
|
|
5563
|
+
frontDoorStage === "bridging" &&
|
|
5564
|
+
msg.type === "message_complete" &&
|
|
5565
|
+
!current.escalationHandedOff
|
|
5566
|
+
) {
|
|
5567
|
+
frontDoorStage = "handedOff";
|
|
5568
|
+
this.escalateTurn(current, capEscalationBridge(bridgeRaw));
|
|
5569
|
+
return;
|
|
5570
|
+
}
|
|
5571
|
+
// A front-door leg that handed off is finished; the escalated leg
|
|
5572
|
+
// drives completion. The front-door leg's own trailing completion
|
|
5573
|
+
// (including the generation_cancelled from its abort) is a no-op.
|
|
5574
|
+
if (leg.frontDoor && current.escalationHandedOff) {
|
|
5575
|
+
return;
|
|
5409
5576
|
}
|
|
5410
5577
|
// A held "[…"-tail that never completed a marker is real text —
|
|
5411
5578
|
// force-flush it before assistantCompleted closes the TTS buffer
|
|
@@ -5482,16 +5649,57 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5482
5649
|
toolName,
|
|
5483
5650
|
});
|
|
5484
5651
|
}
|
|
5485
|
-
|
|
5652
|
+
// The op counts toward the narration threshold on start (not
|
|
5653
|
+
// completion) so a burst of slow tools still trips the ops
|
|
5654
|
+
// trigger while they run.
|
|
5655
|
+
current.progress.ops.push({
|
|
5656
|
+
toolName,
|
|
5657
|
+
...(detail?.toolUseId !== undefined
|
|
5658
|
+
? { toolUseId: detail.toolUseId }
|
|
5659
|
+
: {}),
|
|
5660
|
+
startedAtMs: Date.now(),
|
|
5661
|
+
});
|
|
5662
|
+
current.progress.opsSinceNarration += 1;
|
|
5663
|
+
current.progress.stateEpoch += 1;
|
|
5486
5664
|
this.refreshActivity(current);
|
|
5487
5665
|
log.debug({ turnId, toolName }, "Live voice turn started tool use");
|
|
5666
|
+
this.maybeNarrateProgress(current, "ops");
|
|
5488
5667
|
},
|
|
5489
5668
|
tool_result: (event) => {
|
|
5490
5669
|
const current = this.activeAssistantTurn;
|
|
5491
5670
|
if (current?.token !== token) {
|
|
5492
5671
|
return;
|
|
5493
5672
|
}
|
|
5494
|
-
|
|
5673
|
+
// Match by toolUseId when present; otherwise the last-started
|
|
5674
|
+
// incomplete op with the same name (parallel same-name ops
|
|
5675
|
+
// resolve newest-first).
|
|
5676
|
+
const op =
|
|
5677
|
+
(event.toolUseId !== undefined
|
|
5678
|
+
? current.progress.ops.find(
|
|
5679
|
+
(o) => o.toolUseId === event.toolUseId,
|
|
5680
|
+
)
|
|
5681
|
+
: undefined) ??
|
|
5682
|
+
findLastIncompleteOp(current.progress.ops, event.toolName);
|
|
5683
|
+
// A long-running op finishing is the beat the user has been
|
|
5684
|
+
// waiting through: it narrates immediately rather than waiting for
|
|
5685
|
+
// `opsThreshold` more ops, which on a one-slow-tool turn never
|
|
5686
|
+
// arrive. Short ops stay on the ops trigger — narrating every
|
|
5687
|
+
// quick lookup is the chatter this cadence exists to avoid.
|
|
5688
|
+
let trigger: "ops" | "op_complete" = "ops";
|
|
5689
|
+
if (op) {
|
|
5690
|
+
op.completedAtMs = Date.now();
|
|
5691
|
+
if (event.isError !== undefined) {
|
|
5692
|
+
op.isError = event.isError;
|
|
5693
|
+
}
|
|
5694
|
+
op.resultPreview = event.resultPreview;
|
|
5695
|
+
if (
|
|
5696
|
+
op.completedAtMs - op.startedAtMs >=
|
|
5697
|
+
this.frontModelConfig.progress.longOpMs
|
|
5698
|
+
) {
|
|
5699
|
+
trigger = "op_complete";
|
|
5700
|
+
}
|
|
5701
|
+
}
|
|
5702
|
+
current.progress.stateEpoch += 1;
|
|
5495
5703
|
// Showing a surface implies revealing it: the room is a
|
|
5496
5704
|
// full-screen overlay, so a surface rendered behind it is a
|
|
5497
5705
|
// surface nobody sees.
|
|
@@ -5514,6 +5722,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5514
5722
|
}
|
|
5515
5723
|
}
|
|
5516
5724
|
this.refreshActivity(current);
|
|
5725
|
+
this.maybeNarrateProgress(current, trigger);
|
|
5517
5726
|
},
|
|
5518
5727
|
},
|
|
5519
5728
|
onError: (message) => {
|
|
@@ -5572,7 +5781,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5572
5781
|
}
|
|
5573
5782
|
// The front-door leg may have handed off before its handle resolved;
|
|
5574
5783
|
// abort it rather than exposing it as the turn's live handle.
|
|
5575
|
-
if (
|
|
5784
|
+
if (leg.frontDoor && current.escalationHandedOff) {
|
|
5576
5785
|
handle.abort();
|
|
5577
5786
|
return true;
|
|
5578
5787
|
}
|
|
@@ -5600,17 +5809,37 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5600
5809
|
}
|
|
5601
5810
|
|
|
5602
5811
|
/**
|
|
5603
|
-
*
|
|
5604
|
-
*
|
|
5605
|
-
* canned fallback
|
|
5606
|
-
*
|
|
5812
|
+
* Hand the turn from the front-door leg to the strong "escalated" leg after
|
|
5813
|
+
* the escalate verdict. Speaks the capped bridge (the leg's own post-verdict
|
|
5814
|
+
* holding phrase, or the canned fallback when that was too short) in one
|
|
5815
|
+
* piece and force-flushes it so it plays during the strong-model call, then
|
|
5816
|
+
* starts the escalated leg on the same ActiveAssistantTurn. Idempotent.
|
|
5607
5817
|
*/
|
|
5608
|
-
private
|
|
5818
|
+
private escalateTurn(
|
|
5609
5819
|
activeTurn: ActiveAssistantTurn,
|
|
5610
|
-
|
|
5820
|
+
cappedBridge: string,
|
|
5611
5821
|
): void {
|
|
5612
|
-
|
|
5613
|
-
|
|
5822
|
+
if (activeTurn.escalationHandedOff || activeTurn.finalized) {
|
|
5823
|
+
return;
|
|
5824
|
+
}
|
|
5825
|
+
activeTurn.escalationHandedOff = true;
|
|
5826
|
+
// The escalation bridge below holds the floor, so pending progress
|
|
5827
|
+
// narration would only stack a second filler on top of it.
|
|
5828
|
+
this.clearFillerTimers(activeTurn);
|
|
5829
|
+
// Abort the front-door leg so a model that keeps generating past the
|
|
5830
|
+
// bridge cap adds no latency before the escalated leg starts.
|
|
5831
|
+
activeTurn.handle?.abort();
|
|
5832
|
+
activeTurn.handle = null;
|
|
5833
|
+
|
|
5834
|
+
// Speak the bridge so the strong-model call has no dead air. The model's
|
|
5835
|
+
// own bridge is real assistant speech (captions + TTS); the canned
|
|
5836
|
+
// fallback stays audio-only, matching the persisted-row hygiene (a
|
|
5837
|
+
// deleted row for a bridge the model never produced).
|
|
5838
|
+
const usesFallbackBridge = cappedBridge.length < MIN_SPOKEN_BRIDGE_CHARS;
|
|
5839
|
+
const spokenBridge = usesFallbackBridge
|
|
5840
|
+
? fallbackEscalationBridgeFor(activeTurn.language)
|
|
5841
|
+
: cappedBridge;
|
|
5842
|
+
if (!usesFallbackBridge) {
|
|
5614
5843
|
this.markFirstAssistantDelta(activeTurn.utterance, activeTurn.turnId);
|
|
5615
5844
|
this.markAssistantDelta(activeTurn);
|
|
5616
5845
|
void this.sendFrame(
|
|
@@ -5622,14 +5851,46 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5622
5851
|
// otherwise sit buffered until a sentence boundary and leave the
|
|
5623
5852
|
// caller in silence during the escalated model's call.
|
|
5624
5853
|
this.flushTtsBuffer(activeTurn.token, true);
|
|
5625
|
-
|
|
5854
|
+
} else {
|
|
5855
|
+
// The canned bridge is a fixed localized-table phrase, enqueued
|
|
5856
|
+
// directly (it is already one complete sentence) so the segment can
|
|
5857
|
+
// carry the "en" override when the table lacks the turn's language.
|
|
5858
|
+
const speakable = sanitizeForTts(spokenBridge).trim();
|
|
5859
|
+
if (speakable.length > 0) {
|
|
5860
|
+
this.enqueueTtsSegment(activeTurn.token, speakable, {
|
|
5861
|
+
language: this.fixedPhraseLanguage(
|
|
5862
|
+
activeTurn,
|
|
5863
|
+
FALLBACK_ESCALATION_BRIDGE_BY_LANGUAGE,
|
|
5864
|
+
),
|
|
5865
|
+
});
|
|
5866
|
+
}
|
|
5626
5867
|
}
|
|
5627
|
-
|
|
5628
|
-
//
|
|
5629
|
-
//
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5868
|
+
|
|
5869
|
+
// No overrideProfile here: the bridge pins the escalated leg to the
|
|
5870
|
+
// conversation's own profile, the model the caller's typed turns already
|
|
5871
|
+
// run on (see voice-triage-escalate.ts). The bridge phrase the caller
|
|
5872
|
+
// just heard is handed along so the escalated continuation rule can
|
|
5873
|
+
// quote it and ban a re-announcing echo ("Let me check…" twice in a
|
|
5874
|
+
// row).
|
|
5875
|
+
void this.startAssistantLeg(activeTurn, {
|
|
5876
|
+
content: ESCALATION_CONTINUATION_CONTENT,
|
|
5877
|
+
routingLeg: "escalated",
|
|
5878
|
+
spokenEscalationBridge: spokenBridge,
|
|
5879
|
+
});
|
|
5880
|
+
|
|
5881
|
+
// Escalated legs run the slowest work in the system (strong-model
|
|
5882
|
+
// thinking + tool loops), and the bridge only covers the first couple of
|
|
5883
|
+
// seconds of it — exactly the dead air progress narration exists for.
|
|
5884
|
+
// Re-arm the idle narration timer (cleared above with the ack): its
|
|
5885
|
+
// audible-silence gating means nothing speaks until the bridge audio has
|
|
5886
|
+
// fully drained plus a whole idle interval. Acks stay suppressed
|
|
5887
|
+
// post-handoff — the bridge already served that role.
|
|
5888
|
+
if (
|
|
5889
|
+
this.frontModelConfig.progress.enabled &&
|
|
5890
|
+
this.streamTtsAudio &&
|
|
5891
|
+
this.progressNarrator
|
|
5892
|
+
) {
|
|
5893
|
+
this.armProgressIdleTimer(activeTurn);
|
|
5633
5894
|
}
|
|
5634
5895
|
}
|
|
5635
5896
|
|
|
@@ -5706,6 +5967,61 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5706
5967
|
turn.deltaEpoch += 1;
|
|
5707
5968
|
}
|
|
5708
5969
|
|
|
5970
|
+
// Arms (or re-arms) the dead-air narration timer. The countdown measures
|
|
5971
|
+
// audible silence — time since the turn's audio last (estimatedly) reached
|
|
5972
|
+
// the user's ears — not time since launch, so it covers mid-turn silences
|
|
5973
|
+
// for the whole turn. On expiry with audio still pending, or with the
|
|
5974
|
+
// silence not yet a full interval old, it re-arms for the remainder; only a
|
|
5975
|
+
// full interval of audible silence reaches the narration gatekeeper. The
|
|
5976
|
+
// interval is a polling cadence, not a speaking cadence: most ticks find
|
|
5977
|
+
// nothing new to report and stay quiet, so what the user hears follows the
|
|
5978
|
+
// turn's tool activity (with `maxSilenceMs` as the heartbeat ceiling).
|
|
5979
|
+
private armProgressIdleTimer(
|
|
5980
|
+
turn: ActiveAssistantTurn,
|
|
5981
|
+
delayMs?: number,
|
|
5982
|
+
): void {
|
|
5983
|
+
const { token } = turn;
|
|
5984
|
+
this.clearProgressIdleTimer(turn);
|
|
5985
|
+
turn.progress.idleTimer = setTimeout(() => {
|
|
5986
|
+
turn.progress.idleTimer = null;
|
|
5987
|
+
if (!this.isActiveAssistantTurn(token) || turn.assistantCompleted) {
|
|
5988
|
+
return;
|
|
5989
|
+
}
|
|
5990
|
+
if (!this.turnAudioIdle(turn)) {
|
|
5991
|
+
// Audio is buffered, queued, or still playing: a fresh silence can
|
|
5992
|
+
// only be a full interval old one interval from now.
|
|
5993
|
+
this.armProgressIdleTimer(turn);
|
|
5994
|
+
return;
|
|
5995
|
+
}
|
|
5996
|
+
const remaining = this.progressIdleDeadlineMs(turn) - Date.now();
|
|
5997
|
+
if (remaining > 0) {
|
|
5998
|
+
this.armProgressIdleTimer(turn, remaining);
|
|
5999
|
+
return;
|
|
6000
|
+
}
|
|
6001
|
+
this.maybeNarrateProgress(turn, "idle");
|
|
6002
|
+
this.armProgressIdleTimer(turn);
|
|
6003
|
+
}, delayMs ?? this.frontModelConfig.progress.idleIntervalMs);
|
|
6004
|
+
}
|
|
6005
|
+
|
|
6006
|
+
// Wall-clock instant the current audible silence turns a full interval old.
|
|
6007
|
+
private progressIdleDeadlineMs(turn: ActiveAssistantTurn): number {
|
|
6008
|
+
return (
|
|
6009
|
+
this.progressSilenceSinceMs(turn) +
|
|
6010
|
+
this.frontModelConfig.progress.idleIntervalMs
|
|
6011
|
+
);
|
|
6012
|
+
}
|
|
6013
|
+
|
|
6014
|
+
// When the turn's current audible silence began: the latest of the last
|
|
6015
|
+
// emitted segment, the estimated client playback end, and the last enqueued
|
|
6016
|
+
// filler.
|
|
6017
|
+
private progressSilenceSinceMs(turn: ActiveAssistantTurn): number {
|
|
6018
|
+
return Math.max(
|
|
6019
|
+
turn.progress.lastAudibleAtMs,
|
|
6020
|
+
this.assistantPlaybackTailUntilMs,
|
|
6021
|
+
turn.progress.lastFloorHolderAtMs ?? 0,
|
|
6022
|
+
);
|
|
6023
|
+
}
|
|
6024
|
+
|
|
5709
6025
|
// No assistant audio is pending or (estimatedly) still playing: nothing
|
|
5710
6026
|
// buffered toward the next sentence, every queued TTS segment fully
|
|
5711
6027
|
// emitted, and the client-side playback-tail estimate expired.
|
|
@@ -5717,18 +6033,177 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5717
6033
|
);
|
|
5718
6034
|
}
|
|
5719
6035
|
|
|
6036
|
+
private clearProgressIdleTimer(turn: ActiveAssistantTurn): void {
|
|
6037
|
+
if (turn.progress.idleTimer !== null) {
|
|
6038
|
+
clearTimeout(turn.progress.idleTimer);
|
|
6039
|
+
turn.progress.idleTimer = null;
|
|
6040
|
+
}
|
|
6041
|
+
}
|
|
6042
|
+
|
|
5720
6043
|
// Clears progress narration and verdict timers for events that end the
|
|
5721
6044
|
// current filler lifecycle.
|
|
5722
6045
|
private clearFillerTimers(turn: ActiveAssistantTurn): void {
|
|
5723
|
-
|
|
6046
|
+
this.clearProgressIdleTimer(turn);
|
|
5724
6047
|
if (turn.verdictDeadlineTimer !== null) {
|
|
5725
6048
|
clearTimeout(turn.verdictDeadlineTimer);
|
|
5726
6049
|
turn.verdictDeadlineTimer = null;
|
|
5727
6050
|
}
|
|
5728
6051
|
}
|
|
5729
6052
|
|
|
5730
|
-
//
|
|
5731
|
-
//
|
|
6053
|
+
// Narration may speak whenever the live turn is audibly silent and has
|
|
6054
|
+
// not completed. Escalated turns qualify too: the bridge phrase holds the
|
|
6055
|
+
// floor only while its audio plays (covered by the audible-idle check),
|
|
6056
|
+
// and the strong leg's tool loops are the longest dead air in the system.
|
|
6057
|
+
private turnCanNarrateProgress(turn: ActiveAssistantTurn): boolean {
|
|
6058
|
+
return (
|
|
6059
|
+
this.isActiveAssistantTurn(turn.token) &&
|
|
6060
|
+
!turn.assistantCompleted &&
|
|
6061
|
+
// Nothing is in flight while a decision is pending, so every phrase
|
|
6062
|
+
// narration has would be a lie about who the call is waiting on. The
|
|
6063
|
+
// turn says so once, when it starts waiting, and is quiet after that.
|
|
6064
|
+
turn.pendingApproval === null &&
|
|
6065
|
+
this.turnAudioIdle(turn)
|
|
6066
|
+
);
|
|
6067
|
+
}
|
|
6068
|
+
|
|
6069
|
+
// The idle tick has something worth saying when the turn's tool activity has
|
|
6070
|
+
// moved since the last narration described it, or when the silence has run
|
|
6071
|
+
// past `maxSilenceMs` — the heartbeat ceiling that proves the assistant is
|
|
6072
|
+
// still alive on a turn with no observable activity at all. Every other tick
|
|
6073
|
+
// stays quiet, so the cadence follows the work rather than the clock.
|
|
6074
|
+
private progressIdleHasSomethingToSay(turn: ActiveAssistantTurn): boolean {
|
|
6075
|
+
const { progress } = turn;
|
|
6076
|
+
if (progress.stateEpoch !== progress.narratedEpoch) {
|
|
6077
|
+
return true;
|
|
6078
|
+
}
|
|
6079
|
+
const silentForMs = Date.now() - this.progressSilenceSinceMs(turn);
|
|
6080
|
+
if (silentForMs >= this.frontModelConfig.progress.maxSilenceMs) {
|
|
6081
|
+
return true;
|
|
6082
|
+
}
|
|
6083
|
+
log.debug(
|
|
6084
|
+
{ turnId: turn.turnId, silentForMs },
|
|
6085
|
+
"Live voice progress narration held — nothing new since the last update",
|
|
6086
|
+
);
|
|
6087
|
+
return false;
|
|
6088
|
+
}
|
|
6089
|
+
|
|
6090
|
+
// Gatekeeper for progress narration. It speaks only while the turn is
|
|
6091
|
+
// audibly silent, with one generation at a time and the configured spacing.
|
|
6092
|
+
private maybeNarrateProgress(
|
|
6093
|
+
turn: ActiveAssistantTurn,
|
|
6094
|
+
trigger: "ops" | "idle" | "op_complete",
|
|
6095
|
+
): void {
|
|
6096
|
+
const cfg = this.frontModelConfig.progress;
|
|
6097
|
+
const { progress } = turn;
|
|
6098
|
+
const progressNarrator = this.progressNarrator;
|
|
6099
|
+
if (
|
|
6100
|
+
!cfg.enabled ||
|
|
6101
|
+
!this.streamTtsAudio ||
|
|
6102
|
+
!progressNarrator ||
|
|
6103
|
+
!this.turnCanNarrateProgress(turn) ||
|
|
6104
|
+
progress.narrationInFlight ||
|
|
6105
|
+
(progress.lastFloorHolderAtMs !== null &&
|
|
6106
|
+
Date.now() - progress.lastFloorHolderAtMs < cfg.minGapMs) ||
|
|
6107
|
+
(trigger === "ops" && progress.opsSinceNarration < cfg.opsThreshold) ||
|
|
6108
|
+
(trigger === "idle" && !this.progressIdleHasSomethingToSay(turn))
|
|
6109
|
+
) {
|
|
6110
|
+
return;
|
|
6111
|
+
}
|
|
6112
|
+
void this.speakProgressUpdate(turn, progressNarrator, trigger);
|
|
6113
|
+
}
|
|
6114
|
+
|
|
6115
|
+
// Generate and speak one audio-only progress narration. On a null result,
|
|
6116
|
+
// idle narration falls back to a static phrase while activity-triggered
|
|
6117
|
+
// narration stays silent.
|
|
6118
|
+
private async speakProgressUpdate(
|
|
6119
|
+
turn: ActiveAssistantTurn,
|
|
6120
|
+
progressNarrator: VoiceProgressNarrator,
|
|
6121
|
+
trigger: "ops" | "idle" | "op_complete",
|
|
6122
|
+
): Promise<void> {
|
|
6123
|
+
const { progress } = turn;
|
|
6124
|
+
progress.narrationInFlight = true;
|
|
6125
|
+
// Any delta that lands while generation is in flight makes the text stale.
|
|
6126
|
+
const deltaEpochAtLaunch = turn.deltaEpoch;
|
|
6127
|
+
// The activity this update describes. Tool events that land mid-generation
|
|
6128
|
+
// are news the generated text cannot carry, so they must leave the idle
|
|
6129
|
+
// trigger armed rather than count as already narrated.
|
|
6130
|
+
const stateEpochAtLaunch = progress.stateEpoch;
|
|
6131
|
+
try {
|
|
6132
|
+
const now = Date.now();
|
|
6133
|
+
const currentOp = findLastIncompleteOp(progress.ops);
|
|
6134
|
+
const input: VoiceProgressTextInput = {
|
|
6135
|
+
transcriptSoFar: turn.utterance.finalTranscriptSegments
|
|
6136
|
+
.join(" ")
|
|
6137
|
+
.trim(),
|
|
6138
|
+
completedOps: progress.ops
|
|
6139
|
+
.filter(
|
|
6140
|
+
(op): op is TurnProgressOp & { completedAtMs: number } =>
|
|
6141
|
+
op.completedAtMs !== undefined,
|
|
6142
|
+
)
|
|
6143
|
+
// `ops` is in start order; the narrator wants completion
|
|
6144
|
+
// order, which differs when parallel tools finish out of order.
|
|
6145
|
+
.sort((a, b) => a.completedAtMs - b.completedAtMs)
|
|
6146
|
+
.map((op) => ({
|
|
6147
|
+
toolName: op.toolName,
|
|
6148
|
+
...(op.isError !== undefined ? { isError: op.isError } : {}),
|
|
6149
|
+
...(op.resultPreview !== undefined
|
|
6150
|
+
? { resultPreview: op.resultPreview }
|
|
6151
|
+
: {}),
|
|
6152
|
+
})),
|
|
6153
|
+
currentOp: currentOp
|
|
6154
|
+
? {
|
|
6155
|
+
toolName: currentOp.toolName,
|
|
6156
|
+
elapsedMs: now - currentOp.startedAtMs,
|
|
6157
|
+
}
|
|
6158
|
+
: null,
|
|
6159
|
+
turnElapsedMs: now - turn.launchedAtMs,
|
|
6160
|
+
updateIndex: progress.updatesSpoken + 1,
|
|
6161
|
+
...(turn.language !== undefined ? { languageHint: turn.language } : {}),
|
|
6162
|
+
};
|
|
6163
|
+
const generated = await progressNarrator
|
|
6164
|
+
.generateProgressText(input, turn.abortController.signal)
|
|
6165
|
+
// The narrator contract never rejects. Keep test stubs fail-soft too.
|
|
6166
|
+
.catch(() => null);
|
|
6167
|
+
// Re-check liveness and staleness after the provider call.
|
|
6168
|
+
if (
|
|
6169
|
+
!this.turnCanNarrateProgress(turn) ||
|
|
6170
|
+
turn.deltaEpoch !== deltaEpochAtLaunch ||
|
|
6171
|
+
(progress.lastFloorHolderAtMs !== null &&
|
|
6172
|
+
Date.now() - progress.lastFloorHolderAtMs <
|
|
6173
|
+
this.frontModelConfig.progress.minGapMs)
|
|
6174
|
+
) {
|
|
6175
|
+
return;
|
|
6176
|
+
}
|
|
6177
|
+
let raw = generated;
|
|
6178
|
+
// Generated text is in the turn's language; only the static
|
|
6179
|
+
// fallback comes from a localized table and may need the "en" override.
|
|
6180
|
+
let fillerLanguage: string | undefined;
|
|
6181
|
+
if (raw === null) {
|
|
6182
|
+
if (trigger !== "idle") {
|
|
6183
|
+
return;
|
|
6184
|
+
}
|
|
6185
|
+
raw = pickProgressPhrase(this.progressPhraseCounter++, turn.language);
|
|
6186
|
+
fillerLanguage = this.fixedPhraseLanguage(
|
|
6187
|
+
turn,
|
|
6188
|
+
PROGRESS_FALLBACK_PHRASES_BY_LANGUAGE,
|
|
6189
|
+
);
|
|
6190
|
+
}
|
|
6191
|
+
if (!this.enqueueFillerPhrase(turn, raw, fillerLanguage)) {
|
|
6192
|
+
return;
|
|
6193
|
+
}
|
|
6194
|
+
progress.opsSinceNarration = 0;
|
|
6195
|
+
progress.narratedEpoch = stateEpochAtLaunch;
|
|
6196
|
+
progress.updatesSpoken += 1;
|
|
6197
|
+
// Record only when narration audio actually enqueued.
|
|
6198
|
+
this.markProgressSpoken(turn);
|
|
6199
|
+
// Restart the dead-air countdown from this narration.
|
|
6200
|
+
this.armProgressIdleTimer(turn);
|
|
6201
|
+
} finally {
|
|
6202
|
+
progress.narrationInFlight = false;
|
|
6203
|
+
}
|
|
6204
|
+
}
|
|
6205
|
+
|
|
6206
|
+
// Sanitize and enqueue one progress sentence on the ordered TTS queue.
|
|
5732
6207
|
private enqueueFillerPhrase(
|
|
5733
6208
|
turn: ActiveAssistantTurn,
|
|
5734
6209
|
raw: string,
|
|
@@ -5742,6 +6217,8 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5742
6217
|
countsAsFirstSegment: false,
|
|
5743
6218
|
...(language !== undefined ? { language } : {}),
|
|
5744
6219
|
});
|
|
6220
|
+
// A spoken filler holds the floor, so narration's minGapMs spaces from it.
|
|
6221
|
+
turn.progress.lastFloorHolderAtMs = Date.now();
|
|
5745
6222
|
return true;
|
|
5746
6223
|
}
|
|
5747
6224
|
|
|
@@ -5754,7 +6231,10 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5754
6231
|
turn: ActiveAssistantTurn,
|
|
5755
6232
|
table: Readonly<Record<string, unknown>>,
|
|
5756
6233
|
): string | undefined {
|
|
5757
|
-
return
|
|
6234
|
+
return turn.language !== undefined &&
|
|
6235
|
+
!hasLocalizedEntry(table, turn.language)
|
|
6236
|
+
? "en"
|
|
6237
|
+
: undefined;
|
|
5758
6238
|
}
|
|
5759
6239
|
|
|
5760
6240
|
private bufferAssistantTextForTts(token: symbol, text: string): void {
|
|
@@ -5797,7 +6277,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5797
6277
|
log.info(
|
|
5798
6278
|
{
|
|
5799
6279
|
turnId: currentTurn.turnId,
|
|
5800
|
-
escalated: currentTurn.
|
|
6280
|
+
escalated: currentTurn.escalationHandedOff,
|
|
5801
6281
|
spokenSegments: currentTurn.spokenSegments,
|
|
5802
6282
|
spokenChars: currentTurn.spokenChars,
|
|
5803
6283
|
},
|
|
@@ -6038,7 +6518,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
6038
6518
|
if (settledTurn?.token === token) {
|
|
6039
6519
|
// Anchor the dead-air countdown to the end of emission; the playback
|
|
6040
6520
|
// -tail estimate covers any client-side buffer still draining.
|
|
6041
|
-
settledTurn.progress.
|
|
6521
|
+
settledTurn.progress.lastAudibleAtMs = Date.now();
|
|
6042
6522
|
}
|
|
6043
6523
|
this.pumpTtsSynthesis(token);
|
|
6044
6524
|
}
|
|
@@ -6625,8 +7105,8 @@ export function createLiveVoiceSession(
|
|
|
6625
7105
|
const vadConfig = liveVoiceConfig?.vad;
|
|
6626
7106
|
// Parsed once here into a complete config, shared by endpointing and the
|
|
6627
7107
|
// session (the constructor's own parse is then a no-op re-validation).
|
|
6628
|
-
const frontModelConfig =
|
|
6629
|
-
options.frontModelConfig ??
|
|
7108
|
+
const frontModelConfig = LiveVoiceFrontModelConfigSchema.parse(
|
|
7109
|
+
options.frontModelConfig ?? liveVoiceConfig?.frontModel ?? {},
|
|
6630
7110
|
);
|
|
6631
7111
|
return new LiveVoiceSession(context, {
|
|
6632
7112
|
...options,
|
|
@@ -6791,10 +7271,10 @@ export async function defaultSpawnBackgroundContinuation(args: {
|
|
|
6791
7271
|
// spoken by a session turn, on the session's terms; it was never meant to
|
|
6792
7272
|
// mean invisible.
|
|
6793
7273
|
//
|
|
6794
|
-
// NOT
|
|
6795
|
-
//
|
|
6796
|
-
// and the detach deliberately waits for
|
|
6797
|
-
//
|
|
7274
|
+
// NOT the conversation's own sender: the voice bridge resets that to a
|
|
7275
|
+
// no-op at turn teardown (see voice-session-bridge's clientCallbackInstalled
|
|
7276
|
+
// reset), and the detach deliberately waits for that teardown before
|
|
7277
|
+
// spawning — so a sender-based route is guaranteed to be dead by the time
|
|
6798
7278
|
// these events fire. `broadcastMessage` is the same path the bridge itself
|
|
6799
7279
|
// uses to reach an attached web client. The subagent events carry
|
|
6800
7280
|
// `parentConversationId`, not `conversationId`, so scope explicitly.
|