@vellumai/assistant 0.5.6 → 0.5.8
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/.env.example +16 -2
- package/ARCHITECTURE.md +6 -75
- package/Dockerfile +3 -2
- package/README.md +0 -2
- package/bun.lock +0 -414
- package/docker-entrypoint.sh +9 -0
- package/docs/architecture/keychain-broker.md +45 -240
- package/docs/architecture/memory.md +13 -11
- package/docs/architecture/security.md +0 -17
- package/docs/credential-execution-service.md +2 -2
- package/node_modules/@vellumai/ces-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +120 -1
- package/node_modules/@vellumai/credential-storage/package.json +1 -0
- package/node_modules/@vellumai/egress-proxy/package.json +1 -0
- package/package.json +2 -3
- package/src/__tests__/actor-token-service.test.ts +0 -114
- package/src/__tests__/approval-cascade.test.ts +0 -1
- package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/browser-skill-endstate.test.ts +6 -5
- package/src/__tests__/btw-routes.test.ts +0 -39
- package/src/__tests__/call-controller.test.ts +0 -1
- package/src/__tests__/call-domain.test.ts +0 -128
- package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
- package/src/__tests__/ces-startup-timeout.test.ts +40 -0
- package/src/__tests__/channel-approval-routes.test.ts +0 -5
- package/src/__tests__/channel-readiness-service.test.ts +1 -60
- package/src/__tests__/checker.test.ts +4 -2
- package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
- package/src/__tests__/config-schema-cmd.test.ts +0 -2
- package/src/__tests__/config-schema.test.ts +3 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop.test.ts +2 -4
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
- package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
- package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
- package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
- package/src/__tests__/conversation-queue.test.ts +0 -1
- package/src/__tests__/conversation-skill-tools.test.ts +0 -54
- package/src/__tests__/conversation-slash-queue.test.ts +0 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
- package/src/__tests__/conversation-title-service.test.ts +87 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
- package/src/__tests__/credential-execution-client.test.ts +5 -2
- package/src/__tests__/credential-execution-feature-gates.test.ts +59 -30
- package/src/__tests__/credential-execution-managed-contract.test.ts +35 -20
- package/src/__tests__/credential-security-e2e.test.ts +1 -67
- package/src/__tests__/credential-security-invariants.test.ts +6 -50
- package/src/__tests__/credentials-cli.test.ts +82 -3
- package/src/__tests__/daemon-credential-client.test.ts +123 -0
- package/src/__tests__/db-migration-rollback.test.ts +2015 -1
- package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +34 -143
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
- package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
- package/src/__tests__/guardian-routing-state.test.ts +0 -5
- package/src/__tests__/host-shell-tool.test.ts +6 -7
- package/src/__tests__/http-user-message-parity.test.ts +3 -103
- package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
- package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
- package/src/__tests__/intent-routing.test.ts +0 -13
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
- package/src/__tests__/journal-context.test.ts +335 -0
- package/src/__tests__/keychain-broker-client.test.ts +161 -22
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
- package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
- package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
- package/src/__tests__/memory-recall-quality.test.ts +48 -17
- package/src/__tests__/memory-regressions.test.ts +408 -363
- package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
- package/src/__tests__/migration-export-http.test.ts +2 -2
- package/src/__tests__/migration-import-commit-http.test.ts +2 -2
- package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
- package/src/__tests__/migration-validate-http.test.ts +2 -2
- package/src/__tests__/non-member-access-request.test.ts +2 -7
- package/src/__tests__/notification-decision-fallback.test.ts +4 -0
- package/src/__tests__/notification-decision-identity.test.ts +4 -0
- package/src/__tests__/notification-decision-strategy.test.ts +71 -0
- package/src/__tests__/oauth-cli.test.ts +5 -1
- package/src/__tests__/permission-types.test.ts +1 -0
- package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
- package/src/__tests__/provider-error-scenarios.test.ts +0 -267
- package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
- package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
- package/src/__tests__/qdrant-manager.test.ts +28 -2
- package/src/__tests__/registry.test.ts +0 -6
- package/src/__tests__/relay-server.test.ts +1 -2
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
- package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -1
- package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
- package/src/__tests__/secure-keys.test.ts +95 -272
- package/src/__tests__/shell-identity.test.ts +96 -6
- package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
- package/src/__tests__/skill-feature-flags.test.ts +46 -45
- package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
- package/src/__tests__/skill-load-inline-command.test.ts +8 -12
- package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
- package/src/__tests__/skill-load-tool.test.ts +0 -2
- package/src/__tests__/skill-memory.test.ts +17 -3
- package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
- package/src/__tests__/skills.test.ts +0 -2
- package/src/__tests__/slack-inbound-verification.test.ts +0 -4
- package/src/__tests__/stale-approval-dedup.test.ts +171 -0
- package/src/__tests__/stt-hints.test.ts +437 -0
- package/src/__tests__/suggestion-routes.test.ts +1 -32
- package/src/__tests__/system-prompt.test.ts +0 -1
- package/src/__tests__/task-memory-cleanup.test.ts +14 -0
- package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
- package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
- package/src/__tests__/update-bulletin.test.ts +0 -2
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
- package/src/__tests__/voice-quality.test.ts +58 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -7
- package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
- package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +220 -0
- package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
- package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
- package/src/acp/agent-process.ts +9 -1
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-request-resolvers.ts +164 -38
- package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
- package/src/calls/audio-store.test.ts +97 -0
- package/src/calls/audio-store.ts +205 -0
- package/src/calls/call-controller.ts +90 -8
- package/src/calls/call-domain.ts +3 -0
- package/src/calls/call-store.ts +10 -3
- package/src/calls/fish-audio-client.ts +129 -0
- package/src/calls/relay-server.ts +27 -0
- package/src/calls/stt-hints.ts +189 -0
- package/src/calls/tts-text-sanitizer.ts +61 -0
- package/src/calls/twilio-routes.ts +34 -5
- package/src/calls/types.ts +1 -0
- package/src/calls/voice-ingress-preflight.ts +0 -42
- package/src/calls/voice-quality.ts +38 -5
- package/src/calls/voice-session-bridge.ts +7 -12
- package/src/cli/commands/avatar.ts +2 -2
- package/src/cli/commands/config.ts +1 -4
- package/src/cli/commands/credentials.ts +128 -82
- package/src/cli/commands/doctor.ts +2 -2
- package/src/cli/commands/keys.ts +7 -7
- package/src/cli/commands/memory.ts +1 -1
- package/src/cli/commands/oauth/connections.ts +11 -29
- package/src/cli/commands/oauth/index.ts +7 -0
- package/src/cli/commands/oauth/platform.ts +525 -0
- package/src/cli/commands/platform.ts +3 -3
- package/src/cli/lib/daemon-credential-client.ts +284 -0
- package/src/cli.ts +1 -1
- package/src/config/assistant-feature-flags.ts +186 -5
- package/src/config/bundled-skills/AGENTS.md +34 -0
- package/src/config/bundled-skills/acp/SKILL.md +10 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
- package/src/config/bundled-skills/messaging/SKILL.md +5 -5
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
- package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
- package/src/config/bundled-skills/settings/SKILL.md +15 -2
- package/src/config/bundled-skills/settings/TOOLS.json +47 -2
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
- package/src/config/bundled-skills/slack/SKILL.md +1 -1
- package/src/config/bundled-tool-registry.ts +5 -11
- package/src/config/defaults.ts +0 -2
- package/src/config/env-registry.ts +5 -5
- package/src/config/env.ts +21 -14
- package/src/config/feature-flag-registry.json +49 -9
- package/src/config/loader.ts +106 -42
- package/src/config/schema.ts +9 -29
- package/src/config/schemas/calls.ts +30 -0
- package/src/config/schemas/fish-audio.ts +39 -0
- package/src/config/schemas/inference.ts +2 -2
- package/src/config/schemas/journal.ts +16 -0
- package/src/config/schemas/memory-processing.ts +2 -2
- package/src/config/schemas/security.ts +0 -4
- package/src/config/types.ts +1 -1
- package/src/contacts/contact-store.ts +39 -0
- package/src/contacts/types.ts +2 -0
- package/src/credential-execution/approval-bridge.ts +1 -0
- package/src/credential-execution/executable-discovery.ts +28 -4
- package/src/credential-execution/feature-gates.ts +16 -0
- package/src/credential-execution/process-manager.ts +38 -0
- package/src/credential-execution/startup-timeout.ts +36 -0
- package/src/daemon/approval-generators.ts +3 -9
- package/src/daemon/assistant-attachments.ts +9 -0
- package/src/daemon/config-watcher.ts +5 -0
- package/src/daemon/conversation-error.ts +13 -1
- package/src/daemon/conversation-memory.ts +1 -2
- package/src/daemon/conversation-process.ts +18 -1
- package/src/daemon/conversation-surfaces.ts +30 -1
- package/src/daemon/conversation-tool-setup.ts +0 -105
- package/src/daemon/conversation.ts +21 -1
- package/src/daemon/guardian-action-generators.ts +3 -9
- package/src/daemon/handlers/config-vercel.ts +92 -0
- package/src/daemon/handlers/skills.ts +2 -15
- package/src/daemon/install-symlink.ts +195 -0
- package/src/daemon/lifecycle.ts +234 -51
- package/src/daemon/message-types/conversations.ts +4 -4
- package/src/daemon/message-types/diagnostics.ts +3 -22
- package/src/daemon/message-types/messages.ts +0 -2
- package/src/daemon/message-types/upgrades.ts +8 -0
- package/src/daemon/server.ts +32 -95
- package/src/events/domain-events.ts +2 -1
- package/src/inbound/platform-callback-registration.ts +3 -3
- package/src/instrument.ts +8 -5
- package/src/memory/app-store.ts +31 -0
- package/src/memory/conversation-title-service.ts +50 -1
- package/src/memory/db-init.ts +16 -0
- package/src/memory/indexer.ts +19 -10
- package/src/memory/items-extractor.ts +328 -321
- package/src/memory/job-handlers/conversation-starters.ts +4 -1
- package/src/memory/job-handlers/summarization.ts +26 -16
- package/src/memory/jobs-store.ts +63 -6
- package/src/memory/jobs-worker.ts +31 -7
- package/src/memory/journal-memory.ts +214 -0
- package/src/memory/migrations/001-job-deferrals.ts +19 -0
- package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
- package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
- package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
- package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
- package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
- package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
- package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
- package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
- package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
- package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
- package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
- package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
- package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
- package/src/memory/migrations/116-messages-fts.ts +106 -1
- package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
- package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
- package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
- package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
- package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
- package/src/memory/migrations/141-rename-verification-table.ts +54 -0
- package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
- package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
- package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
- package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
- package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
- package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
- package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
- package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
- package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
- package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
- package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
- package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
- package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
- package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
- package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
- package/src/memory/migrations/index.ts +5 -0
- package/src/memory/migrations/registry.ts +98 -0
- package/src/memory/migrations/validate-migration-state.ts +137 -11
- package/src/memory/qdrant-circuit-breaker.ts +9 -0
- package/src/memory/qdrant-manager.ts +64 -7
- package/src/memory/retriever.test.ts +37 -25
- package/src/memory/retriever.ts +24 -49
- package/src/memory/schema/calls.ts +1 -0
- package/src/memory/schema/contacts.ts +1 -0
- package/src/memory/schema/memory-core.ts +2 -0
- package/src/memory/search/formatting.ts +7 -44
- package/src/memory/search/staleness.ts +4 -0
- package/src/memory/search/tier-classifier.ts +10 -2
- package/src/memory/search/types.ts +2 -5
- package/src/memory/task-memory-cleanup.ts +4 -3
- package/src/notifications/adapters/slack.ts +168 -6
- package/src/notifications/broadcaster.ts +1 -0
- package/src/notifications/copy-composer.ts +59 -2
- package/src/notifications/decision-engine.ts +4 -1
- package/src/notifications/signal.ts +2 -0
- package/src/notifications/types.ts +2 -0
- package/src/oauth/connection-resolver.ts +6 -4
- package/src/permissions/checker.ts +0 -38
- package/src/permissions/shell-identity.ts +76 -22
- package/src/permissions/types.ts +4 -2
- package/src/platform/client.ts +35 -7
- package/src/prompts/journal-context.ts +133 -0
- package/src/prompts/persona-resolver.ts +194 -0
- package/src/prompts/system-prompt.ts +44 -4
- package/src/prompts/templates/SOUL.md +10 -0
- package/src/prompts/templates/users/default.md +1 -0
- package/src/providers/provider-send-message.ts +3 -32
- package/src/providers/registry.ts +29 -179
- package/src/providers/types.ts +1 -1
- package/src/runtime/access-request-helper.ts +4 -0
- package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
- package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
- package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
- package/src/runtime/auth/external-assistant-id.ts +13 -59
- package/src/runtime/auth/route-policy.ts +17 -1
- package/src/runtime/auth/token-service.ts +43 -138
- package/src/runtime/channel-readiness-service.ts +1 -16
- package/src/runtime/gateway-client.ts +47 -4
- package/src/runtime/guardian-decision-types.ts +45 -4
- package/src/runtime/http-server.ts +31 -3
- package/src/runtime/middleware/error-handler.ts +1 -9
- package/src/runtime/routes/access-request-decision.ts +2 -2
- package/src/runtime/routes/app-management-routes.ts +2 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
- package/src/runtime/routes/audio-routes.ts +40 -0
- package/src/runtime/routes/btw-routes.ts +0 -17
- package/src/runtime/routes/channel-readiness-routes.ts +9 -4
- package/src/runtime/routes/conversation-query-routes.ts +63 -1
- package/src/runtime/routes/conversation-routes.ts +4 -44
- package/src/runtime/routes/debug-routes.ts +12 -9
- package/src/runtime/routes/diagnostics-routes.ts +1 -477
- package/src/runtime/routes/guardian-approval-interception.ts +168 -11
- package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
- package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
- package/src/runtime/routes/identity-routes.ts +19 -30
- package/src/runtime/routes/inbound-message-handler.ts +31 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
- package/src/runtime/routes/integrations/twilio.ts +52 -10
- package/src/runtime/routes/integrations/vercel.ts +89 -0
- package/src/runtime/routes/log-export-routes.ts +5 -0
- package/src/runtime/routes/memory-item-routes.test.ts +3 -3
- package/src/runtime/routes/memory-item-routes.ts +46 -14
- package/src/runtime/routes/migration-rollback-routes.ts +209 -0
- package/src/runtime/routes/migration-routes.ts +17 -1
- package/src/runtime/routes/notification-routes.ts +58 -0
- package/src/runtime/routes/schedule-routes.ts +65 -0
- package/src/runtime/routes/secret-routes.ts +141 -10
- package/src/runtime/routes/settings-routes.ts +41 -1
- package/src/runtime/routes/tts-routes.ts +96 -0
- package/src/runtime/routes/upgrade-broadcast-routes.ts +26 -2
- package/src/runtime/routes/workspace-commit-routes.ts +62 -0
- package/src/runtime/routes/workspace-routes.test.ts +22 -1
- package/src/runtime/routes/workspace-routes.ts +1 -1
- package/src/runtime/routes/workspace-utils.ts +86 -2
- package/src/security/ces-credential-client.ts +75 -29
- package/src/security/ces-rpc-credential-backend.ts +86 -0
- package/src/security/credential-backend.ts +22 -92
- package/src/security/keychain-broker-client.ts +10 -2
- package/src/security/secure-keys.ts +113 -115
- package/src/skills/catalog-install.ts +6 -32
- package/src/skills/skill-memory.ts +1 -0
- package/src/subagent/manager.ts +2 -5
- package/src/telemetry/usage-telemetry-reporter.ts +4 -2
- package/src/tools/acp/spawn.ts +78 -1
- package/src/tools/calls/call-start.ts +1 -0
- package/src/tools/credentials/vault.ts +5 -3
- package/src/tools/executor.ts +0 -4
- package/src/tools/memory/definitions.ts +3 -2
- package/src/tools/memory/handlers.ts +10 -7
- package/src/tools/network/script-proxy/session-manager.ts +19 -4
- package/src/tools/network/web-fetch.ts +3 -1
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/tools/types.ts +0 -8
- package/src/util/browser.ts +15 -0
- package/src/util/errors.ts +0 -12
- package/src/util/platform.ts +4 -51
- package/src/workspace/git-service.ts +5 -2
- package/src/workspace/migrations/001-avatar-rename.ts +15 -0
- package/src/workspace/migrations/003-seed-device-id.ts +17 -1
- package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
- package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
- package/src/workspace/migrations/006-services-config.ts +49 -0
- package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
- package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
- package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
- package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
- package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
- package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
- package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
- package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
- package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
- package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
- package/src/workspace/migrations/017-seed-persona-dirs.ts +96 -0
- package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
- package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +27 -5
- package/src/workspace/migrations/registry.ts +12 -0
- package/src/workspace/migrations/runner.ts +106 -2
- package/src/workspace/migrations/types.ts +4 -0
- package/src/workspace/provider-commit-message-generator.ts +12 -21
- package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
- package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
- package/src/__tests__/diagnostics-export.test.ts +0 -288
- package/src/__tests__/local-gateway-health.test.ts +0 -209
- package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
- package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
- package/src/__tests__/secret-ingress-handler.test.ts +0 -120
- package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
- package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
- package/src/__tests__/swarm-orchestrator.test.ts +0 -463
- package/src/__tests__/swarm-plan-validator.test.ts +0 -384
- package/src/__tests__/swarm-recursion.test.ts +0 -197
- package/src/__tests__/swarm-router-planner.test.ts +0 -234
- package/src/__tests__/swarm-tool.test.ts +0 -185
- package/src/__tests__/swarm-worker-backend.test.ts +0 -144
- package/src/__tests__/swarm-worker-runner.test.ts +0 -288
- package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
- package/src/commands/cc-command-registry.ts +0 -248
- package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
- package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
- package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
- package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
- package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
- package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
- package/src/config/schemas/swarm.ts +0 -82
- package/src/logfire.ts +0 -135
- package/src/memory/search/lexical.ts +0 -48
- package/src/providers/failover.ts +0 -186
- package/src/runtime/local-gateway-health.ts +0 -275
- package/src/security/secret-ingress.ts +0 -68
- package/src/swarm/backend-claude-code.ts +0 -225
- package/src/swarm/checkpoint.ts +0 -137
- package/src/swarm/graph-utils.ts +0 -53
- package/src/swarm/index.ts +0 -55
- package/src/swarm/limits.ts +0 -66
- package/src/swarm/orchestrator.ts +0 -424
- package/src/swarm/plan-validator.ts +0 -117
- package/src/swarm/router-planner.ts +0 -162
- package/src/swarm/router-prompts.ts +0 -39
- package/src/swarm/synthesizer.ts +0 -81
- package/src/swarm/types.ts +0 -72
- package/src/swarm/worker-backend.ts +0 -131
- package/src/swarm/worker-prompts.ts +0 -80
- package/src/swarm/worker-runner.ts +0 -170
- package/src/tools/claude-code/claude-code.ts +0 -610
- package/src/tools/swarm/delegate.ts +0 -205
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { getGatewayInternalBaseUrl } from "../config/env.js";
|
|
12
|
+
import { loadConfig } from "../config/loader.js";
|
|
12
13
|
import type { TrustContext } from "../daemon/conversation-runtime-assembly.js";
|
|
13
14
|
import type { ServerMessage } from "../daemon/message-protocol.js";
|
|
15
|
+
import { getPublicBaseUrl } from "../inbound/public-ingress-urls.js";
|
|
14
16
|
import {
|
|
15
17
|
expireCanonicalGuardianRequest,
|
|
16
18
|
getCanonicalRequestByPendingQuestionId,
|
|
@@ -22,6 +24,7 @@ import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
|
|
|
22
24
|
import { mintDaemonDeliveryToken } from "../runtime/auth/token-service.js";
|
|
23
25
|
import { computeToolApprovalDigest } from "../security/tool-approval-digest.js";
|
|
24
26
|
import { getLogger } from "../util/logger.js";
|
|
27
|
+
import { createStreamingEntry } from "./audio-store.js";
|
|
25
28
|
import {
|
|
26
29
|
getMaxCallDurationMs,
|
|
27
30
|
getSilenceTimeoutMs,
|
|
@@ -42,10 +45,12 @@ import {
|
|
|
42
45
|
updateCallSession,
|
|
43
46
|
} from "./call-store.js";
|
|
44
47
|
import { finalizeCall } from "./finalize-call.js";
|
|
48
|
+
import { synthesizeWithFishAudio } from "./fish-audio-client.js";
|
|
45
49
|
import { sendGuardianExpiryNotices } from "./guardian-action-sweep.js";
|
|
46
50
|
import { dispatchGuardianQuestion } from "./guardian-dispatch.js";
|
|
47
51
|
import type { RelayConnection } from "./relay-server.js";
|
|
48
52
|
import type { PromptSpeakerContext } from "./speaker-identification.js";
|
|
53
|
+
import { sanitizeForTts } from "./tts-text-sanitizer.js";
|
|
49
54
|
import {
|
|
50
55
|
ASK_GUARDIAN_CAPTURE_REGEX,
|
|
51
56
|
CALL_OPENING_ACK_MARKER,
|
|
@@ -56,6 +61,7 @@ import {
|
|
|
56
61
|
extractBalancedJson,
|
|
57
62
|
stripInternalSpeechMarkers,
|
|
58
63
|
} from "./voice-control-protocol.js";
|
|
64
|
+
import { isFishAudioTts } from "./voice-quality.js";
|
|
59
65
|
import {
|
|
60
66
|
startVoiceTurn,
|
|
61
67
|
type VoiceTurnHandle,
|
|
@@ -101,6 +107,8 @@ export class CallController {
|
|
|
101
107
|
private task: string | null;
|
|
102
108
|
/** True when the call session was created via the inbound path (no outbound task). */
|
|
103
109
|
private isInbound: boolean;
|
|
110
|
+
/** When true, the disclosure announcement is skipped for this call. */
|
|
111
|
+
private skipDisclosure: boolean;
|
|
104
112
|
/** Instructions queued while an LLM turn is in-flight or during pending guardian input */
|
|
105
113
|
private pendingInstructions: string[] = [];
|
|
106
114
|
/** Ensures the call opener is triggered at most once per call. */
|
|
@@ -131,6 +139,8 @@ export class CallController {
|
|
|
131
139
|
* without blocking the caller.
|
|
132
140
|
*/
|
|
133
141
|
private guardianUnavailableForCall = false;
|
|
142
|
+
/** Active Fish Audio session — tracked so interrupt handling can close it. */
|
|
143
|
+
private activeFishAbort: AbortController | null = null;
|
|
134
144
|
|
|
135
145
|
constructor(
|
|
136
146
|
callSessionId: string,
|
|
@@ -150,9 +160,10 @@ export class CallController {
|
|
|
150
160
|
this.assistantId = opts?.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID;
|
|
151
161
|
this.trustContext = opts?.trustContext ?? null;
|
|
152
162
|
|
|
153
|
-
// Resolve the conversation ID from the call session
|
|
163
|
+
// Resolve the conversation ID and skipDisclosure from the call session
|
|
154
164
|
const session = getCallSession(callSessionId);
|
|
155
165
|
this.conversationId = session?.conversationId ?? callSessionId;
|
|
166
|
+
this.skipDisclosure = session?.skipDisclosure ?? false;
|
|
156
167
|
|
|
157
168
|
this.startDurationTimer();
|
|
158
169
|
this.resetSilenceTimer();
|
|
@@ -340,6 +351,11 @@ export class CallController {
|
|
|
340
351
|
const wasSpeaking = this.state === "speaking";
|
|
341
352
|
this.abortCurrentTurn();
|
|
342
353
|
this.llmRunVersion++;
|
|
354
|
+
// Cancel in-flight Fish Audio synthesis on barge-in
|
|
355
|
+
if (this.activeFishAbort) {
|
|
356
|
+
this.activeFishAbort.abort();
|
|
357
|
+
this.activeFishAbort = null;
|
|
358
|
+
}
|
|
343
359
|
// Explicitly terminate the in-progress TTS turn so the relay can
|
|
344
360
|
// immediately hand control back to the caller after barge-in.
|
|
345
361
|
if (wasSpeaking) {
|
|
@@ -370,6 +386,10 @@ export class CallController {
|
|
|
370
386
|
this.pendingInstructions = [];
|
|
371
387
|
this.llmRunVersion++;
|
|
372
388
|
this.abortCurrentTurn();
|
|
389
|
+
if (this.activeFishAbort) {
|
|
390
|
+
this.activeFishAbort.abort();
|
|
391
|
+
this.activeFishAbort = null;
|
|
392
|
+
}
|
|
373
393
|
this.currentTurnPromise = null;
|
|
374
394
|
unregisterCallController(this.callSessionId);
|
|
375
395
|
|
|
@@ -516,24 +536,45 @@ export class CallController {
|
|
|
516
536
|
runVersion: number,
|
|
517
537
|
runSignal: AbortSignal,
|
|
518
538
|
): Promise<string> {
|
|
539
|
+
// Fish Audio TTS routing: when configured, buffer text by sentence
|
|
540
|
+
// boundaries and synthesize via Fish Audio instead of streaming text
|
|
541
|
+
// tokens for ElevenLabs TTS.
|
|
542
|
+
const config = loadConfig();
|
|
543
|
+
const useFishAudio = isFishAudioTts(config);
|
|
544
|
+
|
|
519
545
|
// Buffer incoming tokens so we can strip control markers ([ASK_GUARDIAN:...], [END_CALL])
|
|
520
546
|
// before they reach TTS. We hold text whenever an unmatched '[' appears, since it
|
|
521
547
|
// could be the start of a control marker.
|
|
522
548
|
let ttsBuffer = "";
|
|
523
549
|
let fullResponseText = "";
|
|
524
550
|
|
|
551
|
+
// When using Fish Audio, we accumulate all text and synthesize
|
|
552
|
+
// the complete response at the end of the turn (better prosody).
|
|
553
|
+
let fishAudioTextBuffer = "";
|
|
554
|
+
|
|
555
|
+
/** Emit a chunk of safe text to the appropriate TTS backend. */
|
|
556
|
+
const emitSafeChunk = (safeText: string): void => {
|
|
557
|
+
const cleaned = sanitizeForTts(safeText);
|
|
558
|
+
if (cleaned.length === 0) return;
|
|
559
|
+
if (useFishAudio) {
|
|
560
|
+
fishAudioTextBuffer += cleaned;
|
|
561
|
+
} else {
|
|
562
|
+
this.relay.sendTextToken(cleaned, false);
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
|
|
525
566
|
const flushSafeText = (): void => {
|
|
526
567
|
if (!this.isCurrentRun(runVersion)) return;
|
|
527
568
|
if (ttsBuffer.length === 0) return;
|
|
528
569
|
const bracketIdx = ttsBuffer.indexOf("[");
|
|
529
570
|
if (bracketIdx === -1) {
|
|
530
571
|
// No bracket at all — safe to flush everything
|
|
531
|
-
|
|
572
|
+
emitSafeChunk(ttsBuffer);
|
|
532
573
|
ttsBuffer = "";
|
|
533
574
|
} else {
|
|
534
575
|
// Flush everything before the bracket
|
|
535
576
|
if (bracketIdx > 0) {
|
|
536
|
-
|
|
577
|
+
emitSafeChunk(ttsBuffer.slice(0, bracketIdx));
|
|
537
578
|
ttsBuffer = ttsBuffer.slice(bracketIdx);
|
|
538
579
|
}
|
|
539
580
|
|
|
@@ -547,10 +588,10 @@ export class CallController {
|
|
|
547
588
|
// Not a control marker prefix — flush up to the next '[' (if any)
|
|
548
589
|
const nextBracket = ttsBuffer.indexOf("[", 1);
|
|
549
590
|
if (nextBracket === -1) {
|
|
550
|
-
|
|
591
|
+
emitSafeChunk(ttsBuffer);
|
|
551
592
|
ttsBuffer = "";
|
|
552
593
|
} else {
|
|
553
|
-
|
|
594
|
+
emitSafeChunk(ttsBuffer.slice(0, nextBracket));
|
|
554
595
|
ttsBuffer = ttsBuffer.slice(nextBracket);
|
|
555
596
|
}
|
|
556
597
|
}
|
|
@@ -585,6 +626,7 @@ export class CallController {
|
|
|
585
626
|
trustContext: this.trustContext ?? undefined,
|
|
586
627
|
isInbound: this.isInbound,
|
|
587
628
|
task: this.task,
|
|
629
|
+
skipDisclosure: this.skipDisclosure,
|
|
588
630
|
onTextDelta,
|
|
589
631
|
onComplete,
|
|
590
632
|
onError,
|
|
@@ -625,10 +667,50 @@ export class CallController {
|
|
|
625
667
|
// Final sweep: strip any remaining control markers from the buffer
|
|
626
668
|
ttsBuffer = stripInternalSpeechMarkers(ttsBuffer);
|
|
627
669
|
if (ttsBuffer.length > 0) {
|
|
628
|
-
|
|
670
|
+
emitSafeChunk(ttsBuffer);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// When using Fish Audio, synthesize the complete response text in a
|
|
674
|
+
// single REST API call. The full text gives Fish Audio better context
|
|
675
|
+
// for prosody and intonation. Audio streams back via chunked transfer
|
|
676
|
+
// encoding and is forwarded to Twilio as it arrives.
|
|
677
|
+
const sanitizedFishText = sanitizeForTts(fishAudioTextBuffer.trim());
|
|
678
|
+
if (useFishAudio && sanitizedFishText.length > 0) {
|
|
679
|
+
if (!this.isCurrentRun(runVersion)) return fullResponseText;
|
|
680
|
+
let handle: ReturnType<typeof createStreamingEntry> | null = null;
|
|
681
|
+
try {
|
|
682
|
+
const format = config.fishAudio.format ?? "mp3";
|
|
683
|
+
handle = createStreamingEntry(format as "mp3" | "wav" | "opus");
|
|
684
|
+
const baseUrl = getPublicBaseUrl(config);
|
|
685
|
+
const url = `${baseUrl}/v1/audio/${handle.audioId}`;
|
|
686
|
+
this.relay.sendPlayUrl(url);
|
|
687
|
+
const abortController = new AbortController();
|
|
688
|
+
this.activeFishAbort = abortController;
|
|
689
|
+
await synthesizeWithFishAudio(
|
|
690
|
+
sanitizedFishText,
|
|
691
|
+
config.fishAudio,
|
|
692
|
+
{
|
|
693
|
+
onChunk: (chunk) => handle!.push(chunk),
|
|
694
|
+
signal: abortController.signal,
|
|
695
|
+
},
|
|
696
|
+
);
|
|
697
|
+
} catch (err) {
|
|
698
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
699
|
+
log.debug("Fish Audio synthesis aborted (barge-in)");
|
|
700
|
+
} else {
|
|
701
|
+
log.error({ err }, "Fish Audio synthesis failed — skipping");
|
|
702
|
+
}
|
|
703
|
+
} finally {
|
|
704
|
+
this.activeFishAbort = null;
|
|
705
|
+
handle?.finalize();
|
|
706
|
+
}
|
|
629
707
|
}
|
|
630
708
|
|
|
631
|
-
// Signal end of this turn's speech
|
|
709
|
+
// Signal end of this turn's speech. An empty token with `last: true`
|
|
710
|
+
// tells ConversationRelay to start listening — it does NOT trigger TTS
|
|
711
|
+
// synthesis. This is required even when Fish Audio handled all audio
|
|
712
|
+
// playback, because ConversationRelay still needs the end-of-turn signal
|
|
713
|
+
// to transition from "assistant speaking" to "caller speaking" state.
|
|
632
714
|
this.relay.sendTextToken("", true);
|
|
633
715
|
|
|
634
716
|
// Mark the greeting's first response as awaiting ack
|
|
@@ -652,7 +734,7 @@ export class CallController {
|
|
|
652
734
|
recordCallEvent(this.callSessionId, "assistant_spoke", {
|
|
653
735
|
text: responseText,
|
|
654
736
|
});
|
|
655
|
-
const spokenText = stripInternalSpeechMarkers(responseText).trim();
|
|
737
|
+
const spokenText = sanitizeForTts(stripInternalSpeechMarkers(responseText)).trim();
|
|
656
738
|
if (spokenText.length > 0) {
|
|
657
739
|
const session = getCallSession(this.callSessionId);
|
|
658
740
|
if (session) {
|
package/src/calls/call-domain.ts
CHANGED
|
@@ -85,6 +85,7 @@ export type StartCallInput = {
|
|
|
85
85
|
conversationId: string;
|
|
86
86
|
assistantId?: string;
|
|
87
87
|
callerIdentityMode?: "assistant_number" | "user_number";
|
|
88
|
+
skipDisclosure?: boolean;
|
|
88
89
|
};
|
|
89
90
|
|
|
90
91
|
export type CancelCallInput = {
|
|
@@ -364,6 +365,7 @@ export async function startCall(
|
|
|
364
365
|
context: callContext,
|
|
365
366
|
conversationId,
|
|
366
367
|
callerIdentityMode,
|
|
368
|
+
skipDisclosure,
|
|
367
369
|
assistantId = DAEMON_INTERNAL_ASSISTANT_ID,
|
|
368
370
|
} = input;
|
|
369
371
|
|
|
@@ -440,6 +442,7 @@ export async function startCall(
|
|
|
440
442
|
task: callContext ? `${task}\n\nContext: ${callContext}` : task,
|
|
441
443
|
callerIdentityMode: identityResult.mode,
|
|
442
444
|
callerIdentitySource: identityResult.source,
|
|
445
|
+
skipDisclosure,
|
|
443
446
|
initiatedFromConversationId: conversationId,
|
|
444
447
|
});
|
|
445
448
|
sessionId = session.id;
|
package/src/calls/call-store.ts
CHANGED
|
@@ -41,6 +41,10 @@ const parseCallSession = createRowMapper<
|
|
|
41
41
|
inviteGuardianName: "inviteGuardianName",
|
|
42
42
|
callerIdentityMode: "callerIdentityMode",
|
|
43
43
|
callerIdentitySource: "callerIdentitySource",
|
|
44
|
+
skipDisclosure: {
|
|
45
|
+
from: "skipDisclosure",
|
|
46
|
+
transform: (v: unknown) => v === 1,
|
|
47
|
+
},
|
|
44
48
|
initiatedFromConversationId: "initiatedFromConversationId",
|
|
45
49
|
startedAt: "startedAt",
|
|
46
50
|
endedAt: "endedAt",
|
|
@@ -87,11 +91,13 @@ export function createCallSession(opts: {
|
|
|
87
91
|
inviteGuardianName?: string;
|
|
88
92
|
callerIdentityMode?: string;
|
|
89
93
|
callerIdentitySource?: string;
|
|
94
|
+
skipDisclosure?: boolean;
|
|
90
95
|
initiatedFromConversationId?: string;
|
|
91
96
|
}): CallSession {
|
|
92
97
|
const db = getDb();
|
|
93
98
|
const now = Date.now();
|
|
94
|
-
const
|
|
99
|
+
const skipDisclosure = opts.skipDisclosure ?? false;
|
|
100
|
+
const row = {
|
|
95
101
|
id: uuid(),
|
|
96
102
|
conversationId: opts.conversationId,
|
|
97
103
|
provider: opts.provider,
|
|
@@ -106,6 +112,7 @@ export function createCallSession(opts: {
|
|
|
106
112
|
inviteGuardianName: opts.inviteGuardianName ?? null,
|
|
107
113
|
callerIdentityMode: opts.callerIdentityMode ?? null,
|
|
108
114
|
callerIdentitySource: opts.callerIdentitySource ?? null,
|
|
115
|
+
skipDisclosure: skipDisclosure ? 1 : 0,
|
|
109
116
|
initiatedFromConversationId: opts.initiatedFromConversationId ?? null,
|
|
110
117
|
startedAt: null,
|
|
111
118
|
endedAt: null,
|
|
@@ -113,8 +120,8 @@ export function createCallSession(opts: {
|
|
|
113
120
|
createdAt: now,
|
|
114
121
|
updatedAt: now,
|
|
115
122
|
};
|
|
116
|
-
db.insert(callSessions).values(
|
|
117
|
-
return
|
|
123
|
+
db.insert(callSessions).values(row).run();
|
|
124
|
+
return { ...row, skipDisclosure };
|
|
118
125
|
}
|
|
119
126
|
|
|
120
127
|
export function getCallSession(id: string): CallSession | null {
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { FishAudioConfig } from "../config/schemas/fish-audio.js";
|
|
2
|
+
import { credentialKey } from "../security/credential-key.js";
|
|
3
|
+
import { getSecureKeyAsync } from "../security/secure-keys.js";
|
|
4
|
+
import { getLogger } from "../util/logger.js";
|
|
5
|
+
|
|
6
|
+
const log = getLogger("fish-audio-client");
|
|
7
|
+
|
|
8
|
+
/** Timeout waiting for the first chunk from Fish Audio (ms). */
|
|
9
|
+
const FIRST_CHUNK_TIMEOUT_MS = 10_000;
|
|
10
|
+
|
|
11
|
+
/** Timeout waiting between consecutive chunks (ms). */
|
|
12
|
+
const IDLE_TIMEOUT_MS = 5_000;
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Fish Audio REST API (POST /v1/tts)
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
interface SynthesizeOptions {
|
|
19
|
+
onChunk?: (chunk: Uint8Array) => void;
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Synthesize text to audio using the Fish Audio REST API with the s2-pro
|
|
25
|
+
* model. Streams audio chunks via the optional `onChunk` callback as they
|
|
26
|
+
* arrive from the server's chunked transfer-encoded response. Returns the
|
|
27
|
+
* complete audio buffer when the response finishes.
|
|
28
|
+
*
|
|
29
|
+
* Pass an `AbortSignal` to cancel in-flight synthesis (e.g. on barge-in).
|
|
30
|
+
*/
|
|
31
|
+
export async function synthesizeWithFishAudio(
|
|
32
|
+
text: string,
|
|
33
|
+
config: FishAudioConfig,
|
|
34
|
+
options?: SynthesizeOptions,
|
|
35
|
+
): Promise<Buffer> {
|
|
36
|
+
const apiKey = await getSecureKeyAsync(
|
|
37
|
+
credentialKey("fish-audio", "api_key"),
|
|
38
|
+
);
|
|
39
|
+
if (!apiKey) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
"Fish Audio API key not configured. Store it via: assistant credentials set --service fish-audio --field api_key <key>",
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const body = {
|
|
46
|
+
text,
|
|
47
|
+
reference_id: config.referenceId || undefined,
|
|
48
|
+
model: "s2-pro",
|
|
49
|
+
format: config.format,
|
|
50
|
+
mp3_bitrate: 192,
|
|
51
|
+
chunk_length: config.chunkLength,
|
|
52
|
+
normalize: true,
|
|
53
|
+
latency: config.latency,
|
|
54
|
+
temperature: 1.0,
|
|
55
|
+
prosody: config.speed !== 1.0 ? { speed: config.speed } : undefined,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
log.info(
|
|
59
|
+
{
|
|
60
|
+
referenceId: config.referenceId,
|
|
61
|
+
format: config.format,
|
|
62
|
+
textLength: text.length,
|
|
63
|
+
},
|
|
64
|
+
"Starting Fish Audio synthesis",
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const response = await fetch("https://api.fish.audio/v1/tts", {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: {
|
|
70
|
+
Authorization: `Bearer ${apiKey}`,
|
|
71
|
+
"Content-Type": "application/json",
|
|
72
|
+
},
|
|
73
|
+
body: JSON.stringify(body),
|
|
74
|
+
signal: options?.signal,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
const errorText = await response.text();
|
|
79
|
+
throw new Error(`Fish Audio API error (${response.status}): ${errorText}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!response.body) {
|
|
83
|
+
throw new Error("Fish Audio API returned no body");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const chunks: Uint8Array[] = [];
|
|
87
|
+
const reader = response.body.getReader();
|
|
88
|
+
let isFirstChunk = true;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
while (true) {
|
|
92
|
+
const timeoutMs = isFirstChunk ? FIRST_CHUNK_TIMEOUT_MS : IDLE_TIMEOUT_MS;
|
|
93
|
+
let timerId: ReturnType<typeof setTimeout>;
|
|
94
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
95
|
+
timerId = setTimeout(
|
|
96
|
+
() => reject(new Error(`Fish Audio read timed out after ${timeoutMs}ms`)),
|
|
97
|
+
timeoutMs,
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
let done: boolean;
|
|
101
|
+
let value: Uint8Array | undefined;
|
|
102
|
+
try {
|
|
103
|
+
({ done, value } = await Promise.race([reader.read(), timeout]));
|
|
104
|
+
} finally {
|
|
105
|
+
clearTimeout(timerId!);
|
|
106
|
+
}
|
|
107
|
+
if (done) break;
|
|
108
|
+
if (value) {
|
|
109
|
+
isFirstChunk = false;
|
|
110
|
+
chunks.push(value);
|
|
111
|
+
options?.onChunk?.(value);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} catch (err) {
|
|
115
|
+
try { await reader.cancel(); } catch { /* Ignore cancellation errors */ }
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const totalLength = chunks.reduce((sum, c) => sum + c.byteLength, 0);
|
|
120
|
+
const merged = new Uint8Array(totalLength);
|
|
121
|
+
let offset = 0;
|
|
122
|
+
for (const chunk of chunks) {
|
|
123
|
+
merged.set(chunk, offset);
|
|
124
|
+
offset += chunk.byteLength;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
log.debug({ bytes: totalLength }, "Fish Audio synthesis complete");
|
|
128
|
+
return Buffer.from(merged);
|
|
129
|
+
}
|
|
@@ -139,6 +139,12 @@ export interface RelayEndMessage {
|
|
|
139
139
|
handoffData?: string;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
export interface RelayPlayMessage {
|
|
143
|
+
type: "play";
|
|
144
|
+
source: string;
|
|
145
|
+
interruptible: boolean;
|
|
146
|
+
}
|
|
147
|
+
|
|
142
148
|
// ── WebSocket data type ──────────────────────────────────────────────
|
|
143
149
|
|
|
144
150
|
export interface RelayWebSocketData {
|
|
@@ -323,6 +329,27 @@ export class RelayConnection {
|
|
|
323
329
|
}
|
|
324
330
|
}
|
|
325
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Send a play-audio URL to the caller. Used when the assistant handles
|
|
334
|
+
* TTS synthesis itself (e.g. Fish Audio) instead of relying on
|
|
335
|
+
* ConversationRelay's built-in TTS.
|
|
336
|
+
*/
|
|
337
|
+
sendPlayUrl(url: string): void {
|
|
338
|
+
const message: RelayPlayMessage = {
|
|
339
|
+
type: "play",
|
|
340
|
+
source: url,
|
|
341
|
+
interruptible: true,
|
|
342
|
+
};
|
|
343
|
+
try {
|
|
344
|
+
this.ws.send(JSON.stringify(message));
|
|
345
|
+
} catch (err) {
|
|
346
|
+
log.error(
|
|
347
|
+
{ err, callSessionId: this.callSessionId },
|
|
348
|
+
"Failed to send play URL",
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
326
353
|
/**
|
|
327
354
|
* End the ConversationRelay session.
|
|
328
355
|
*/
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findContactByAddress,
|
|
3
|
+
findGuardianForChannel,
|
|
4
|
+
listContacts,
|
|
5
|
+
listGuardianChannels,
|
|
6
|
+
} from "../contacts/contact-store.js";
|
|
7
|
+
import { getAssistantName } from "../daemon/identity-helpers.js";
|
|
8
|
+
import { DEFAULT_USER_REFERENCE, resolveGuardianName } from "../prompts/user-reference.js";
|
|
9
|
+
import { getLogger } from "../util/logger.js";
|
|
10
|
+
|
|
11
|
+
const logger = getLogger("stt-hints");
|
|
12
|
+
|
|
13
|
+
export interface SttHintsInput {
|
|
14
|
+
staticHints: string[];
|
|
15
|
+
assistantName: string | null;
|
|
16
|
+
guardianName: string | null;
|
|
17
|
+
taskDescription: string | null;
|
|
18
|
+
targetContactName: string | null;
|
|
19
|
+
callerContactName: string | null;
|
|
20
|
+
inviteFriendName: string | null;
|
|
21
|
+
inviteGuardianName: string | null;
|
|
22
|
+
recentContactNames: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const MAX_HINTS_LENGTH = 500;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Assemble STT vocabulary hints from multiple sources into a single
|
|
29
|
+
* comma-separated string suitable for speech-to-text provider hint APIs.
|
|
30
|
+
*
|
|
31
|
+
* Pure function — no DB or filesystem dependencies.
|
|
32
|
+
*/
|
|
33
|
+
export function buildSttHints(input: SttHintsInput): string {
|
|
34
|
+
const hints: string[] = [...input.staticHints];
|
|
35
|
+
|
|
36
|
+
if (input.assistantName != null && input.assistantName.trim().length > 0) {
|
|
37
|
+
hints.push(input.assistantName.trim());
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (
|
|
41
|
+
input.guardianName != null &&
|
|
42
|
+
input.guardianName.trim().length > 0 &&
|
|
43
|
+
input.guardianName.trim() !== DEFAULT_USER_REFERENCE
|
|
44
|
+
) {
|
|
45
|
+
hints.push(input.guardianName.trim());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (input.inviteFriendName != null && input.inviteFriendName.trim().length > 0) {
|
|
49
|
+
hints.push(input.inviteFriendName.trim());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (input.inviteGuardianName != null && input.inviteGuardianName.trim().length > 0) {
|
|
53
|
+
hints.push(input.inviteGuardianName.trim());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (input.targetContactName != null && input.targetContactName.trim().length > 0) {
|
|
57
|
+
hints.push(input.targetContactName.trim());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (input.callerContactName != null && input.callerContactName.trim().length > 0) {
|
|
61
|
+
hints.push(input.callerContactName.trim());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Extract potential proper nouns from task description.
|
|
65
|
+
// Split on sentence boundaries, then for each sentence take words
|
|
66
|
+
// after the first that start with an uppercase letter.
|
|
67
|
+
if (input.taskDescription != null && input.taskDescription.trim().length > 0) {
|
|
68
|
+
// Split on sentence-ending punctuation followed by whitespace, but avoid
|
|
69
|
+
// splitting on periods after common abbreviations (Dr., Mr., etc.) so that
|
|
70
|
+
// names like "Dr. Smith" aren't fragmented and dropped by the first-word skip.
|
|
71
|
+
const sentences = input.taskDescription.split(
|
|
72
|
+
/(?<!\b(?:Mr|Mrs|Ms|Dr|Jr|Sr|St|Rev|Prof|Gen|Sgt|Lt|Col))[.]\s+|[!?]\s+/,
|
|
73
|
+
);
|
|
74
|
+
for (const sentence of sentences) {
|
|
75
|
+
const words = sentence.trim().split(/\s+/);
|
|
76
|
+
// Skip the first word (always capitalized at sentence start)
|
|
77
|
+
for (let i = 1; i < words.length; i++) {
|
|
78
|
+
// Use Unicode-aware \p{L} to preserve accented/non-Latin letters (José, Łukasz, etc.)
|
|
79
|
+
const word = words[i].replace(/[^\p{L}'-]/gu, "");
|
|
80
|
+
if (word.length > 0 && /^\p{Lu}/u.test(word)) {
|
|
81
|
+
hints.push(word);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
hints.push(...input.recentContactNames);
|
|
88
|
+
|
|
89
|
+
// Deduplicate (case-insensitive), filter empty/whitespace-only, trim each
|
|
90
|
+
const seen = new Set<string>();
|
|
91
|
+
const deduped: string[] = [];
|
|
92
|
+
for (const hint of hints) {
|
|
93
|
+
const trimmed = hint.trim();
|
|
94
|
+
if (trimmed.length === 0) continue;
|
|
95
|
+
const key = trimmed.toLowerCase();
|
|
96
|
+
if (seen.has(key)) continue;
|
|
97
|
+
seen.add(key);
|
|
98
|
+
deduped.push(trimmed);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const joined = deduped.join(",");
|
|
102
|
+
|
|
103
|
+
if (joined.length <= MAX_HINTS_LENGTH) {
|
|
104
|
+
return joined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Truncate at the last comma before the limit to avoid partial words
|
|
108
|
+
const truncated = joined.slice(0, MAX_HINTS_LENGTH);
|
|
109
|
+
const lastComma = truncated.lastIndexOf(",");
|
|
110
|
+
if (lastComma === -1) {
|
|
111
|
+
// Single hint that exceeds the limit — return it truncated
|
|
112
|
+
return truncated;
|
|
113
|
+
}
|
|
114
|
+
return truncated.slice(0, lastComma);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Wire real data sources (contacts DB, identity helpers, config) into
|
|
119
|
+
* {@link buildSttHints}. All DB lookups are best-effort — errors are
|
|
120
|
+
* logged but never propagate so hints can never fail a call.
|
|
121
|
+
*/
|
|
122
|
+
export function resolveCallHints(
|
|
123
|
+
session: {
|
|
124
|
+
task: string | null;
|
|
125
|
+
toNumber: string;
|
|
126
|
+
fromNumber: string;
|
|
127
|
+
direction: "inbound" | "outbound";
|
|
128
|
+
inviteFriendName: string | null;
|
|
129
|
+
inviteGuardianName: string | null;
|
|
130
|
+
} | null,
|
|
131
|
+
staticHints: string[],
|
|
132
|
+
): string {
|
|
133
|
+
const assistantName = getAssistantName();
|
|
134
|
+
|
|
135
|
+
// Look up the guardian contact for a displayName fallback (mirrors relay-server pattern)
|
|
136
|
+
let guardianDisplayName: string | undefined;
|
|
137
|
+
try {
|
|
138
|
+
const voiceGuardian = findGuardianForChannel("phone");
|
|
139
|
+
const guardianChannels = voiceGuardian ? null : listGuardianChannels();
|
|
140
|
+
const guardianContact = voiceGuardian?.contact ?? guardianChannels?.contact;
|
|
141
|
+
guardianDisplayName = guardianContact?.displayName;
|
|
142
|
+
} catch (err) {
|
|
143
|
+
logger.warn({ err }, "Failed to look up guardian contact for STT hints");
|
|
144
|
+
}
|
|
145
|
+
const guardianName = resolveGuardianName(guardianDisplayName);
|
|
146
|
+
|
|
147
|
+
let targetContactName: string | null = null;
|
|
148
|
+
let callerContactName: string | null = null;
|
|
149
|
+
let recentContactNames: string[] = [];
|
|
150
|
+
|
|
151
|
+
// For inbound calls, fromNumber is the caller (the interesting party);
|
|
152
|
+
// toNumber is the assistant's own Twilio number (not useful for contact lookup).
|
|
153
|
+
// For outbound calls, toNumber is who we're calling.
|
|
154
|
+
try {
|
|
155
|
+
if (session) {
|
|
156
|
+
const otherPartyNumber =
|
|
157
|
+
session.direction === "inbound" ? session.fromNumber : session.toNumber;
|
|
158
|
+
const otherPartyContact = findContactByAddress("phone", otherPartyNumber);
|
|
159
|
+
if (otherPartyContact) {
|
|
160
|
+
if (session.direction === "inbound") {
|
|
161
|
+
callerContactName = otherPartyContact.displayName;
|
|
162
|
+
} else {
|
|
163
|
+
targetContactName = otherPartyContact.displayName;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} catch (err) {
|
|
168
|
+
logger.warn({ err }, "Failed to look up contact for STT hints");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const recentContacts = listContacts(15);
|
|
173
|
+
recentContactNames = recentContacts.map((c) => c.displayName);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
logger.warn({ err }, "Failed to list recent contacts for STT hints");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return buildSttHints({
|
|
179
|
+
staticHints,
|
|
180
|
+
assistantName,
|
|
181
|
+
guardianName,
|
|
182
|
+
taskDescription: session?.task ?? null,
|
|
183
|
+
targetContactName,
|
|
184
|
+
callerContactName,
|
|
185
|
+
inviteFriendName: session?.inviteFriendName ?? null,
|
|
186
|
+
inviteGuardianName: session?.inviteGuardianName ?? null,
|
|
187
|
+
recentContactNames,
|
|
188
|
+
});
|
|
189
|
+
}
|