@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
|
@@ -17,8 +17,6 @@ import {
|
|
|
17
17
|
getRequestByPendingQuestionOrNull,
|
|
18
18
|
listGuardianRequestDeliveriesOrEmpty,
|
|
19
19
|
} from "../channels/gateway-guardian-requests.js";
|
|
20
|
-
import { loadConfig } from "../config/loader.js";
|
|
21
|
-
import type { VoiceProgressConfig } from "../config/schemas/voice.js";
|
|
22
20
|
import type { TrustContext } from "../daemon/trust-context-types.js";
|
|
23
21
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
|
|
24
22
|
import { computeToolApprovalDigest } from "../security/tool-approval-digest.js";
|
|
@@ -34,14 +32,11 @@ import type { TtsProvider, TtsProviderId } from "../tts/types.js";
|
|
|
34
32
|
import { getLogger } from "../util/logger.js";
|
|
35
33
|
import type { CallAudioFormat } from "./audio-store.js";
|
|
36
34
|
import {
|
|
37
|
-
BARGE_IN_TEARDOWN_WAIT_MS,
|
|
38
|
-
DURATION_WARNING_LEAD_MS,
|
|
39
35
|
getEndCallDrainMaxWaitMs,
|
|
40
36
|
getEndCallListenWindowMs,
|
|
41
37
|
getMaxCallDurationMs,
|
|
42
38
|
getSilenceTimeoutMs,
|
|
43
39
|
getUserConsultationTimeoutMs,
|
|
44
|
-
POST_GOODBYE_HANGUP_DELAY_MS,
|
|
45
40
|
} from "./call-constants.js";
|
|
46
41
|
import {
|
|
47
42
|
formatDuration,
|
|
@@ -65,15 +60,12 @@ import type { CallTransport } from "./call-transport.js";
|
|
|
65
60
|
import { finalizeCall } from "./finalize-call.js";
|
|
66
61
|
import { sendGuardianExpiryNotices } from "./guardian-action-sweep.js";
|
|
67
62
|
import { dispatchGuardianQuestion } from "./guardian-dispatch.js";
|
|
68
|
-
import {
|
|
69
|
-
createVoiceProgressNarrator,
|
|
70
|
-
type VoiceProgressNarrator,
|
|
71
|
-
} from "./progress-narration.js";
|
|
72
63
|
import {
|
|
73
64
|
findPlayableTelephonyTtsFallbackProvider,
|
|
74
65
|
resolveCallTtsProvider,
|
|
75
66
|
resolveSynthesisFormats,
|
|
76
67
|
} from "./resolve-call-tts-provider.js";
|
|
68
|
+
import type { PromptSpeakerContext } from "./speaker-identification.js";
|
|
77
69
|
import {
|
|
78
70
|
resolveTelephonyLanguageVoice,
|
|
79
71
|
resolveTelephonySynthesisLanguage,
|
|
@@ -84,24 +76,16 @@ import {
|
|
|
84
76
|
CALL_OPENING_ACK_MARKER,
|
|
85
77
|
CALL_OPENING_MARKER,
|
|
86
78
|
CALL_VERIFICATION_COMPLETE_MARKER,
|
|
87
|
-
|
|
79
|
+
couldBeControlMarker,
|
|
88
80
|
END_CALL_MARKER,
|
|
89
81
|
extractBalancedJson,
|
|
90
82
|
stripInternalSpeechMarkers,
|
|
91
83
|
} from "./voice-control-protocol.js";
|
|
92
|
-
import {
|
|
93
|
-
createFrontDoorLegCoordinator,
|
|
94
|
-
type FrontDoorLegCoordinator,
|
|
95
|
-
type SpokenEscalationBridge,
|
|
96
|
-
} from "./voice-leg-coordinator.js";
|
|
97
|
-
import { createProgressCadence } from "./voice-progress-cadence.js";
|
|
98
84
|
import {
|
|
99
85
|
CONVERSATION_BUSY_MESSAGE,
|
|
100
86
|
startVoiceTurn,
|
|
101
|
-
type VoiceTurnCallbacks,
|
|
102
87
|
type VoiceTurnHandle,
|
|
103
88
|
} from "./voice-session-bridge.js";
|
|
104
|
-
import { type VoiceRoutingLeg } from "./voice-triage-escalate.js";
|
|
105
89
|
|
|
106
90
|
const log = getLogger("call-controller");
|
|
107
91
|
|
|
@@ -133,32 +117,6 @@ interface PendingEndCall {
|
|
|
133
117
|
wake: (() => void) | null;
|
|
134
118
|
}
|
|
135
119
|
|
|
136
|
-
/**
|
|
137
|
-
* What one voice turn produced, across both of its legs when it escalated.
|
|
138
|
-
* Feeds post-turn marker detection and the call event log.
|
|
139
|
-
*/
|
|
140
|
-
interface VoiceTurnOutcome {
|
|
141
|
-
/**
|
|
142
|
-
* The filtered response text: what the caller heard, plus the control
|
|
143
|
-
* markers the model emitted for the controller to act on.
|
|
144
|
-
*/
|
|
145
|
-
text: string;
|
|
146
|
-
/** Persisted row of the caller's utterance, when the bridge reported it. */
|
|
147
|
-
userMessageId: string | null;
|
|
148
|
-
/**
|
|
149
|
-
* Persisted row of the leg that answered: the escalated leg's when the
|
|
150
|
-
* turn escalated, else the front-door leg's own.
|
|
151
|
-
*/
|
|
152
|
-
assistantMessageId: string | null;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/** One leg of a triaged phone turn, as handed to the session bridge. */
|
|
156
|
-
interface PhoneVoiceLeg {
|
|
157
|
-
content: string;
|
|
158
|
-
routingLeg: VoiceRoutingLeg;
|
|
159
|
-
spokenEscalationBridge?: string;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
120
|
export class CallController {
|
|
163
121
|
private callSessionId: string;
|
|
164
122
|
private transport: CallTransport;
|
|
@@ -243,11 +201,6 @@ export class CallController {
|
|
|
243
201
|
* resolution only.
|
|
244
202
|
*/
|
|
245
203
|
private resolveSynthesisLanguage: () => string | undefined;
|
|
246
|
-
/** Spoken progress narration (voice.frontModel.progress), null when off. */
|
|
247
|
-
private readonly progressNarrator: VoiceProgressNarrator | null;
|
|
248
|
-
private readonly progressConfig: VoiceProgressConfig;
|
|
249
|
-
/** Rotates the static narration fallback across turns. */
|
|
250
|
-
private progressPhraseCounter = 0;
|
|
251
204
|
|
|
252
205
|
constructor(
|
|
253
206
|
callSessionId: string,
|
|
@@ -258,9 +211,6 @@ export class CallController {
|
|
|
258
211
|
assistantId?: string;
|
|
259
212
|
trustContext?: TrustContext;
|
|
260
213
|
resolveSynthesisLanguage?: () => string | undefined;
|
|
261
|
-
/** Test seam: an explicit null disables narration for the call. */
|
|
262
|
-
progressNarrator?: VoiceProgressNarrator | null;
|
|
263
|
-
progressConfig?: VoiceProgressConfig;
|
|
264
214
|
},
|
|
265
215
|
) {
|
|
266
216
|
this.callSessionId = callSessionId;
|
|
@@ -273,12 +223,6 @@ export class CallController {
|
|
|
273
223
|
this.resolveSynthesisLanguage =
|
|
274
224
|
opts?.resolveSynthesisLanguage ??
|
|
275
225
|
(() => resolveTelephonySynthesisLanguage());
|
|
276
|
-
this.progressConfig =
|
|
277
|
-
opts?.progressConfig ?? loadConfig().voice.frontModel.progress;
|
|
278
|
-
this.progressNarrator =
|
|
279
|
-
opts?.progressNarrator !== undefined
|
|
280
|
-
? opts.progressNarrator
|
|
281
|
-
: createVoiceProgressNarrator({ config: this.progressConfig });
|
|
282
226
|
|
|
283
227
|
// Resolve the conversation ID and skipDisclosure from the call session
|
|
284
228
|
const session = getCallSession(callSessionId);
|
|
@@ -369,7 +313,10 @@ export class CallController {
|
|
|
369
313
|
* Caller utterances always trigger normal turns, even when a guardian
|
|
370
314
|
* consultation is pending — the consultation is tracked separately.
|
|
371
315
|
*/
|
|
372
|
-
async handleCallerUtterance(
|
|
316
|
+
async handleCallerUtterance(
|
|
317
|
+
transcript: string,
|
|
318
|
+
speaker?: PromptSpeakerContext,
|
|
319
|
+
): Promise<void> {
|
|
373
320
|
// If the caller speaks while an END_CALL teardown is pending (during the
|
|
374
321
|
// drain wait or the listen window), this is a deferral — the caller is
|
|
375
322
|
// re-engaging after we tried to hang up. Track it so we can cap repeats.
|
|
@@ -396,15 +343,13 @@ export class CallController {
|
|
|
396
343
|
this.currentTurnPromise = null;
|
|
397
344
|
await Promise.race([
|
|
398
345
|
teardownPromise.catch(() => {}),
|
|
399
|
-
new Promise<void>((resolve) =>
|
|
400
|
-
setTimeout(resolve, BARGE_IN_TEARDOWN_WAIT_MS),
|
|
401
|
-
),
|
|
346
|
+
new Promise<void>((resolve) => setTimeout(resolve, 2000)),
|
|
402
347
|
]);
|
|
403
348
|
}
|
|
404
349
|
|
|
405
350
|
this.state = "processing";
|
|
406
351
|
this.resetSilenceTimer();
|
|
407
|
-
const callerContent = transcript;
|
|
352
|
+
const callerContent = this.formatCallerUtterance(transcript, speaker);
|
|
408
353
|
const shouldMarkOpeningAck = this.awaitingOpeningAck;
|
|
409
354
|
if (shouldMarkOpeningAck) {
|
|
410
355
|
this.awaitingOpeningAck = false;
|
|
@@ -488,35 +433,37 @@ export class CallController {
|
|
|
488
433
|
}
|
|
489
434
|
|
|
490
435
|
/**
|
|
491
|
-
* Handle a barge-in from
|
|
436
|
+
* Handle a barge-in attempt from inbound caller audio.
|
|
492
437
|
*
|
|
493
|
-
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
* gate only says whether there is anything to cancel.
|
|
438
|
+
* Only interrupts the in-flight turn when the assistant is actively
|
|
439
|
+
* speaking. When the controller is idle or still processing (no TTS
|
|
440
|
+
* output yet), the barge-in is ignored — this prevents false
|
|
441
|
+
* interruption on initial inbound media frames that arrive before
|
|
442
|
+
* the assistant has had a chance to produce its first response.
|
|
499
443
|
*
|
|
500
|
-
* @param onAccepted Invoked synchronously after the gate
|
|
501
|
-
* before {@link handleInterrupt} runs. Transports use this
|
|
502
|
-
* queued outbound audio without wiping the end-of-turn mark
|
|
503
|
-
* handleInterrupt enqueues
|
|
504
|
-
* barge-in is ignored.
|
|
505
|
-
* @returns `true` if the barge-in was accepted (
|
|
506
|
-
* `false` if it was ignored (assistant idle).
|
|
444
|
+
* @param onAccepted Invoked synchronously after the speaking gate
|
|
445
|
+
* passes but before {@link handleInterrupt} runs. Transports use this
|
|
446
|
+
* to flush queued outbound audio without wiping the end-of-turn mark
|
|
447
|
+
* that handleInterrupt enqueues — and without flushing at all when
|
|
448
|
+
* the barge-in is ignored.
|
|
449
|
+
* @returns `true` if the barge-in was accepted (assistant was speaking),
|
|
450
|
+
* `false` if it was ignored (assistant idle or processing).
|
|
507
451
|
*/
|
|
508
452
|
handleBargeIn(onAccepted?: () => void): boolean {
|
|
509
|
-
if (this.state
|
|
453
|
+
if (this.state !== "speaking") {
|
|
510
454
|
log.debug(
|
|
511
|
-
{
|
|
512
|
-
|
|
455
|
+
{
|
|
456
|
+
callSessionId: this.callSessionId,
|
|
457
|
+
state: this.state,
|
|
458
|
+
},
|
|
459
|
+
"Barge-in ignored — assistant not speaking",
|
|
513
460
|
);
|
|
514
461
|
return false;
|
|
515
462
|
}
|
|
516
463
|
|
|
517
464
|
log.info(
|
|
518
|
-
{ callSessionId: this.callSessionId
|
|
519
|
-
"Barge-in accepted
|
|
465
|
+
{ callSessionId: this.callSessionId },
|
|
466
|
+
"Barge-in accepted — interrupting assistant speech",
|
|
520
467
|
);
|
|
521
468
|
onAccepted?.();
|
|
522
469
|
this.handleInterrupt();
|
|
@@ -524,12 +471,11 @@ export class CallController {
|
|
|
524
471
|
}
|
|
525
472
|
|
|
526
473
|
/**
|
|
527
|
-
* Handle caller interrupting the assistant's
|
|
474
|
+
* Handle caller interrupting the assistant's speech.
|
|
528
475
|
*
|
|
529
476
|
* This is the hard interrupt path used for explicit teardown and
|
|
530
477
|
* internal abort scenarios. For barge-in from inbound audio, prefer
|
|
531
|
-
* {@link handleBargeIn}
|
|
532
|
-
* the transport's sustained-speech guard to vouch for the interruption.
|
|
478
|
+
* {@link handleBargeIn} which gates on the speaking state.
|
|
533
479
|
*/
|
|
534
480
|
handleInterrupt(): void {
|
|
535
481
|
const wasSpeaking = this.state === "speaking";
|
|
@@ -639,6 +585,22 @@ export class CallController {
|
|
|
639
585
|
}
|
|
640
586
|
}
|
|
641
587
|
|
|
588
|
+
private formatCallerUtterance(
|
|
589
|
+
transcript: string,
|
|
590
|
+
speaker?: PromptSpeakerContext,
|
|
591
|
+
): string {
|
|
592
|
+
if (!speaker) {
|
|
593
|
+
return transcript;
|
|
594
|
+
}
|
|
595
|
+
const safeId = speaker.speakerId.replaceAll('"', "'");
|
|
596
|
+
const safeLabel = speaker.speakerLabel.replaceAll('"', "'");
|
|
597
|
+
const confidencePart =
|
|
598
|
+
speaker.speakerConfidence != null
|
|
599
|
+
? ` confidence="${speaker.speakerConfidence.toFixed(2)}"`
|
|
600
|
+
: "";
|
|
601
|
+
return `[SPEAKER id="${safeId}" label="${safeLabel}" source="${speaker.source}"${confidencePart}] ${transcript}`;
|
|
602
|
+
}
|
|
603
|
+
|
|
642
604
|
/**
|
|
643
605
|
* Execute a single voice turn through the conversation pipeline and stream
|
|
644
606
|
* the response back through the relay.
|
|
@@ -655,9 +617,6 @@ export class CallController {
|
|
|
655
617
|
}
|
|
656
618
|
const runVersion = ++this.llmRunVersion;
|
|
657
619
|
const runSignal = this.abortController.signal;
|
|
658
|
-
// Stamped before any pre-bridge work so the bridge's dispatch-timing log
|
|
659
|
-
// attributes the whole turn, TTS provider resolution included.
|
|
660
|
-
const launchedAtMs = Date.now();
|
|
661
620
|
|
|
662
621
|
// Clear silence timer while actively processing. The caller said
|
|
663
622
|
// something (or a turn was triggered), so silence detection should
|
|
@@ -670,21 +629,19 @@ export class CallController {
|
|
|
670
629
|
try {
|
|
671
630
|
// Stay in `processing` through the lock-wait and LLM generation; flip to
|
|
672
631
|
// `speaking` only when real outbound audio/tokens start (see
|
|
673
|
-
// beginSpeaking).
|
|
674
|
-
// is audible; either phase is interruptible by sustained caller speech.
|
|
632
|
+
// beginSpeaking). This keeps barge-in from aborting a silent turn.
|
|
675
633
|
this.state = "processing";
|
|
676
634
|
|
|
677
|
-
const
|
|
635
|
+
const fullResponseText = await this.streamTtsTokens(
|
|
678
636
|
content,
|
|
679
637
|
runVersion,
|
|
680
638
|
runSignal,
|
|
681
|
-
launchedAtMs,
|
|
682
639
|
);
|
|
683
640
|
if (!this.isCurrentRun(runVersion)) {
|
|
684
641
|
return;
|
|
685
642
|
}
|
|
686
643
|
|
|
687
|
-
await this.handleTurnCompletion(
|
|
644
|
+
await this.handleTurnCompletion(fullResponseText);
|
|
688
645
|
} catch (err: unknown) {
|
|
689
646
|
this.currentTurnHandle = null;
|
|
690
647
|
// Aborted requests are expected (interruptions, rapid utterances)
|
|
@@ -743,25 +700,15 @@ export class CallController {
|
|
|
743
700
|
}
|
|
744
701
|
|
|
745
702
|
/**
|
|
746
|
-
*
|
|
747
|
-
*
|
|
748
|
-
*
|
|
749
|
-
*
|
|
750
|
-
* Every turn opens on the front-door leg: a fast, toolless model that
|
|
751
|
-
* either answers outright or hands off with a spoken holding phrase to an
|
|
752
|
-
* escalated leg on the conversation's own model. Both legs stream through
|
|
753
|
-
* the same TTS machinery and share this turn's single end-of-turn signal,
|
|
754
|
-
* so the caller never gets a listening window between the bridge and the
|
|
755
|
-
* answer. Phone turns commit on the STT provider's utterance-boundary
|
|
756
|
-
* final and carry no partial transcript, so the front-door leg never
|
|
757
|
-
* holds: routing is escalate-only.
|
|
703
|
+
* Stream TTS tokens from the conversation pipeline, buffering to strip
|
|
704
|
+
* control markers before they reach the relay. Returns the full
|
|
705
|
+
* accumulated response text for post-turn marker detection.
|
|
758
706
|
*/
|
|
759
707
|
private async streamTtsTokens(
|
|
760
708
|
content: string,
|
|
761
709
|
runVersion: number,
|
|
762
710
|
runSignal: AbortSignal,
|
|
763
|
-
|
|
764
|
-
): Promise<VoiceTurnOutcome> {
|
|
711
|
+
): Promise<string> {
|
|
765
712
|
// Resolve the active TTS provider through the global abstraction.
|
|
766
713
|
// The catalog's callMode determines the call path: synthesized-play
|
|
767
714
|
// providers synthesize each speakable segment via the provider API as
|
|
@@ -780,18 +727,14 @@ export class CallController {
|
|
|
780
727
|
// Buffer incoming tokens so we can strip control markers ([ASK_GUARDIAN:...], [END_CALL])
|
|
781
728
|
// before they reach TTS. We hold text whenever an unmatched '[' appears, since it
|
|
782
729
|
// could be the start of a control marker.
|
|
730
|
+
let ttsBuffer = "";
|
|
783
731
|
let fullResponseText = "";
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
// but not yet emitted, text deltas seen (a narration generated while the
|
|
791
|
-
// model was already speaking is stale), and whether the turn is over.
|
|
792
|
-
let segmentsInFlight = 0;
|
|
793
|
-
let deltaEpoch = 0;
|
|
794
|
-
let turnSettled = false;
|
|
732
|
+
// Reasoning models can inline <think> spans in the content stream when a
|
|
733
|
+
// profile has not opted into parseThinkTags. Neither the spoken path nor
|
|
734
|
+
// the post-turn consumers of fullResponseText (transcripts,
|
|
735
|
+
// assistant_spoke, END_CALL/ASK_GUARDIAN detection) may see them: both
|
|
736
|
+
// are fed only filtered text.
|
|
737
|
+
const reasoningFilter = createReasoningTagFilter();
|
|
795
738
|
|
|
796
739
|
// Synthesized path: text is split at speakable boundaries as it streams
|
|
797
740
|
// and each segment is synthesized while the LLM keeps generating. The
|
|
@@ -827,7 +770,6 @@ export class CallController {
|
|
|
827
770
|
const speakSegmentViaPcmFallback = async (
|
|
828
771
|
failedProviderId: string,
|
|
829
772
|
segment: string,
|
|
830
|
-
language: string | undefined,
|
|
831
773
|
): Promise<void> => {
|
|
832
774
|
if (pcmFallbackProvider === undefined) {
|
|
833
775
|
pcmFallbackProvider =
|
|
@@ -859,7 +801,6 @@ export class CallController {
|
|
|
859
801
|
segment,
|
|
860
802
|
runVersion,
|
|
861
803
|
audioFormat,
|
|
862
|
-
language,
|
|
863
804
|
);
|
|
864
805
|
if (fallbackStatus !== "ok") {
|
|
865
806
|
// The fallback provider is failing too — stop retrying.
|
|
@@ -867,12 +808,9 @@ export class CallController {
|
|
|
867
808
|
}
|
|
868
809
|
};
|
|
869
810
|
|
|
870
|
-
// `language` overrides the turn's synthesis language for a fixed phrase
|
|
871
|
-
// whose text is not in the caller's language; undefined rides the turn's.
|
|
872
811
|
const enqueueSynthesisSegments = (
|
|
873
812
|
ttsProvider: TtsProvider,
|
|
874
813
|
segments: string[],
|
|
875
|
-
language?: string,
|
|
876
814
|
): void => {
|
|
877
815
|
for (const rawSegment of segments) {
|
|
878
816
|
// Sanitized per segment (not per delta) so markdown spanning deltas
|
|
@@ -882,23 +820,21 @@ export class CallController {
|
|
|
882
820
|
continue;
|
|
883
821
|
}
|
|
884
822
|
firstSynthSegmentEnqueued = true;
|
|
885
|
-
segmentsInFlight += 1;
|
|
886
823
|
synthesisChain = synthesisChain.then(async () => {
|
|
824
|
+
if (
|
|
825
|
+
!this.isCurrentRun(runVersion) ||
|
|
826
|
+
synthesisFailure ||
|
|
827
|
+
synthesisCancelled
|
|
828
|
+
) {
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
887
831
|
try {
|
|
888
|
-
if (
|
|
889
|
-
!this.isCurrentRun(runVersion) ||
|
|
890
|
-
synthesisFailure ||
|
|
891
|
-
synthesisCancelled
|
|
892
|
-
) {
|
|
893
|
-
return;
|
|
894
|
-
}
|
|
895
832
|
if (!synthesisFellBack) {
|
|
896
833
|
const status = await this.synthesizeAndStreamAudio(
|
|
897
834
|
ttsProvider,
|
|
898
835
|
segment,
|
|
899
836
|
runVersion,
|
|
900
837
|
audioFormat,
|
|
901
|
-
language,
|
|
902
838
|
);
|
|
903
839
|
if (status === "ok") {
|
|
904
840
|
return;
|
|
@@ -924,14 +860,9 @@ export class CallController {
|
|
|
924
860
|
this.transport.sendTextToken(`${segment} `, false);
|
|
925
861
|
return;
|
|
926
862
|
}
|
|
927
|
-
await speakSegmentViaPcmFallback(ttsProvider.id, segment
|
|
863
|
+
await speakSegmentViaPcmFallback(ttsProvider.id, segment);
|
|
928
864
|
} catch (err) {
|
|
929
865
|
synthesisFailure = { err };
|
|
930
|
-
} finally {
|
|
931
|
-
segmentsInFlight -= 1;
|
|
932
|
-
// The segment's audio has been handed off; the transport's
|
|
933
|
-
// playback tail covers what is still playing.
|
|
934
|
-
cadence.noteAudioSettled();
|
|
935
866
|
}
|
|
936
867
|
});
|
|
937
868
|
}
|
|
@@ -957,367 +888,150 @@ export class CallController {
|
|
|
957
888
|
}
|
|
958
889
|
this.beginSpeakingOnAudioStart(runVersion);
|
|
959
890
|
this.transport.sendTextToken(cleaned, false);
|
|
960
|
-
cadence.noteAudioSettled();
|
|
961
891
|
}
|
|
962
892
|
};
|
|
963
893
|
|
|
964
|
-
|
|
965
|
-
* Speak a fixed phrase from a localized table (already one complete
|
|
966
|
-
* sentence, no markers) outside the model-text stream. `language` is the
|
|
967
|
-
* hint the phrase must carry: "en" when the table lacked the caller's
|
|
968
|
-
* language, so the English text is not rendered under a ko/ar/ta hint
|
|
969
|
-
* (on the native route that is the transport's system-copy contract);
|
|
970
|
-
* undefined rides the turn's language like model text.
|
|
971
|
-
*/
|
|
972
|
-
const speakFixedPhrase = (
|
|
973
|
-
text: string,
|
|
974
|
-
language: string | undefined,
|
|
975
|
-
): boolean => {
|
|
976
|
-
const cleaned = sanitizeForTts(text).trim();
|
|
977
|
-
if (cleaned.length === 0) {
|
|
978
|
-
return false;
|
|
979
|
-
}
|
|
980
|
-
if (synthProvider) {
|
|
981
|
-
enqueueSynthesisSegments(synthProvider, [cleaned], language);
|
|
982
|
-
return true;
|
|
983
|
-
}
|
|
984
|
-
this.beginSpeakingOnAudioStart(runVersion);
|
|
985
|
-
this.transport.sendTextToken(`${cleaned} `, false, {
|
|
986
|
-
systemCopy: language !== undefined,
|
|
987
|
-
});
|
|
988
|
-
cadence.noteAudioSettled();
|
|
989
|
-
return true;
|
|
990
|
-
};
|
|
991
|
-
|
|
992
|
-
// Spoken progress narration for the turn's dead air (tool loops on the
|
|
993
|
-
// escalated leg). The shared cadence owns the timing; this host supplies
|
|
994
|
-
// the phone's view of audible silence: nothing buffered toward a
|
|
995
|
-
// sentence, no segment mid-synthesis, and the transport's queue empty
|
|
996
|
-
// with its playback-tail estimate expired.
|
|
997
|
-
const callerTranscript =
|
|
998
|
-
content === CALL_OPENING_MARKER ||
|
|
999
|
-
content === CALL_VERIFICATION_COMPLETE_MARKER
|
|
1000
|
-
? ""
|
|
1001
|
-
: content;
|
|
1002
|
-
const cadence = createProgressCadence({
|
|
1003
|
-
config: this.progressConfig,
|
|
1004
|
-
narrator: this.progressNarrator,
|
|
1005
|
-
nextFallbackPhraseIndex: () => this.progressPhraseCounter++,
|
|
1006
|
-
host: {
|
|
1007
|
-
turnId: `${this.callSessionId}#${runVersion}`,
|
|
1008
|
-
launchedAtMs,
|
|
1009
|
-
signal: runSignal,
|
|
1010
|
-
canNarrate: () => this.isCurrentRun(runVersion) && !turnSettled,
|
|
1011
|
-
isAudioIdle: () =>
|
|
1012
|
-
pendingSynthText.length === 0 &&
|
|
1013
|
-
segmentsInFlight === 0 &&
|
|
1014
|
-
(this.transport.isPlaybackIdle?.() ?? true),
|
|
1015
|
-
playbackTailUntilMs: () => this.transport.playbackTailUntilMs?.() ?? 0,
|
|
1016
|
-
transcriptSoFar: () => callerTranscript,
|
|
1017
|
-
language: () => this.resolveSynthesisLanguage(),
|
|
1018
|
-
deltaEpoch: () => deltaEpoch,
|
|
1019
|
-
speak: speakFixedPhrase,
|
|
1020
|
-
},
|
|
1021
|
-
});
|
|
1022
|
-
const settleTurn = (): void => {
|
|
1023
|
-
turnSettled = true;
|
|
1024
|
-
cadence.clear();
|
|
1025
|
-
};
|
|
1026
|
-
runSignal.addEventListener("abort", settleTurn, { once: true });
|
|
1027
|
-
|
|
1028
|
-
// Speech goes out through the shared control-marker holdback: text up to
|
|
1029
|
-
// a possibly-streaming marker flushes, the marker itself is stripped, and
|
|
1030
|
-
// ordinary bracketed text ("[A]", "[note]") never stalls TTS. Marker
|
|
1031
|
-
// detection for actions (END_CALL, ASK_GUARDIAN) reads fullResponseText
|
|
1032
|
-
// separately, after the turn.
|
|
1033
|
-
const flushSafeText = createControlMarkerHoldback((chunk) => {
|
|
894
|
+
const flushSafeText = (): void => {
|
|
1034
895
|
if (!this.isCurrentRun(runVersion)) {
|
|
1035
896
|
return;
|
|
1036
897
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
if (!this.isCurrentRun(runVersion) || routingLeg === "escalated") {
|
|
1051
|
-
return;
|
|
1052
|
-
}
|
|
1053
|
-
outcome.userMessageId = messageId;
|
|
1054
|
-
},
|
|
1055
|
-
persisted_assistant_message_id: (messageId) => {
|
|
1056
|
-
if (!this.isCurrentRun(runVersion) || handedOff()) {
|
|
1057
|
-
return;
|
|
898
|
+
if (ttsBuffer.length === 0) {
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
const bracketIdx = ttsBuffer.indexOf("[");
|
|
902
|
+
if (bracketIdx === -1) {
|
|
903
|
+
// No bracket at all — safe to flush everything
|
|
904
|
+
emitSafeChunk(ttsBuffer);
|
|
905
|
+
ttsBuffer = "";
|
|
906
|
+
} else {
|
|
907
|
+
// Flush everything before the bracket
|
|
908
|
+
if (bracketIdx > 0) {
|
|
909
|
+
emitSafeChunk(ttsBuffer.slice(0, bracketIdx));
|
|
910
|
+
ttsBuffer = ttsBuffer.slice(bracketIdx);
|
|
1058
911
|
}
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
912
|
+
|
|
913
|
+
// Only hold the buffer if the bracket text could be the start of a
|
|
914
|
+
// known control marker. Otherwise flush immediately so ordinary
|
|
915
|
+
// bracketed text (e.g. "[A]", "[note]") doesn't stall TTS.
|
|
916
|
+
const afterBracket = ttsBuffer;
|
|
917
|
+
const couldBeControl = couldBeControlMarker(afterBracket);
|
|
918
|
+
|
|
919
|
+
if (!couldBeControl) {
|
|
920
|
+
// Not a control marker prefix — flush up to the next '[' (if any)
|
|
921
|
+
const nextBracket = ttsBuffer.indexOf("[", 1);
|
|
922
|
+
if (nextBracket === -1) {
|
|
923
|
+
emitSafeChunk(ttsBuffer);
|
|
924
|
+
ttsBuffer = "";
|
|
925
|
+
} else {
|
|
926
|
+
emitSafeChunk(ttsBuffer.slice(0, nextBracket));
|
|
927
|
+
ttsBuffer = ttsBuffer.slice(nextBracket);
|
|
928
|
+
}
|
|
1067
929
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
},
|
|
1076
|
-
tool_result: (event) => {
|
|
930
|
+
// Otherwise hold it — might be a control marker still being streamed
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
// Use a promise to track completion of the voice turn
|
|
935
|
+
const turnComplete = new Promise<void>((resolve, reject) => {
|
|
936
|
+
const onTextDelta = (text: string): void => {
|
|
1077
937
|
if (!this.isCurrentRun(runVersion)) {
|
|
1078
938
|
return;
|
|
1079
939
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
}
|
|
1090
|
-
});
|
|
1091
|
-
|
|
1092
|
-
// Speak the escalation bridge so the strong-model call has no dead air.
|
|
1093
|
-
// The model's own holding phrase is real assistant speech and stays in
|
|
1094
|
-
// the turn's text, matching the row the bridge's transcript hygiene
|
|
1095
|
-
// keeps for it; the canned fallback is audio-only, matching the row it
|
|
1096
|
-
// deletes.
|
|
1097
|
-
const speakEscalationBridge = (bridge: SpokenEscalationBridge): void => {
|
|
1098
|
-
if (bridge.usesFallback) {
|
|
1099
|
-
speakFixedPhrase(bridge.spokenBridge, bridge.language);
|
|
1100
|
-
return;
|
|
1101
|
-
}
|
|
1102
|
-
fullResponseText += `${bridge.spokenBridge} `;
|
|
1103
|
-
flushSafeText(fullResponseText, { force: true });
|
|
1104
|
-
// Force-synthesize the bridge now. On the synthesized-TTS path text is
|
|
1105
|
-
// held in pendingSynthText until a sentence boundary, so an
|
|
1106
|
-
// unpunctuated bridge would otherwise sit unspoken until the post-leg
|
|
1107
|
-
// drain, leaving the caller in silence during the escalated model's
|
|
1108
|
-
// call: the exact gap the bridge exists to mask.
|
|
1109
|
-
if (synthProvider) {
|
|
1110
|
-
const { segments } = extractSpeakableSegments(pendingSynthText, true);
|
|
1111
|
-
pendingSynthText = "";
|
|
1112
|
-
enqueueSynthesisSegments(synthProvider, segments);
|
|
1113
|
-
}
|
|
1114
|
-
};
|
|
940
|
+
// One filter feeds both consumers: the spoken stream and the text
|
|
941
|
+
// used for transcripts, assistant_spoke, and END_CALL/ASK_GUARDIAN
|
|
942
|
+
// marker detection. A control marker inside a reasoning span must
|
|
943
|
+
// never trigger a real action the caller did not hear.
|
|
944
|
+
const speakable = reasoningFilter.push(text);
|
|
945
|
+
fullResponseText += speakable;
|
|
946
|
+
ttsBuffer += speakable;
|
|
947
|
+
ttsBuffer = stripInternalSpeechMarkers(ttsBuffer);
|
|
948
|
+
flushSafeText();
|
|
949
|
+
};
|
|
1115
950
|
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
let escalatedLeg: Promise<void> | null = null;
|
|
1120
|
-
|
|
1121
|
-
// Run one leg through the session bridge, streaming its deltas through
|
|
1122
|
-
// the shared TTS closures above. Errors cancel and drain this turn's
|
|
1123
|
-
// synthesis before propagating.
|
|
1124
|
-
const runVoiceLeg = async (leg: PhoneVoiceLeg): Promise<void> => {
|
|
1125
|
-
// Reasoning models can inline <think> spans in the content stream when
|
|
1126
|
-
// a profile has not opted into parseThinkTags. Neither the spoken path
|
|
1127
|
-
// nor the post-turn consumers of fullResponseText (transcripts,
|
|
1128
|
-
// assistant_spoke, END_CALL/ASK_GUARDIAN detection) may see them: both
|
|
1129
|
-
// are fed only filtered text. One filter per leg: a span never straddles
|
|
1130
|
-
// the hand-off.
|
|
1131
|
-
const reasoningFilter = createReasoningTagFilter();
|
|
1132
|
-
let legHandle: VoiceTurnHandle | null = null;
|
|
1133
|
-
// Verdict-first: a front-door leg's leading tokens decide the turn's
|
|
1134
|
-
// fate, so its raw stream is a control plane until they classify. The
|
|
1135
|
-
// shared coordinator reads it and sequences the hand-off; only text it
|
|
1136
|
-
// releases is spoken or recorded. Phone has no partial transcripts,
|
|
1137
|
-
// so the hold verdict is never taught.
|
|
1138
|
-
const coordinator: FrontDoorLegCoordinator | null =
|
|
1139
|
-
leg.routingLeg === "front-door"
|
|
1140
|
-
? createFrontDoorLegCoordinator({
|
|
1141
|
-
holdEnabled: false,
|
|
1142
|
-
host: {
|
|
1143
|
-
isLive: () =>
|
|
1144
|
-
this.isCurrentRun(runVersion) && !runSignal.aborted,
|
|
1145
|
-
language: () => this.resolveSynthesisLanguage(),
|
|
1146
|
-
progress: cadence,
|
|
1147
|
-
onAnswerText: (text) => {
|
|
1148
|
-
fullResponseText += text;
|
|
1149
|
-
flushSafeText(fullResponseText);
|
|
1150
|
-
},
|
|
1151
|
-
abortLeg: () => legHandle?.abort(),
|
|
1152
|
-
speakBridge: speakEscalationBridge,
|
|
1153
|
-
startEscalatedLeg: (escalated) => {
|
|
1154
|
-
const started = runVoiceLeg(escalated);
|
|
1155
|
-
// Awaited by the turn; the rejection is observed there.
|
|
1156
|
-
started.catch(() => {});
|
|
1157
|
-
escalatedLeg = started;
|
|
1158
|
-
},
|
|
1159
|
-
},
|
|
1160
|
-
})
|
|
1161
|
-
: null;
|
|
951
|
+
const onComplete = (): void => {
|
|
952
|
+
resolve();
|
|
953
|
+
};
|
|
1162
954
|
|
|
1163
|
-
const
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
deltaEpoch += 1;
|
|
1167
|
-
}
|
|
1168
|
-
if (coordinator !== null) {
|
|
1169
|
-
coordinator.push(speakable);
|
|
1170
|
-
return;
|
|
1171
|
-
}
|
|
1172
|
-
fullResponseText += speakable;
|
|
1173
|
-
flushSafeText(fullResponseText);
|
|
1174
|
-
};
|
|
955
|
+
const onError = (message: string): void => {
|
|
956
|
+
reject(new Error(message));
|
|
957
|
+
};
|
|
1175
958
|
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
959
|
+
// Start the voice turn through the session bridge
|
|
960
|
+
startVoiceTurn({
|
|
961
|
+
conversationId: this.conversationId,
|
|
962
|
+
callSessionId: this.callSessionId,
|
|
963
|
+
content,
|
|
964
|
+
assistantId: this.assistantId,
|
|
965
|
+
trustContext: this.trustContext ?? undefined,
|
|
966
|
+
isInbound: this.isInbound,
|
|
967
|
+
task: this.task,
|
|
968
|
+
skipDisclosure: this.skipDisclosure,
|
|
969
|
+
onTextDelta,
|
|
970
|
+
onComplete,
|
|
971
|
+
onError,
|
|
972
|
+
signal: runSignal,
|
|
973
|
+
})
|
|
974
|
+
.then((handle) => {
|
|
975
|
+
if (this.isCurrentRun(runVersion)) {
|
|
976
|
+
this.currentTurnHandle = handle;
|
|
977
|
+
} else {
|
|
978
|
+
// Turn was superseded before handle arrived; abort immediately
|
|
979
|
+
handle.abort();
|
|
1181
980
|
}
|
|
1182
|
-
|
|
1183
|
-
|
|
981
|
+
})
|
|
982
|
+
.catch((err) => {
|
|
983
|
+
reject(err);
|
|
984
|
+
});
|
|
1184
985
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
runSignal.aborted ||
|
|
1192
|
-
coordinator?.handedOff
|
|
1193
|
-
) {
|
|
1194
|
-
resolve();
|
|
1195
|
-
return;
|
|
1196
|
-
}
|
|
1197
|
-
// A held "[..." tail that never completed a marker is real text,
|
|
1198
|
-
// released to both consumers before the leg settles.
|
|
1199
|
-
ingest(reasoningFilter.flush());
|
|
1200
|
-
coordinator?.complete();
|
|
986
|
+
// Defensive: if the turn is aborted (e.g. barge-in) and the event
|
|
987
|
+
// sink callbacks are never invoked, resolve the promise so it
|
|
988
|
+
// doesn't hang forever.
|
|
989
|
+
runSignal.addEventListener(
|
|
990
|
+
"abort",
|
|
991
|
+
() => {
|
|
1201
992
|
resolve();
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
};
|
|
1207
|
-
|
|
1208
|
-
startVoiceTurn({
|
|
1209
|
-
conversationId: this.conversationId,
|
|
1210
|
-
callSessionId: this.callSessionId,
|
|
1211
|
-
// The call session is the phone analogue of a live-voice session:
|
|
1212
|
-
// its id joins the turn's telemetry row to the call, and the entry
|
|
1213
|
-
// names the direction the call came from.
|
|
1214
|
-
voiceTelemetry: {
|
|
1215
|
-
sessionId: this.callSessionId,
|
|
1216
|
-
entry: this.isInbound ? "phone_inbound" : "phone_outbound",
|
|
1217
|
-
},
|
|
1218
|
-
content: leg.content,
|
|
1219
|
-
assistantId: this.assistantId,
|
|
1220
|
-
trustContext: this.trustContext ?? undefined,
|
|
1221
|
-
isInbound: this.isInbound,
|
|
1222
|
-
task: this.task,
|
|
1223
|
-
skipDisclosure: this.skipDisclosure,
|
|
1224
|
-
launchedAtMs,
|
|
1225
|
-
routingLeg: leg.routingLeg,
|
|
1226
|
-
...(leg.spokenEscalationBridge !== undefined
|
|
1227
|
-
? { spokenEscalationBridge: leg.spokenEscalationBridge }
|
|
1228
|
-
: {}),
|
|
1229
|
-
onTextDelta,
|
|
1230
|
-
onComplete,
|
|
1231
|
-
onError,
|
|
1232
|
-
callbacks: legCallbacks(
|
|
1233
|
-
leg.routingLeg,
|
|
1234
|
-
() => coordinator?.handedOff === true,
|
|
1235
|
-
),
|
|
1236
|
-
signal: runSignal,
|
|
1237
|
-
})
|
|
1238
|
-
.then((handle) => {
|
|
1239
|
-
legHandle = handle;
|
|
1240
|
-
if (this.isCurrentRun(runVersion) && !coordinator?.handedOff) {
|
|
1241
|
-
this.currentTurnHandle = handle;
|
|
1242
|
-
} else {
|
|
1243
|
-
// Superseded, or the front-door leg handed off before its
|
|
1244
|
-
// handle arrived: abort immediately.
|
|
1245
|
-
handle.abort();
|
|
1246
|
-
}
|
|
1247
|
-
})
|
|
1248
|
-
.catch((err) => {
|
|
1249
|
-
reject(err);
|
|
1250
|
-
});
|
|
1251
|
-
|
|
1252
|
-
// Defensive: if the turn is aborted (e.g. barge-in) and the event
|
|
1253
|
-
// sink callbacks are never invoked, resolve the promise so it
|
|
1254
|
-
// doesn't hang forever.
|
|
1255
|
-
runSignal.addEventListener(
|
|
1256
|
-
"abort",
|
|
1257
|
-
() => {
|
|
1258
|
-
resolve();
|
|
1259
|
-
},
|
|
1260
|
-
{ once: true },
|
|
1261
|
-
);
|
|
1262
|
-
});
|
|
993
|
+
},
|
|
994
|
+
{ once: true },
|
|
995
|
+
);
|
|
996
|
+
});
|
|
1263
997
|
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
}
|
|
1280
|
-
settleTurn();
|
|
1281
|
-
await synthesisChain.catch(() => {});
|
|
1282
|
-
throw err;
|
|
998
|
+
// Eagerly mark the rejection as handled so runtimes (e.g. bun) don't
|
|
999
|
+
// flag it as an unhandled rejection when onError fires synchronously
|
|
1000
|
+
// inside the Promise constructor before this await adds its handler.
|
|
1001
|
+
// The await below still re-throws, caught by the outer try-catch.
|
|
1002
|
+
turnComplete.catch(() => {});
|
|
1003
|
+
try {
|
|
1004
|
+
await turnComplete;
|
|
1005
|
+
} catch (err) {
|
|
1006
|
+
// Cancel and settle this turn's synthesis before the error reaches
|
|
1007
|
+
// the outer handler, so no straggling segment plays the partial
|
|
1008
|
+
// answer over the recovery prompt. While this run is current no
|
|
1009
|
+
// newer run's synthesis can be in flight, so the abort is safe.
|
|
1010
|
+
if (this.isCurrentRun(runVersion)) {
|
|
1011
|
+
synthesisCancelled = true;
|
|
1012
|
+
this.abortActiveSynthesis();
|
|
1283
1013
|
}
|
|
1284
|
-
};
|
|
1285
|
-
|
|
1286
|
-
// Superseded mid-stream (barge-in): drain the segment chain (queued
|
|
1287
|
-
// links short-circuit on staleness) so this turn's synthesis fully
|
|
1288
|
-
// settles instead of racing the next turn.
|
|
1289
|
-
const settleSuperseded = async (): Promise<VoiceTurnOutcome> => {
|
|
1290
|
-
settleTurn();
|
|
1291
1014
|
await synthesisChain.catch(() => {});
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
};
|
|
1295
|
-
|
|
1296
|
-
// Narration speaks into the turn's audible dead air on a cadence wherever
|
|
1297
|
-
// in the turn it occurs; the front-door leg is toolless and quick, so in
|
|
1298
|
-
// practice the escalated leg's tool loops are what it covers.
|
|
1299
|
-
cadence.arm();
|
|
1300
|
-
await runVoiceLeg({ content, routingLeg: "front-door" });
|
|
1015
|
+
throw err;
|
|
1016
|
+
}
|
|
1301
1017
|
if (!this.isCurrentRun(runVersion)) {
|
|
1302
|
-
|
|
1018
|
+
// Superseded mid-stream (barge-in): drain the segment chain — queued
|
|
1019
|
+
// links short-circuit on staleness — so this turn's synthesis fully
|
|
1020
|
+
// settles instead of racing the next turn.
|
|
1021
|
+
await synthesisChain.catch(() => {});
|
|
1022
|
+
return fullResponseText;
|
|
1303
1023
|
}
|
|
1304
1024
|
|
|
1305
|
-
//
|
|
1306
|
-
//
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1025
|
+
// Final sweep: release any held-back partial tag to both consumers,
|
|
1026
|
+
// then strip any remaining control markers from the buffer.
|
|
1027
|
+
const filterTail = reasoningFilter.flush();
|
|
1028
|
+
fullResponseText += filterTail;
|
|
1029
|
+
ttsBuffer += filterTail;
|
|
1030
|
+
ttsBuffer = stripInternalSpeechMarkers(ttsBuffer);
|
|
1031
|
+
if (ttsBuffer.length > 0) {
|
|
1032
|
+
emitSafeChunk(ttsBuffer);
|
|
1312
1033
|
}
|
|
1313
1034
|
|
|
1314
|
-
// The turn's legs are done: no narration past this point.
|
|
1315
|
-
settleTurn();
|
|
1316
|
-
|
|
1317
|
-
// Final sweep: a held "[..." tail that never completed a marker is real
|
|
1318
|
-
// text, so the forced flush speaks it instead of dropping it.
|
|
1319
|
-
flushSafeText(fullResponseText, { force: true });
|
|
1320
|
-
|
|
1321
1035
|
// Synthesized path: force-extract whatever never reached a speakable
|
|
1322
1036
|
// boundary, then drain the chain so every segment's audio (or its
|
|
1323
1037
|
// native fallback) is enqueued before the end-of-turn signal. The
|
|
@@ -1338,8 +1052,7 @@ export class CallController {
|
|
|
1338
1052
|
// doesn't inject its end-of-turn marker (or fallback text) into the next
|
|
1339
1053
|
// turn's output stream.
|
|
1340
1054
|
if (!this.isCurrentRun(runVersion)) {
|
|
1341
|
-
|
|
1342
|
-
return outcome;
|
|
1055
|
+
return fullResponseText;
|
|
1343
1056
|
}
|
|
1344
1057
|
|
|
1345
1058
|
// Signal end of this turn's speech. An empty token with `last: true`
|
|
@@ -1356,8 +1069,7 @@ export class CallController {
|
|
|
1356
1069
|
this.lastSentWasOpener = false;
|
|
1357
1070
|
}
|
|
1358
1071
|
|
|
1359
|
-
|
|
1360
|
-
return outcome;
|
|
1072
|
+
return fullResponseText;
|
|
1361
1073
|
}
|
|
1362
1074
|
|
|
1363
1075
|
/**
|
|
@@ -1381,7 +1093,6 @@ export class CallController {
|
|
|
1381
1093
|
text: string,
|
|
1382
1094
|
runVersion: number,
|
|
1383
1095
|
format: CallAudioFormat = "mp3",
|
|
1384
|
-
languageOverride?: string,
|
|
1385
1096
|
): Promise<SegmentSynthesisStatus> {
|
|
1386
1097
|
let sink: AudioStoreSink | null = null;
|
|
1387
1098
|
let playUrlSent = false;
|
|
@@ -1403,9 +1114,7 @@ export class CallController {
|
|
|
1403
1114
|
|
|
1404
1115
|
this.activeSynthesisAbort = abortController;
|
|
1405
1116
|
|
|
1406
|
-
|
|
1407
|
-
// its own hint; model text rides the turn's resolved language.
|
|
1408
|
-
const language = languageOverride ?? this.resolveSynthesisLanguage();
|
|
1117
|
+
const language = this.resolveSynthesisLanguage();
|
|
1409
1118
|
// A language-known segment may select the synthesizing provider's
|
|
1410
1119
|
// configured per-language voice; no entry keeps the provider default.
|
|
1411
1120
|
const voiceId = resolveTelephonyLanguageVoice(provider.id, language);
|
|
@@ -1493,19 +1202,12 @@ export class CallController {
|
|
|
1493
1202
|
* (ASK_GUARDIAN_APPROVAL / ASK_GUARDIAN), call finalization (END_CALL),
|
|
1494
1203
|
* and normal idle transition.
|
|
1495
1204
|
*/
|
|
1496
|
-
private async handleTurnCompletion(
|
|
1497
|
-
const responseText =
|
|
1205
|
+
private async handleTurnCompletion(fullResponseText: string): Promise<void> {
|
|
1206
|
+
const responseText = fullResponseText;
|
|
1498
1207
|
|
|
1499
|
-
// Record the assistant response event
|
|
1500
|
-
// mirrors when the bridge reported them.
|
|
1208
|
+
// Record the assistant response event
|
|
1501
1209
|
recordCallEvent(this.callSessionId, "assistant_spoke", {
|
|
1502
1210
|
text: responseText,
|
|
1503
|
-
...(outcome.userMessageId !== null
|
|
1504
|
-
? { userMessageId: outcome.userMessageId }
|
|
1505
|
-
: {}),
|
|
1506
|
-
...(outcome.assistantMessageId !== null
|
|
1507
|
-
? { assistantMessageId: outcome.assistantMessageId }
|
|
1508
|
-
: {}),
|
|
1509
1211
|
});
|
|
1510
1212
|
const spokenText = sanitizeForTts(
|
|
1511
1213
|
stripInternalSpeechMarkers(responseText),
|
|
@@ -1910,11 +1612,9 @@ export class CallController {
|
|
|
1910
1612
|
/**
|
|
1911
1613
|
* Flip from the pre-speech `processing` phase to `speaking` at the moment the
|
|
1912
1614
|
* first real outbound audio/token is emitted. Guarded so a superseded or
|
|
1913
|
-
* aborted (idle) turn never (re)enters `speaking
|
|
1914
|
-
*
|
|
1915
|
-
*
|
|
1916
|
-
* abort a turn in either phase once the transport's sustained-speech
|
|
1917
|
-
* guard has vouched for it.
|
|
1615
|
+
* aborted (idle) turn never (re)enters `speaking`, and so barge-in
|
|
1616
|
+
* (handleBargeIn, gated on `speaking`) can't abort a turn that is still
|
|
1617
|
+
* waiting for the processing lock or generating with no audio yet.
|
|
1918
1618
|
*/
|
|
1919
1619
|
private beginSpeaking(runVersion: number): void {
|
|
1920
1620
|
if (!this.isCurrentRun(runVersion)) {
|
|
@@ -2133,7 +1833,7 @@ export class CallController {
|
|
|
2133
1833
|
|
|
2134
1834
|
private startDurationTimer(): void {
|
|
2135
1835
|
const maxDurationMs = getMaxCallDurationMs();
|
|
2136
|
-
const warningMs = maxDurationMs -
|
|
1836
|
+
const warningMs = maxDurationMs - 2 * 60 * 1000; // 2 minutes before max
|
|
2137
1837
|
|
|
2138
1838
|
if (warningMs > 0) {
|
|
2139
1839
|
this.durationWarningTimer = setTimeout(() => {
|
|
@@ -2194,7 +1894,7 @@ export class CallController {
|
|
|
2194
1894
|
},
|
|
2195
1895
|
);
|
|
2196
1896
|
}
|
|
2197
|
-
},
|
|
1897
|
+
}, 3000);
|
|
2198
1898
|
}, maxDurationMs);
|
|
2199
1899
|
}
|
|
2200
1900
|
|