@vellumai/assistant 0.3.4 → 0.3.6
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/Dockerfile +2 -0
- package/README.md +88 -2
- package/eslint.config.mjs +31 -0
- package/package.json +1 -1
- package/scripts/ipc/check-swift-decoder-drift.ts +4 -1
- package/scripts/ipc/generate-swift.ts +31 -2
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +438 -1
- package/src/__tests__/approval-conversation-turn.test.ts +214 -0
- package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
- package/src/__tests__/approval-message-composer.test.ts +253 -0
- package/src/__tests__/browser-manager.test.ts +1 -0
- package/src/__tests__/call-conversation-messages.test.ts +130 -0
- package/src/__tests__/call-domain.test.ts +12 -2
- package/src/__tests__/call-orchestrator.test.ts +799 -249
- package/src/__tests__/call-pointer-messages.test.ts +148 -0
- package/src/__tests__/call-recovery.test.ts +3 -0
- package/src/__tests__/call-routes-http.test.ts +32 -2
- package/src/__tests__/call-store.test.ts +3 -0
- package/src/__tests__/channel-approval-routes.test.ts +1277 -98
- package/src/__tests__/channel-approval.test.ts +37 -0
- package/src/__tests__/channel-approvals.test.ts +36 -50
- package/src/__tests__/channel-guardian.test.ts +630 -22
- package/src/__tests__/channel-readiness-service.test.ts +324 -0
- package/src/__tests__/checker.test.ts +14 -7
- package/src/__tests__/clarification-resolver.test.ts +44 -24
- package/src/__tests__/commit-message-enrichment-service.test.ts +9 -4
- package/src/__tests__/computer-use-session-working-dir.test.ts +8 -0
- package/src/__tests__/config-schema.test.ts +14 -8
- package/src/__tests__/context-window-manager.test.ts +30 -2
- package/src/__tests__/contradiction-checker.test.ts +20 -5
- package/src/__tests__/credential-security-invariants.test.ts +7 -2
- package/src/__tests__/daemon-lifecycle.test.ts +13 -12
- package/src/__tests__/db-migration-rollback.test.ts +752 -0
- package/src/__tests__/dictation-mode-detection.test.ts +63 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +2 -0
- package/src/__tests__/entity-search.test.ts +615 -0
- package/src/__tests__/fuzzy-match-property.test.ts +5 -5
- package/src/__tests__/guardian-action-store.test.ts +123 -0
- package/src/__tests__/guardian-action-sweep.test.ts +277 -0
- package/src/__tests__/guardian-dispatch.test.ts +389 -0
- package/src/__tests__/guardian-question-copy.test.ts +47 -0
- package/src/__tests__/handlers-telegram-config.test.ts +4 -2
- package/src/__tests__/handlers-twilio-config.test.ts +533 -0
- package/src/__tests__/intent-routing.test.ts +2 -0
- package/src/__tests__/ipc-snapshot.test.ts +291 -1
- package/src/__tests__/memory-upsert-concurrency.test.ts +828 -0
- package/src/__tests__/messaging-send-tool.test.ts +65 -0
- package/src/__tests__/model-intents.test.ts +96 -0
- package/src/__tests__/no-direct-anthropic-sdk-imports.test.ts +42 -0
- package/src/__tests__/oauth2-gateway-transport.test.ts +130 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +2 -0
- package/src/__tests__/provider-commit-message-generator.test.ts +89 -13
- package/src/__tests__/provider-error-scenarios.test.ts +621 -0
- package/src/__tests__/provider-fail-open-selection.test.ts +119 -0
- package/src/__tests__/qdrant-manager.test.ts +27 -20
- package/src/__tests__/relay-server.test.ts +779 -40
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +6 -0
- package/src/__tests__/run-orchestrator.test.ts +42 -4
- package/src/__tests__/runtime-runs-http.test.ts +17 -1
- package/src/__tests__/runtime-runs.test.ts +16 -0
- package/src/__tests__/schedule-store.test.ts +18 -4
- package/src/__tests__/scheduler-recurrence.test.ts +13 -4
- package/src/__tests__/session-abort-tool-results.test.ts +6 -0
- package/src/__tests__/session-agent-loop.test.ts +857 -0
- package/src/__tests__/session-conflict-gate.test.ts +6 -0
- package/src/__tests__/session-pre-run-repair.test.ts +6 -0
- package/src/__tests__/session-profile-injection.test.ts +6 -0
- package/src/__tests__/session-provider-retry-repair.test.ts +6 -0
- package/src/__tests__/session-queue.test.ts +6 -0
- package/src/__tests__/session-runtime-assembly.test.ts +321 -13
- package/src/__tests__/session-slash-known.test.ts +6 -0
- package/src/__tests__/session-slash-queue.test.ts +6 -0
- package/src/__tests__/session-slash-unknown.test.ts +6 -0
- package/src/__tests__/session-surfaces-task-progress.test.ts +2 -0
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
- package/src/__tests__/session-workspace-injection.test.ts +6 -0
- package/src/__tests__/session-workspace-tool-tracking.test.ts +6 -0
- package/src/__tests__/skills.test.ts +2 -0
- package/src/__tests__/sms-messaging-provider.test.ts +126 -0
- package/src/__tests__/starter-task-flow.test.ts +2 -0
- package/src/__tests__/swarm-dag-pathological.test.ts +535 -0
- package/src/__tests__/system-prompt.test.ts +2 -0
- package/src/__tests__/task-management-tools.test.ts +2 -2
- package/src/__tests__/task-runner.test.ts +14 -4
- package/src/__tests__/terminal-tools.test.ts +25 -19
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +545 -0
- package/src/__tests__/tool-executor-shell-integration.test.ts +11 -11
- package/src/__tests__/tool-executor.test.ts +23 -24
- package/src/__tests__/trust-store.test.ts +3 -3
- package/src/__tests__/twilio-rest.test.ts +29 -0
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +3 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +11 -0
- package/src/__tests__/twilio-routes.test.ts +167 -11
- package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
- package/src/__tests__/user-reference.test.ts +2 -0
- package/src/__tests__/voice-quality.test.ts +222 -0
- package/src/__tests__/web-search.test.ts +46 -30
- package/src/__tests__/work-item-output.test.ts +110 -0
- package/src/agent/loop.ts +1 -1
- package/src/agent-heartbeat/agent-heartbeat-service.ts +2 -10
- package/src/amazon/client.ts +1418 -0
- package/src/amazon/request-extractor.ts +135 -0
- package/src/amazon/session.ts +109 -0
- package/src/autonomy/autonomy-store.ts +5 -5
- package/src/browser-extension-relay/client.ts +124 -0
- package/src/browser-extension-relay/protocol.ts +63 -0
- package/src/browser-extension-relay/server.ts +177 -0
- package/src/bundler/app-bundler.ts +3 -3
- package/src/bundler/bundle-signer.ts +1 -1
- package/src/bundler/signature-verifier.ts +1 -1
- package/src/calls/call-conversation-messages.ts +33 -0
- package/src/calls/call-domain.ts +114 -10
- package/src/calls/call-orchestrator.ts +268 -59
- package/src/calls/call-pointer-messages.ts +53 -0
- package/src/calls/call-recovery.ts +3 -8
- package/src/calls/call-store.ts +69 -87
- package/src/calls/elevenlabs-config.ts +3 -2
- package/src/calls/guardian-action-sweep.ts +105 -0
- package/src/calls/guardian-dispatch.ts +203 -0
- package/src/calls/guardian-question-copy.ts +133 -0
- package/src/calls/relay-server.ts +466 -8
- package/src/calls/speaker-identification.ts +1 -1
- package/src/calls/twilio-config.ts +22 -14
- package/src/calls/twilio-provider.ts +6 -4
- package/src/calls/twilio-rest.ts +308 -7
- package/src/calls/twilio-routes.ts +65 -12
- package/src/calls/types.ts +3 -1
- package/src/channels/types.ts +25 -0
- package/src/cli/amazon.ts +815 -0
- package/src/cli/config-commands.ts +2 -2
- package/src/cli/core-commands.ts +4 -3
- package/src/cli/influencer.ts +244 -0
- package/src/cli/map.ts +89 -6
- package/src/cli.ts +1 -1
- package/src/config/agent-schema.ts +171 -0
- package/src/config/bundled-skills/amazon/SKILL.md +127 -0
- package/src/config/bundled-skills/amazon/icon.svg +13 -0
- package/src/config/bundled-skills/api-mapping/SKILL.md +78 -0
- package/src/config/bundled-skills/browser/SKILL.md +1 -0
- package/src/config/bundled-skills/browser/TOOLS.json +17 -0
- package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +25 -0
- package/src/config/bundled-skills/doordash/SKILL.md +51 -51
- package/src/config/bundled-skills/email-setup/SKILL.md +14 -5
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +183 -0
- package/src/config/bundled-skills/influencer/SKILL.md +144 -0
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
- package/src/config/bundled-skills/macos-automation/icon.svg +12 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +176 -0
- package/src/config/bundled-skills/media-processing/TOOLS.json +230 -0
- package/src/config/bundled-skills/media-processing/__tests__/concurrency-pool.test.ts +77 -0
- package/src/config/bundled-skills/media-processing/__tests__/cost-tracker.test.ts +69 -0
- package/src/config/bundled-skills/media-processing/__tests__/preprocess.test.ts +303 -0
- package/src/config/bundled-skills/media-processing/services/concurrency-pool.ts +55 -0
- package/src/config/bundled-skills/media-processing/services/cost-tracker.ts +86 -0
- package/src/config/bundled-skills/media-processing/services/gemini-map.ts +339 -0
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +551 -0
- package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +259 -0
- package/src/config/bundled-skills/media-processing/services/reduce.ts +197 -0
- package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +136 -0
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +59 -0
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
- package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
- package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +143 -0
- package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
- package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +65 -0
- package/src/config/bundled-skills/messaging/SKILL.md +33 -8
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -7
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +2 -1
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +88 -23
- package/src/config/bundled-skills/twitter/SKILL.md +19 -3
- package/src/config/bundled-skills/twitter/icon.svg +14 -0
- package/src/config/bundled-tool-registry.ts +310 -0
- package/src/config/calls-schema.ts +181 -0
- package/src/config/core-schema.ts +309 -0
- package/src/config/defaults.ts +28 -3
- package/src/config/env-registry.ts +162 -0
- package/src/config/env.ts +175 -0
- package/src/config/loader.ts +6 -6
- package/src/config/memory-schema.ts +528 -0
- package/src/config/sandbox-schema.ts +55 -0
- package/src/config/schema.ts +158 -1133
- package/src/config/skill-state.ts +1 -1
- package/src/config/skills-schema.ts +32 -0
- package/src/config/skills.ts +35 -24
- package/src/config/system-prompt.ts +131 -56
- package/src/config/templates/IDENTITY.md +2 -2
- package/src/config/templates/SOUL.md +1 -1
- package/src/config/types.ts +1 -0
- package/src/config/user-reference.ts +4 -9
- package/src/config/vellum-skills/catalog.json +6 -7
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +5 -1
- package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +4 -3
- package/src/config/vellum-skills/sms-setup/SKILL.md +216 -0
- package/src/config/vellum-skills/twilio-setup/SKILL.md +40 -8
- package/src/context/window-manager.ts +27 -7
- package/src/daemon/approval-generators.ts +186 -0
- package/src/daemon/approved-devices-store.ts +140 -0
- package/src/daemon/assistant-attachments.ts +1 -1
- package/src/daemon/classifier.ts +35 -32
- package/src/daemon/config-watcher.ts +1 -1
- package/src/daemon/daemon-control.ts +217 -0
- package/src/daemon/handlers/apps.ts +2 -3
- package/src/daemon/handlers/config-channels.ts +158 -0
- package/src/daemon/handlers/config-inbox.ts +540 -0
- package/src/daemon/handlers/config-ingress.ts +231 -0
- package/src/daemon/handlers/config-integrations.ts +258 -0
- package/src/daemon/handlers/config-model.ts +143 -0
- package/src/daemon/handlers/config-parental.ts +163 -0
- package/src/daemon/handlers/config-scheduling.ts +172 -0
- package/src/daemon/handlers/config-slack.ts +92 -0
- package/src/daemon/handlers/config-telegram.ts +301 -0
- package/src/daemon/handlers/config-tools.ts +177 -0
- package/src/daemon/handlers/config-trust.ts +104 -0
- package/src/daemon/handlers/config-twilio.ts +1080 -0
- package/src/daemon/handlers/config.ts +53 -1689
- package/src/daemon/handlers/diagnostics.ts +1 -1
- package/src/daemon/handlers/dictation.ts +180 -0
- package/src/daemon/handlers/documents.ts +18 -32
- package/src/daemon/handlers/identity.ts +14 -23
- package/src/daemon/handlers/index.ts +11 -0
- package/src/daemon/handlers/misc.ts +3 -5
- package/src/daemon/handlers/pairing.ts +98 -0
- package/src/daemon/handlers/sessions.ts +56 -5
- package/src/daemon/handlers/shared.ts +6 -1
- package/src/daemon/handlers/skills.ts +1 -1
- package/src/daemon/handlers/twitter-auth.ts +2 -0
- package/src/daemon/handlers/work-items.ts +17 -9
- package/src/daemon/handlers/workspace-files.ts +4 -3
- package/src/daemon/install-cli-launchers.ts +113 -0
- package/src/daemon/ipc-contract/apps.ts +356 -0
- package/src/daemon/ipc-contract/browser.ts +74 -0
- package/src/daemon/ipc-contract/computer-use.ts +151 -0
- package/src/daemon/ipc-contract/diagnostics.ts +56 -0
- package/src/daemon/ipc-contract/documents.ts +74 -0
- package/src/daemon/ipc-contract/inbox.ts +209 -0
- package/src/daemon/ipc-contract/integrations.ts +284 -0
- package/src/daemon/ipc-contract/memory.ts +48 -0
- package/src/daemon/ipc-contract/messages.ts +211 -0
- package/src/daemon/ipc-contract/pairing.ts +45 -0
- package/src/daemon/ipc-contract/parental-control.ts +95 -0
- package/src/daemon/ipc-contract/schedules.ts +97 -0
- package/src/daemon/ipc-contract/sessions.ts +315 -0
- package/src/daemon/ipc-contract/shared.ts +42 -0
- package/src/daemon/ipc-contract/skills.ts +120 -0
- package/src/daemon/ipc-contract/subagents.ts +58 -0
- package/src/daemon/ipc-contract/surfaces.ts +250 -0
- package/src/daemon/ipc-contract/trust.ts +60 -0
- package/src/daemon/ipc-contract/work-items.ts +225 -0
- package/src/daemon/ipc-contract/workspace.ts +113 -0
- package/src/daemon/ipc-contract-inventory.json +70 -0
- package/src/daemon/ipc-contract-inventory.ts +55 -29
- package/src/daemon/ipc-contract.ts +229 -2426
- package/src/daemon/ipc-protocol.ts +1 -1
- package/src/daemon/ipc-validate.ts +7 -0
- package/src/daemon/lifecycle.ts +97 -377
- package/src/daemon/pairing-store.ts +177 -0
- package/src/daemon/providers-setup.ts +43 -0
- package/src/daemon/ride-shotgun-handler.ts +68 -3
- package/src/daemon/server.ts +66 -46
- package/src/daemon/session-agent-loop-handlers.ts +421 -0
- package/src/daemon/session-agent-loop.ts +117 -275
- package/src/daemon/session-dynamic-profile.ts +1 -1
- package/src/daemon/session-history.ts +1 -1
- package/src/daemon/session-media-retry.ts +1 -1
- package/src/daemon/session-messaging.ts +37 -2
- package/src/daemon/session-notifiers.ts +5 -25
- package/src/daemon/session-process.ts +99 -59
- package/src/daemon/session-queue-manager.ts +96 -4
- package/src/daemon/session-runtime-assembly.ts +199 -10
- package/src/daemon/session-surfaces.ts +19 -4
- package/src/daemon/session-tool-setup.ts +30 -30
- package/src/daemon/session-workspace.ts +1 -1
- package/src/daemon/session.ts +35 -2
- package/src/daemon/shutdown-handlers.ts +122 -0
- package/src/daemon/trace-emitter.ts +1 -1
- package/src/daemon/watch-handler.ts +36 -33
- package/src/doordash/cart-queries.ts +787 -0
- package/src/doordash/client.ts +144 -127
- package/src/doordash/order-queries.ts +85 -0
- package/src/doordash/queries.ts +10 -1308
- package/src/doordash/search-queries.ts +203 -0
- package/src/doordash/session.ts +3 -2
- package/src/doordash/store-queries.ts +246 -0
- package/src/doordash/types.ts +367 -0
- package/src/email/providers/agentmail.ts +2 -1
- package/src/email/providers/index.ts +3 -2
- package/src/email/service.ts +3 -2
- package/src/errors.ts +43 -0
- package/src/home-base/prebuilt/seed.ts +1 -1
- package/src/hooks/cli.ts +6 -5
- package/src/hooks/config.ts +6 -8
- package/src/hooks/discovery.ts +6 -5
- package/src/hooks/manager.ts +4 -3
- package/src/hooks/runner.ts +2 -2
- package/src/hooks/templates.ts +5 -5
- package/src/inbound/public-ingress-urls.ts +6 -4
- package/src/index.ts +4 -2
- package/src/influencer/client.ts +1104 -0
- package/src/instrument.ts +4 -3
- package/src/logfire.ts +4 -3
- package/src/memory/admin.ts +25 -35
- package/src/memory/attachments-store.ts +4 -7
- package/src/memory/channel-delivery-store.ts +30 -1
- package/src/memory/channel-guardian-store.ts +202 -2
- package/src/memory/clarification-resolver.ts +37 -33
- package/src/memory/conflict-store.ts +67 -61
- package/src/memory/contradiction-checker.ts +141 -117
- package/src/memory/conversation-store.ts +335 -51
- package/src/memory/db-connection.ts +27 -4
- package/src/memory/db-init.ts +265 -4
- package/src/memory/db.ts +14 -1
- package/src/memory/embedding-backend.ts +27 -5
- package/src/memory/embedding-ollama.ts +2 -1
- package/src/memory/entity-extractor.ts +38 -35
- package/src/memory/guardian-action-store.ts +430 -0
- package/src/memory/inbox-escalation-projection.ts +59 -0
- package/src/memory/inbox-thread-store.ts +218 -0
- package/src/memory/ingress-invite-store.ts +338 -0
- package/src/memory/ingress-member-store.ts +350 -0
- package/src/memory/items-extractor.ts +91 -97
- package/src/memory/job-handlers/index-maintenance.ts +3 -3
- package/src/memory/job-handlers/media-processing.ts +69 -0
- package/src/memory/job-handlers/summarization.ts +32 -26
- package/src/memory/job-utils.ts +3 -10
- package/src/memory/jobs-store.ts +8 -10
- package/src/memory/jobs-worker.ts +55 -36
- package/src/memory/media-store.ts +759 -0
- package/src/memory/migrations/001-job-deferrals.ts +45 -0
- package/src/memory/migrations/002-tool-invocations-fk.ts +43 -0
- package/src/memory/migrations/003-memory-fts-backfill.ts +24 -0
- package/src/memory/migrations/004-entity-relation-dedup.ts +87 -0
- package/src/memory/migrations/005-fingerprint-scope-unique.ts +80 -0
- package/src/memory/migrations/006-scope-salted-fingerprints.ts +62 -0
- package/src/memory/migrations/007-assistant-id-to-self.ts +254 -0
- package/src/memory/migrations/008-remove-assistant-id-columns.ts +208 -0
- package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +83 -0
- package/src/memory/migrations/010-ext-conv-bindings-channel-chat-unique.ts +56 -0
- package/src/memory/migrations/011-call-sessions-provider-sid-dedup.ts +63 -0
- package/src/memory/migrations/012-call-sessions-add-initiated-from.ts +19 -0
- package/src/memory/migrations/013-guardian-action-tables.ts +68 -0
- package/src/memory/migrations/014-backfill-inbox-thread-state.ts +76 -0
- package/src/memory/migrations/015-drop-active-search-index.ts +27 -0
- package/src/memory/migrations/016-memory-segments-indexes.ts +11 -0
- package/src/memory/migrations/017-memory-items-indexes.ts +10 -0
- package/src/memory/migrations/018-remaining-table-indexes.ts +13 -0
- package/src/memory/migrations/index.ts +24 -0
- package/src/memory/migrations/registry.ts +79 -0
- package/src/memory/migrations/validate-migration-state.ts +69 -0
- package/src/memory/qdrant-manager.ts +49 -8
- package/src/memory/query-builder.ts +1 -1
- package/src/memory/raw-query.ts +119 -0
- package/src/memory/recall-cache.ts +4 -1
- package/src/memory/retriever.ts +165 -47
- package/src/memory/schema-migration.ts +25 -984
- package/src/memory/schema.ts +228 -7
- package/src/memory/search/entity.ts +205 -31
- package/src/memory/search/lexical.ts +81 -52
- package/src/memory/search/ranking.ts +27 -23
- package/src/memory/search/semantic.ts +157 -19
- package/src/memory/search/types.ts +24 -0
- package/src/memory/shared-app-links-store.ts +4 -5
- package/src/memory/validation.ts +19 -0
- package/src/messaging/draft-store.ts +5 -6
- package/src/messaging/provider-types.ts +2 -0
- package/src/messaging/providers/sms/adapter.ts +201 -0
- package/src/messaging/providers/sms/client.ts +93 -0
- package/src/messaging/providers/sms/types.ts +7 -0
- package/src/messaging/providers/telegram-bot/adapter.ts +2 -5
- package/src/messaging/providers/whatsapp/adapter.ts +136 -0
- package/src/messaging/providers/whatsapp/client.ts +67 -0
- package/src/messaging/style-analyzer.ts +5 -4
- package/src/messaging/thread-summarizer.ts +61 -69
- package/src/messaging/triage-engine.ts +62 -71
- package/src/migrations/config-merge.ts +53 -0
- package/src/migrations/data-layout.ts +68 -0
- package/src/migrations/data-merge.ts +33 -0
- package/src/migrations/hooks-merge.ts +90 -0
- package/src/migrations/index.ts +6 -0
- package/src/migrations/log.ts +23 -0
- package/src/migrations/skills-merge.ts +33 -0
- package/src/migrations/workspace-layout.ts +79 -0
- package/src/permissions/checker.ts +133 -11
- package/src/permissions/prompter.ts +14 -0
- package/src/permissions/shell-identity.ts +31 -1
- package/src/permissions/trust-store.ts +21 -1
- package/src/providers/anthropic/client.ts +4 -4
- package/src/providers/failover.ts +2 -2
- package/src/providers/model-intents.ts +70 -0
- package/src/providers/ollama/client.ts +2 -1
- package/src/providers/provider-send-message.ts +176 -0
- package/src/providers/registry.ts +71 -30
- package/src/providers/retry.ts +35 -1
- package/src/providers/types.ts +12 -1
- package/src/runtime/approval-conversation-turn.ts +97 -0
- package/src/runtime/approval-message-composer.ts +253 -0
- package/src/runtime/channel-approval-parser.ts +36 -2
- package/src/runtime/channel-approvals.ts +11 -24
- package/src/runtime/channel-guardian-service.ts +88 -21
- package/src/runtime/channel-readiness-service.ts +418 -0
- package/src/runtime/channel-readiness-types.ts +35 -0
- package/src/runtime/channel-retry-sweep.ts +184 -0
- package/src/runtime/guardian-context-resolver.ts +108 -0
- package/src/runtime/http-server.ts +275 -717
- package/src/runtime/http-types.ts +59 -3
- package/src/runtime/middleware/auth.ts +116 -0
- package/src/runtime/middleware/error-handler.ts +33 -0
- package/src/runtime/middleware/twilio-validation.ts +127 -0
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/call-routes.ts +51 -7
- package/src/runtime/routes/channel-delivery-routes.ts +170 -0
- package/src/runtime/routes/channel-guardian-routes.ts +1191 -0
- package/src/runtime/routes/channel-inbound-routes.ts +1152 -0
- package/src/runtime/routes/channel-route-shared.ts +144 -0
- package/src/runtime/routes/channel-routes.ts +32 -1588
- package/src/runtime/routes/conversation-routes.ts +50 -7
- package/src/runtime/routes/events-routes.ts +2 -2
- package/src/runtime/routes/identity-routes.ts +126 -0
- package/src/runtime/routes/pairing-routes.ts +143 -0
- package/src/runtime/routes/run-routes.ts +15 -1
- package/src/runtime/run-orchestrator.ts +86 -35
- package/src/schedule/schedule-store.ts +36 -32
- package/src/schedule/scheduler.ts +3 -3
- package/src/security/encrypted-store.ts +5 -7
- package/src/security/oauth2.ts +45 -15
- package/src/security/parental-control-store.ts +183 -0
- package/src/security/secret-allowlist.ts +4 -3
- package/src/security/secret-scanner.ts +5 -5
- package/src/security/secure-keys.ts +1 -1
- package/src/security/token-manager.ts +3 -2
- package/src/services/vercel-deploy.ts +6 -2
- package/src/skills/tool-manifest.ts +3 -3
- package/src/skills/vellum-catalog-remote.ts +75 -16
- package/src/slack/slack-webhook.ts +2 -1
- package/src/swarm/orchestrator.ts +92 -1
- package/src/swarm/router-planner.ts +6 -9
- package/src/swarm/worker-prompts.ts +9 -12
- package/src/tasks/task-compiler.ts +19 -28
- package/src/tasks/task-runner.ts +1 -1
- package/src/tools/assets/materialize.ts +2 -2
- package/src/tools/assets/search.ts +15 -14
- package/src/tools/browser/__tests__/auth-detector.test.ts +1 -0
- package/src/tools/browser/auto-navigate.ts +1 -0
- package/src/tools/browser/browser-execution.ts +10 -1
- package/src/tools/browser/browser-manager.ts +119 -4
- package/src/tools/browser/network-recorder.ts +5 -0
- package/src/tools/calls/call-start.ts +1 -0
- package/src/tools/credentials/broker.ts +11 -2
- package/src/tools/credentials/metadata-store.ts +18 -14
- package/src/tools/credentials/post-connect-hooks.ts +61 -0
- package/src/tools/credentials/vault.ts +49 -23
- package/src/tools/execution-target.ts +11 -1
- package/src/tools/executor.ts +68 -9
- package/src/tools/host-terminal/cli-discover.ts +1 -1
- package/src/tools/network/script-proxy/http-forwarder.ts +1 -1
- package/src/tools/network/script-proxy/mitm-handler.ts +1 -1
- package/src/tools/network/script-proxy/server.ts +1 -1
- package/src/tools/network/script-proxy/session-manager.ts +6 -5
- package/src/tools/network/web-fetch.ts +18 -2
- package/src/tools/network/web-search.ts +8 -4
- package/src/tools/reminder/reminder-store.ts +14 -15
- package/src/tools/schedule/create.ts +1 -0
- package/src/tools/schedule/list.ts +2 -1
- package/src/tools/shared/filesystem/file-ops-service.ts +5 -7
- package/src/tools/skills/skill-script-runner.ts +24 -9
- package/src/tools/skills/skill-tool-factory.ts +1 -0
- package/src/tools/tasks/work-item-enqueue.ts +2 -2
- package/src/tools/terminal/evaluate-typescript.ts +21 -12
- package/src/tools/terminal/parser.ts +50 -0
- package/src/tools/types.ts +2 -0
- package/src/tools/watcher/delete.ts +6 -0
- package/src/tools/weather/service.ts +1 -1
- package/src/twitter/client.ts +190 -24
- package/src/twitter/router.ts +1 -1
- package/src/twitter/session.ts +4 -3
- package/src/util/clipboard.ts +1 -1
- package/src/util/errors.ts +65 -8
- package/src/util/fs.ts +40 -0
- package/src/util/json.ts +10 -0
- package/src/util/log-redact.ts +189 -0
- package/src/util/logger.ts +19 -17
- package/src/util/object.ts +3 -0
- package/src/util/platform.ts +105 -363
- package/src/util/pricing.ts +1 -1
- package/src/util/promise-guard.ts +1 -1
- package/src/util/retry.ts +19 -0
- package/src/util/row-mapper.ts +79 -0
- package/src/util/silently.ts +21 -0
- package/src/watcher/engine.ts +5 -1
- package/src/watcher/provider-types.ts +20 -0
- package/src/watcher/providers/github.ts +156 -0
- package/src/watcher/providers/gmail.ts +1 -0
- package/src/watcher/providers/google-calendar.ts +1 -0
- package/src/watcher/providers/linear.ts +460 -0
- package/src/watcher/providers/slack.ts +1 -0
- package/src/work-items/work-item-runner.ts +1 -1
- package/src/workspace/git-service.ts +1 -1
- package/src/workspace/provider-commit-message-generator.ts +51 -22
- package/src/__tests__/call-bridge.test.ts +0 -517
- package/src/__tests__/session-process-bridge.test.ts +0 -244
- package/src/calls/call-bridge.ts +0 -168
- package/src/config/vellum-skills/google-oauth-setup/SKILL.md +0 -199
|
@@ -17,7 +17,6 @@ import { describe, test, expect, beforeEach, afterAll, mock, type Mock } from 'b
|
|
|
17
17
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
18
18
|
import { tmpdir } from 'node:os';
|
|
19
19
|
import { join } from 'node:path';
|
|
20
|
-
import { EventEmitter } from 'node:events';
|
|
21
20
|
|
|
22
21
|
const testDir = mkdtempSync(join(tmpdir(), 'relay-server-test-'));
|
|
23
22
|
|
|
@@ -33,6 +32,7 @@ mock.module('../util/platform.js', () => ({
|
|
|
33
32
|
getDbPath: () => join(testDir, 'test.db'),
|
|
34
33
|
getLogPath: () => join(testDir, 'test.log'),
|
|
35
34
|
ensureDataDir: () => {},
|
|
35
|
+
readHttpToken: () => null,
|
|
36
36
|
}));
|
|
37
37
|
|
|
38
38
|
mock.module('../util/logger.js', () => ({
|
|
@@ -44,53 +44,75 @@ mock.module('../util/logger.js', () => ({
|
|
|
44
44
|
|
|
45
45
|
// ── Config mock ─────────────────────────────────────────────────────
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
47
|
+
const mockConfig = {
|
|
48
|
+
provider: 'anthropic',
|
|
49
|
+
providerOrder: ['anthropic'],
|
|
50
|
+
apiKeys: { anthropic: 'test-key' },
|
|
51
|
+
calls: {
|
|
52
|
+
enabled: true,
|
|
53
|
+
provider: 'twilio',
|
|
54
|
+
maxDurationSeconds: 3600,
|
|
55
|
+
userConsultTimeoutSeconds: 120,
|
|
56
|
+
disclosure: { enabled: false, text: '' },
|
|
57
|
+
safety: { denyCategories: [] },
|
|
58
|
+
verification: {
|
|
59
|
+
enabled: false,
|
|
60
|
+
maxAttempts: 3,
|
|
61
|
+
codeLength: 6,
|
|
57
62
|
},
|
|
58
|
-
}
|
|
63
|
+
},
|
|
64
|
+
memory: { enabled: false },
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
mock.module('../config/loader.js', () => ({
|
|
68
|
+
getConfig: () => mockConfig,
|
|
59
69
|
}));
|
|
60
70
|
|
|
61
|
-
// ──
|
|
71
|
+
// ── Helpers for building mock provider responses ────────────────────
|
|
62
72
|
|
|
63
|
-
function
|
|
64
|
-
const emitter = new EventEmitter();
|
|
73
|
+
function createMockProviderResponse(tokens: string[]) {
|
|
65
74
|
const fullText = tokens.join('');
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
75
|
+
return async (
|
|
76
|
+
_messages: unknown[],
|
|
77
|
+
_tools: unknown[],
|
|
78
|
+
_systemPrompt: string,
|
|
79
|
+
options?: { onEvent?: (event: { type: string; text?: string }) => void; signal?: AbortSignal },
|
|
80
|
+
) => {
|
|
81
|
+
for (const token of tokens) {
|
|
82
|
+
options?.onEvent?.({ type: 'text_delta', text: token });
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
content: [{ type: 'text', text: fullText }],
|
|
86
|
+
model: 'claude-sonnet-4-20250514',
|
|
87
|
+
usage: { inputTokens: 100, outputTokens: 50 },
|
|
88
|
+
stopReason: 'end_turn',
|
|
89
|
+
};
|
|
80
90
|
};
|
|
81
|
-
|
|
82
|
-
return stream;
|
|
83
91
|
}
|
|
84
92
|
|
|
85
|
-
|
|
93
|
+
// ── Provider registry mock ──────────────────────────────────────────
|
|
86
94
|
|
|
87
|
-
|
|
88
|
-
|
|
95
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
96
|
+
let mockSendMessage: Mock<any>;
|
|
97
|
+
|
|
98
|
+
mock.module('../providers/registry.js', () => {
|
|
99
|
+
mockSendMessage = mock(createMockProviderResponse(['Hello']));
|
|
89
100
|
return {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
101
|
+
listProviders: () => ['anthropic'],
|
|
102
|
+
getFailoverProvider: () => ({
|
|
103
|
+
name: 'anthropic',
|
|
104
|
+
sendMessage: (...args: unknown[]) => mockSendMessage(...args),
|
|
105
|
+
}),
|
|
106
|
+
getDefaultModel: (providerName: string) => {
|
|
107
|
+
const defaults: Record<string, string> = {
|
|
108
|
+
anthropic: 'claude-opus-4-6',
|
|
109
|
+
openai: 'gpt-5.2',
|
|
110
|
+
gemini: 'gemini-3-flash',
|
|
111
|
+
ollama: 'llama3.2',
|
|
112
|
+
fireworks: 'accounts/fireworks/models/kimi-k2p5',
|
|
113
|
+
openrouter: 'x-ai/grok-4',
|
|
93
114
|
};
|
|
115
|
+
return defaults[providerName] ?? defaults.anthropic;
|
|
94
116
|
},
|
|
95
117
|
};
|
|
96
118
|
});
|
|
@@ -104,9 +126,15 @@ import {
|
|
|
104
126
|
getCallSession,
|
|
105
127
|
getCallEvents,
|
|
106
128
|
} from '../calls/call-store.js';
|
|
129
|
+
import { getMessages } from '../memory/conversation-store.js';
|
|
107
130
|
import { registerCallCompletionNotifier, unregisterCallCompletionNotifier } from '../calls/call-state.js';
|
|
108
131
|
import { RelayConnection, activeRelayConnections } from '../calls/relay-server.js';
|
|
109
132
|
import type { RelayWebSocketData } from '../calls/relay-server.js';
|
|
133
|
+
import {
|
|
134
|
+
createVerificationChallenge,
|
|
135
|
+
getGuardianBinding,
|
|
136
|
+
} from '../runtime/channel-guardian-service.js';
|
|
137
|
+
import { createBinding } from '../memory/channel-guardian-store.js';
|
|
110
138
|
|
|
111
139
|
initializeDb();
|
|
112
140
|
|
|
@@ -159,18 +187,48 @@ function ensureConversation(id: string): void {
|
|
|
159
187
|
|
|
160
188
|
function resetTables() {
|
|
161
189
|
const db = getDb();
|
|
190
|
+
db.run('DELETE FROM guardian_action_deliveries');
|
|
191
|
+
db.run('DELETE FROM guardian_action_requests');
|
|
162
192
|
db.run('DELETE FROM call_pending_questions');
|
|
163
193
|
db.run('DELETE FROM call_events');
|
|
164
194
|
db.run('DELETE FROM call_sessions');
|
|
195
|
+
db.run('DELETE FROM tool_invocations');
|
|
196
|
+
db.run('DELETE FROM messages');
|
|
165
197
|
db.run('DELETE FROM conversations');
|
|
198
|
+
db.run('DELETE FROM channel_guardian_verification_challenges');
|
|
199
|
+
db.run('DELETE FROM channel_guardian_bindings');
|
|
200
|
+
db.run('DELETE FROM channel_guardian_rate_limits');
|
|
166
201
|
ensuredConvIds = new Set();
|
|
167
202
|
}
|
|
168
203
|
|
|
204
|
+
function getLatestAssistantText(conversationId: string): string | null {
|
|
205
|
+
const messages = getMessages(conversationId).filter((m) => m.role === 'assistant');
|
|
206
|
+
if (messages.length === 0) return null;
|
|
207
|
+
const latest = messages[messages.length - 1];
|
|
208
|
+
try {
|
|
209
|
+
const parsed = JSON.parse(latest.content) as unknown;
|
|
210
|
+
if (Array.isArray(parsed)) {
|
|
211
|
+
return parsed
|
|
212
|
+
.filter((block): block is { type: string; text?: string } => typeof block === 'object' && block != null)
|
|
213
|
+
.filter((block) => block.type === 'text')
|
|
214
|
+
.map((block) => block.text ?? '')
|
|
215
|
+
.join('');
|
|
216
|
+
}
|
|
217
|
+
if (typeof parsed === 'string') return parsed;
|
|
218
|
+
} catch {
|
|
219
|
+
// Ignore parse failures and fall back to raw content.
|
|
220
|
+
}
|
|
221
|
+
return latest.content;
|
|
222
|
+
}
|
|
223
|
+
|
|
169
224
|
describe('relay-server', () => {
|
|
170
225
|
beforeEach(() => {
|
|
171
226
|
resetTables();
|
|
172
227
|
activeRelayConnections.clear();
|
|
173
|
-
|
|
228
|
+
mockSendMessage.mockImplementation(createMockProviderResponse(['Hello']));
|
|
229
|
+
mockConfig.calls.verification.enabled = false;
|
|
230
|
+
mockConfig.calls.verification.maxAttempts = 3;
|
|
231
|
+
mockConfig.calls.verification.codeLength = 6;
|
|
174
232
|
});
|
|
175
233
|
|
|
176
234
|
// ── Setup message handling ──────────────────────────────────────
|
|
@@ -211,6 +269,41 @@ describe('relay-server', () => {
|
|
|
211
269
|
relay.destroy();
|
|
212
270
|
});
|
|
213
271
|
|
|
272
|
+
test('handleMessage: setup triggers initial assistant greeting turn', async () => {
|
|
273
|
+
ensureConversation('conv-relay-setup-greet');
|
|
274
|
+
const session = createCallSession({
|
|
275
|
+
conversationId: 'conv-relay-setup-greet',
|
|
276
|
+
provider: 'twilio',
|
|
277
|
+
fromNumber: '+15551111111',
|
|
278
|
+
toNumber: '+15552222222',
|
|
279
|
+
task: 'Confirm appointment time',
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
mockSendMessage.mockImplementation(createMockProviderResponse(['Hello, I am calling to confirm your appointment.']));
|
|
283
|
+
|
|
284
|
+
const { ws, relay } = createMockWs(session.id);
|
|
285
|
+
|
|
286
|
+
await relay.handleMessage(JSON.stringify({
|
|
287
|
+
type: 'setup',
|
|
288
|
+
callSid: 'CA_setup_greet_123',
|
|
289
|
+
from: '+15551111111',
|
|
290
|
+
to: '+15552222222',
|
|
291
|
+
}));
|
|
292
|
+
|
|
293
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
294
|
+
|
|
295
|
+
const textMessages = ws.sentMessages
|
|
296
|
+
.map((raw) => JSON.parse(raw) as { type: string; token?: string; last?: boolean })
|
|
297
|
+
.filter((m) => m.type === 'text');
|
|
298
|
+
expect(textMessages.some((m) => (m.token ?? '').includes('confirm your appointment'))).toBe(true);
|
|
299
|
+
expect(textMessages.some((m) => m.last === true)).toBe(true);
|
|
300
|
+
|
|
301
|
+
const events = getCallEvents(session.id).filter((e) => e.eventType === 'assistant_spoke');
|
|
302
|
+
expect(events.length).toBeGreaterThan(0);
|
|
303
|
+
|
|
304
|
+
relay.destroy();
|
|
305
|
+
});
|
|
306
|
+
|
|
214
307
|
test('handleTransportClosed: normal close marks call completed and notifies completion', () => {
|
|
215
308
|
ensureConversation('conv-relay-close-normal');
|
|
216
309
|
const session = createCallSession({
|
|
@@ -235,6 +328,7 @@ describe('relay-server', () => {
|
|
|
235
328
|
const endedEvents = getCallEvents(session.id).filter((e) => e.eventType === 'call_ended');
|
|
236
329
|
expect(endedEvents.length).toBe(1);
|
|
237
330
|
expect(completionCount).toBe(1);
|
|
331
|
+
expect(getLatestAssistantText('conv-relay-close-normal')).toContain('**Call completed**');
|
|
238
332
|
|
|
239
333
|
unregisterCallCompletionNotifier('conv-relay-close-normal');
|
|
240
334
|
relay.destroy();
|
|
@@ -259,6 +353,7 @@ describe('relay-server', () => {
|
|
|
259
353
|
expect(updated!.lastError).toContain('abnormal closure');
|
|
260
354
|
const failEvents = getCallEvents(session.id).filter((e) => e.eventType === 'call_failed');
|
|
261
355
|
expect(failEvents.length).toBe(1);
|
|
356
|
+
expect(getLatestAssistantText('conv-relay-close-abnormal')).toContain('**Call failed**');
|
|
262
357
|
|
|
263
358
|
relay.destroy();
|
|
264
359
|
});
|
|
@@ -285,8 +380,9 @@ describe('relay-server', () => {
|
|
|
285
380
|
|
|
286
381
|
// Verify event recorded with custom parameters
|
|
287
382
|
const events = getCallEvents(session.id);
|
|
288
|
-
|
|
289
|
-
|
|
383
|
+
const connectedEvents = events.filter((e) => e.eventType === 'call_connected');
|
|
384
|
+
expect(connectedEvents.length).toBe(1);
|
|
385
|
+
const payload = JSON.parse(connectedEvents[0].payloadJson);
|
|
290
386
|
expect(payload.customParameters).toEqual({ taskId: 'task-1', priority: 'high' });
|
|
291
387
|
|
|
292
388
|
relay.destroy();
|
|
@@ -359,6 +455,9 @@ describe('relay-server', () => {
|
|
|
359
455
|
to: '+15552222222',
|
|
360
456
|
}));
|
|
361
457
|
|
|
458
|
+
// Let any async initial-greeting turn settle so we can compare only
|
|
459
|
+
// the effect of the partial prompt itself.
|
|
460
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
362
461
|
const messagesBeforePrompt = ws.sentMessages.length;
|
|
363
462
|
|
|
364
463
|
// Send a partial prompt (last=false)
|
|
@@ -467,6 +566,58 @@ describe('relay-server', () => {
|
|
|
467
566
|
relay.destroy();
|
|
468
567
|
});
|
|
469
568
|
|
|
569
|
+
test('verification failure remains failed if transport closes during goodbye delay', async () => {
|
|
570
|
+
ensureConversation('conv-relay-verify-race');
|
|
571
|
+
ensureConversation('conv-relay-verify-race-initiator');
|
|
572
|
+
const session = createCallSession({
|
|
573
|
+
conversationId: 'conv-relay-verify-race',
|
|
574
|
+
provider: 'twilio',
|
|
575
|
+
fromNumber: '+15551111111',
|
|
576
|
+
toNumber: '+15552222222',
|
|
577
|
+
initiatedFromConversationId: 'conv-relay-verify-race-initiator',
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
mockConfig.calls.verification.enabled = true;
|
|
581
|
+
mockConfig.calls.verification.maxAttempts = 1;
|
|
582
|
+
mockConfig.calls.verification.codeLength = 1;
|
|
583
|
+
|
|
584
|
+
const { relay } = createMockWs(session.id);
|
|
585
|
+
|
|
586
|
+
await relay.handleMessage(JSON.stringify({
|
|
587
|
+
type: 'setup',
|
|
588
|
+
callSid: 'CA_verify_race_123',
|
|
589
|
+
from: '+15551111111',
|
|
590
|
+
to: '+15552222222',
|
|
591
|
+
}));
|
|
592
|
+
|
|
593
|
+
const verificationCode = relay.getVerificationCode();
|
|
594
|
+
expect(verificationCode).not.toBeNull();
|
|
595
|
+
const wrongDigit = verificationCode === '0' ? '1' : '0';
|
|
596
|
+
|
|
597
|
+
await relay.handleMessage(JSON.stringify({
|
|
598
|
+
type: 'dtmf',
|
|
599
|
+
digit: wrongDigit,
|
|
600
|
+
}));
|
|
601
|
+
|
|
602
|
+
// Simulate the callee hanging up before the delayed endSession executes.
|
|
603
|
+
relay.handleTransportClosed(1000, 'callee hung up');
|
|
604
|
+
|
|
605
|
+
const updated = getCallSession(session.id);
|
|
606
|
+
expect(updated).not.toBeNull();
|
|
607
|
+
expect(updated!.status).toBe('failed');
|
|
608
|
+
expect(updated!.lastError).toContain('max attempts exceeded');
|
|
609
|
+
expect(getLatestAssistantText('conv-relay-verify-race')).toContain('**Call failed**');
|
|
610
|
+
|
|
611
|
+
// Let the delayed endSession callback flush to avoid timer bleed across tests.
|
|
612
|
+
await new Promise((resolve) => setTimeout(resolve, 2100));
|
|
613
|
+
|
|
614
|
+
const finalState = getCallSession(session.id);
|
|
615
|
+
expect(finalState).not.toBeNull();
|
|
616
|
+
expect(finalState!.status).toBe('failed');
|
|
617
|
+
|
|
618
|
+
relay.destroy();
|
|
619
|
+
});
|
|
620
|
+
|
|
470
621
|
// ── Error handling ──────────────────────────────────────────────
|
|
471
622
|
|
|
472
623
|
test('handleMessage: error message records call_failed event', async () => {
|
|
@@ -685,4 +836,592 @@ describe('relay-server', () => {
|
|
|
685
836
|
relay.destroy();
|
|
686
837
|
expect(() => relay.destroy()).not.toThrow();
|
|
687
838
|
});
|
|
839
|
+
|
|
840
|
+
// ── Inbound call setup ────────────────────────────────────────────
|
|
841
|
+
|
|
842
|
+
test('handleMessage: inbound call (no task) triggers greeting without verification', async () => {
|
|
843
|
+
ensureConversation('conv-relay-inbound-greet');
|
|
844
|
+
// Inbound sessions have no task and no initiatedFromConversationId
|
|
845
|
+
const session = createCallSession({
|
|
846
|
+
conversationId: 'conv-relay-inbound-greet',
|
|
847
|
+
provider: 'twilio',
|
|
848
|
+
fromNumber: '+15559999999',
|
|
849
|
+
toNumber: '+15551111111',
|
|
850
|
+
// no task — inbound call
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
// Enable verification to prove inbound calls skip it
|
|
854
|
+
mockConfig.calls.verification.enabled = true;
|
|
855
|
+
|
|
856
|
+
mockSendMessage.mockImplementation(createMockProviderResponse(['Hello, how can I help you today?']));
|
|
857
|
+
|
|
858
|
+
const { ws, relay } = createMockWs(session.id);
|
|
859
|
+
|
|
860
|
+
await relay.handleMessage(JSON.stringify({
|
|
861
|
+
type: 'setup',
|
|
862
|
+
callSid: 'CA_inbound_greet_123',
|
|
863
|
+
from: '+15559999999',
|
|
864
|
+
to: '+15551111111',
|
|
865
|
+
}));
|
|
866
|
+
|
|
867
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
868
|
+
|
|
869
|
+
// Should NOT have started verification (no verification code prompt)
|
|
870
|
+
expect(relay.getVerificationCode()).toBeNull();
|
|
871
|
+
|
|
872
|
+
// Should have generated a greeting via the orchestrator
|
|
873
|
+
const textMessages = ws.sentMessages
|
|
874
|
+
.map((raw) => JSON.parse(raw) as { type: string; token?: string; last?: boolean })
|
|
875
|
+
.filter((m) => m.type === 'text');
|
|
876
|
+
expect(textMessages.some((m) => (m.token ?? '').includes('how can I help'))).toBe(true);
|
|
877
|
+
expect(textMessages.some((m) => m.last === true)).toBe(true);
|
|
878
|
+
|
|
879
|
+
relay.destroy();
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
test('handleMessage: inbound call persists caller transcript to voice conversation', async () => {
|
|
883
|
+
ensureConversation('conv-relay-inbound-persist');
|
|
884
|
+
const session = createCallSession({
|
|
885
|
+
conversationId: 'conv-relay-inbound-persist',
|
|
886
|
+
provider: 'twilio',
|
|
887
|
+
fromNumber: '+15559999999',
|
|
888
|
+
toNumber: '+15551111111',
|
|
889
|
+
// no task — inbound call
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
mockSendMessage.mockImplementation(createMockProviderResponse(['Sure, let me help with that.']));
|
|
893
|
+
|
|
894
|
+
const { relay } = createMockWs(session.id);
|
|
895
|
+
|
|
896
|
+
await relay.handleMessage(JSON.stringify({
|
|
897
|
+
type: 'setup',
|
|
898
|
+
callSid: 'CA_inbound_persist_123',
|
|
899
|
+
from: '+15559999999',
|
|
900
|
+
to: '+15551111111',
|
|
901
|
+
}));
|
|
902
|
+
|
|
903
|
+
// Wait for initial greeting to settle
|
|
904
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
905
|
+
|
|
906
|
+
// Send a caller utterance
|
|
907
|
+
await relay.handleMessage(JSON.stringify({
|
|
908
|
+
type: 'prompt',
|
|
909
|
+
voicePrompt: 'I would like to schedule an appointment',
|
|
910
|
+
lang: 'en-US',
|
|
911
|
+
last: true,
|
|
912
|
+
}));
|
|
913
|
+
|
|
914
|
+
// Verify caller transcript is persisted to the voice conversation
|
|
915
|
+
const userMessages = getMessages('conv-relay-inbound-persist').filter((m) => m.role === 'user');
|
|
916
|
+
expect(userMessages.length).toBeGreaterThan(0);
|
|
917
|
+
const lastUserMsg = userMessages[userMessages.length - 1];
|
|
918
|
+
expect(lastUserMsg.content).toContain('schedule an appointment');
|
|
919
|
+
|
|
920
|
+
// Verify assistant response is also persisted
|
|
921
|
+
const assistantMessages = getMessages('conv-relay-inbound-persist').filter((m) => m.role === 'assistant');
|
|
922
|
+
expect(assistantMessages.length).toBeGreaterThan(0);
|
|
923
|
+
|
|
924
|
+
relay.destroy();
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
test('handleMessage: inbound call supports multi-turn conversation', async () => {
|
|
928
|
+
ensureConversation('conv-relay-inbound-multi');
|
|
929
|
+
const session = createCallSession({
|
|
930
|
+
conversationId: 'conv-relay-inbound-multi',
|
|
931
|
+
provider: 'twilio',
|
|
932
|
+
fromNumber: '+15559999999',
|
|
933
|
+
toNumber: '+15551111111',
|
|
934
|
+
// no task — inbound call
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
let turnCount = 0;
|
|
938
|
+
mockSendMessage.mockImplementation(async (_messages: unknown[], _tools: unknown[], _systemPrompt: unknown, options?: { onEvent?: (event: { type: string; text?: string }) => void }) => {
|
|
939
|
+
turnCount++;
|
|
940
|
+
let tokens: string[];
|
|
941
|
+
if (turnCount === 1) tokens = ['Hello, how can I help you?'];
|
|
942
|
+
else if (turnCount === 2) tokens = ['Sure, I can help with that.'];
|
|
943
|
+
else tokens = ['Your appointment is confirmed.'];
|
|
944
|
+
for (const token of tokens) {
|
|
945
|
+
options?.onEvent?.({ type: 'text_delta', text: token });
|
|
946
|
+
}
|
|
947
|
+
return {
|
|
948
|
+
content: [{ type: 'text', text: tokens.join('') }],
|
|
949
|
+
model: 'claude-sonnet-4-20250514',
|
|
950
|
+
usage: { inputTokens: 100, outputTokens: 50 },
|
|
951
|
+
stopReason: 'end_turn',
|
|
952
|
+
};
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
const { ws: _ws, relay } = createMockWs(session.id);
|
|
956
|
+
|
|
957
|
+
// Setup
|
|
958
|
+
await relay.handleMessage(JSON.stringify({
|
|
959
|
+
type: 'setup',
|
|
960
|
+
callSid: 'CA_inbound_multi_123',
|
|
961
|
+
from: '+15559999999',
|
|
962
|
+
to: '+15551111111',
|
|
963
|
+
}));
|
|
964
|
+
|
|
965
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
966
|
+
|
|
967
|
+
// First caller turn
|
|
968
|
+
await relay.handleMessage(JSON.stringify({
|
|
969
|
+
type: 'prompt',
|
|
970
|
+
voicePrompt: 'I need to schedule something',
|
|
971
|
+
lang: 'en-US',
|
|
972
|
+
last: true,
|
|
973
|
+
}));
|
|
974
|
+
|
|
975
|
+
// Second caller turn
|
|
976
|
+
await relay.handleMessage(JSON.stringify({
|
|
977
|
+
type: 'prompt',
|
|
978
|
+
voicePrompt: 'How about next Tuesday?',
|
|
979
|
+
lang: 'en-US',
|
|
980
|
+
last: true,
|
|
981
|
+
}));
|
|
982
|
+
|
|
983
|
+
// Verify conversation history has multiple turns
|
|
984
|
+
const history = relay.getConversationHistory();
|
|
985
|
+
expect(history.length).toBe(2);
|
|
986
|
+
expect(history[0].text).toBe('I need to schedule something');
|
|
987
|
+
expect(history[1].text).toBe('How about next Tuesday?');
|
|
988
|
+
|
|
989
|
+
// Verify LLM was called for each turn (greeting + 2 caller turns)
|
|
990
|
+
expect(turnCount).toBe(3);
|
|
991
|
+
|
|
992
|
+
// Verify events were recorded for both caller utterances
|
|
993
|
+
const events = getCallEvents(session.id).filter((e) => e.eventType === 'caller_spoke');
|
|
994
|
+
expect(events.length).toBe(2);
|
|
995
|
+
|
|
996
|
+
relay.destroy();
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
// ── Inbound voice guardian verification gate ────────────────────────
|
|
1000
|
+
|
|
1001
|
+
test('inbound guardian verification: DTMF code entry succeeds and starts normal call flow', async () => {
|
|
1002
|
+
ensureConversation('conv-guardian-dtmf-ok');
|
|
1003
|
+
const session = createCallSession({
|
|
1004
|
+
conversationId: 'conv-guardian-dtmf-ok',
|
|
1005
|
+
provider: 'twilio',
|
|
1006
|
+
fromNumber: '+15559999999',
|
|
1007
|
+
toNumber: '+15551111111',
|
|
1008
|
+
assistantId: 'test-assistant',
|
|
1009
|
+
// no task — inbound call
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
// Create a pending voice guardian challenge
|
|
1013
|
+
const challenge = createVerificationChallenge('test-assistant', 'voice');
|
|
1014
|
+
const secret = challenge.secret;
|
|
1015
|
+
|
|
1016
|
+
mockSendMessage.mockImplementation(createMockProviderResponse(['Hello, how can I help you?']));
|
|
1017
|
+
|
|
1018
|
+
const { ws, relay } = createMockWs(session.id);
|
|
1019
|
+
|
|
1020
|
+
await relay.handleMessage(JSON.stringify({
|
|
1021
|
+
type: 'setup',
|
|
1022
|
+
callSid: 'CA_guardian_dtmf_ok',
|
|
1023
|
+
from: '+15559999999',
|
|
1024
|
+
to: '+15551111111',
|
|
1025
|
+
}));
|
|
1026
|
+
|
|
1027
|
+
// Should be in verification-pending state
|
|
1028
|
+
expect(relay.isGuardianVerificationActive()).toBe(true);
|
|
1029
|
+
expect(relay.getConnectionState()).toBe('verification_pending');
|
|
1030
|
+
|
|
1031
|
+
// Verify TTS prompt was sent asking for code
|
|
1032
|
+
const setupMessages = ws.sentMessages
|
|
1033
|
+
.map((raw) => JSON.parse(raw) as { type: string; token?: string })
|
|
1034
|
+
.filter((m) => m.type === 'text');
|
|
1035
|
+
expect(setupMessages.some((m) => (m.token ?? '').includes('verification code'))).toBe(true);
|
|
1036
|
+
|
|
1037
|
+
// Enter the correct code via DTMF
|
|
1038
|
+
for (const digit of secret) {
|
|
1039
|
+
await relay.handleMessage(JSON.stringify({ type: 'dtmf', digit }));
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1043
|
+
|
|
1044
|
+
// Verification should have succeeded
|
|
1045
|
+
expect(relay.isGuardianVerificationActive()).toBe(false);
|
|
1046
|
+
expect(relay.getConnectionState()).toBe('connected');
|
|
1047
|
+
|
|
1048
|
+
// Guardian binding should have been created
|
|
1049
|
+
const binding = getGuardianBinding('test-assistant', 'voice');
|
|
1050
|
+
expect(binding).not.toBeNull();
|
|
1051
|
+
|
|
1052
|
+
// Orchestrator greeting should have fired
|
|
1053
|
+
const textMessages = ws.sentMessages
|
|
1054
|
+
.map((raw) => JSON.parse(raw) as { type: string; token?: string })
|
|
1055
|
+
.filter((m) => m.type === 'text');
|
|
1056
|
+
expect(textMessages.some((m) => (m.token ?? '').includes('how can I help'))).toBe(true);
|
|
1057
|
+
|
|
1058
|
+
// Verify events recorded
|
|
1059
|
+
const guardianEvents = getCallEvents(session.id);
|
|
1060
|
+
expect(guardianEvents.some((e) => e.eventType === 'guardian_voice_verification_started')).toBe(true);
|
|
1061
|
+
expect(guardianEvents.some((e) => e.eventType === 'guardian_voice_verification_succeeded')).toBe(true);
|
|
1062
|
+
|
|
1063
|
+
relay.destroy();
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
test('inbound guardian verification: speech-based code entry succeeds', async () => {
|
|
1067
|
+
ensureConversation('conv-guardian-speech-ok');
|
|
1068
|
+
const session = createCallSession({
|
|
1069
|
+
conversationId: 'conv-guardian-speech-ok',
|
|
1070
|
+
provider: 'twilio',
|
|
1071
|
+
fromNumber: '+15559999999',
|
|
1072
|
+
toNumber: '+15551111111',
|
|
1073
|
+
assistantId: 'test-assistant',
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
const challenge = createVerificationChallenge('test-assistant', 'voice');
|
|
1077
|
+
const secret = challenge.secret;
|
|
1078
|
+
|
|
1079
|
+
mockSendMessage.mockImplementation(createMockProviderResponse(['Hello, verified caller!']));
|
|
1080
|
+
|
|
1081
|
+
const { ws, relay } = createMockWs(session.id);
|
|
1082
|
+
|
|
1083
|
+
await relay.handleMessage(JSON.stringify({
|
|
1084
|
+
type: 'setup',
|
|
1085
|
+
callSid: 'CA_guardian_speech_ok',
|
|
1086
|
+
from: '+15559999999',
|
|
1087
|
+
to: '+15551111111',
|
|
1088
|
+
}));
|
|
1089
|
+
|
|
1090
|
+
expect(relay.isGuardianVerificationActive()).toBe(true);
|
|
1091
|
+
|
|
1092
|
+
// Speak the code as individual digit characters
|
|
1093
|
+
const spokenCode = secret.split('').join(' ');
|
|
1094
|
+
await relay.handleMessage(JSON.stringify({
|
|
1095
|
+
type: 'prompt',
|
|
1096
|
+
voicePrompt: spokenCode,
|
|
1097
|
+
lang: 'en-US',
|
|
1098
|
+
last: true,
|
|
1099
|
+
}));
|
|
1100
|
+
|
|
1101
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1102
|
+
|
|
1103
|
+
// Verification should have succeeded
|
|
1104
|
+
expect(relay.isGuardianVerificationActive()).toBe(false);
|
|
1105
|
+
expect(relay.getConnectionState()).toBe('connected');
|
|
1106
|
+
|
|
1107
|
+
// Binding created
|
|
1108
|
+
const binding = getGuardianBinding('test-assistant', 'voice');
|
|
1109
|
+
expect(binding).not.toBeNull();
|
|
1110
|
+
|
|
1111
|
+
// Greeting should have started
|
|
1112
|
+
const textMessages = ws.sentMessages
|
|
1113
|
+
.map((raw) => JSON.parse(raw) as { type: string; token?: string })
|
|
1114
|
+
.filter((m) => m.type === 'text');
|
|
1115
|
+
expect(textMessages.some((m) => (m.token ?? '').includes('verified caller'))).toBe(true);
|
|
1116
|
+
|
|
1117
|
+
relay.destroy();
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
test('inbound call: caller matching voice guardian binding is classified as guardian', async () => {
|
|
1121
|
+
ensureConversation('conv-guardian-role-match');
|
|
1122
|
+
const session = createCallSession({
|
|
1123
|
+
conversationId: 'conv-guardian-role-match',
|
|
1124
|
+
provider: 'twilio',
|
|
1125
|
+
fromNumber: '+15550001111',
|
|
1126
|
+
toNumber: '+15551111111',
|
|
1127
|
+
assistantId: 'test-assistant',
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
createBinding({
|
|
1131
|
+
assistantId: 'test-assistant',
|
|
1132
|
+
channel: 'voice',
|
|
1133
|
+
guardianExternalUserId: '+15550001111',
|
|
1134
|
+
guardianDeliveryChatId: '+15550001111',
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
mockSendMessage.mockImplementation(createMockProviderResponse(['Hello there.']));
|
|
1138
|
+
|
|
1139
|
+
const { relay } = createMockWs(session.id);
|
|
1140
|
+
|
|
1141
|
+
await relay.handleMessage(JSON.stringify({
|
|
1142
|
+
type: 'setup',
|
|
1143
|
+
callSid: 'CA_guardian_role_match',
|
|
1144
|
+
from: '+15550001111',
|
|
1145
|
+
to: '+15551111111',
|
|
1146
|
+
}));
|
|
1147
|
+
|
|
1148
|
+
const runtimeContext = (relay.getOrchestrator() as unknown as { guardianContext?: { sourceChannel?: string; actorRole?: string; guardianExternalUserId?: string } })?.guardianContext;
|
|
1149
|
+
expect(runtimeContext?.sourceChannel).toBe('voice');
|
|
1150
|
+
expect(runtimeContext?.actorRole).toBe('guardian');
|
|
1151
|
+
expect(runtimeContext?.guardianExternalUserId).toBe('+15550001111');
|
|
1152
|
+
|
|
1153
|
+
relay.destroy();
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
test('inbound call: caller not matching voice guardian binding is classified as non-guardian', async () => {
|
|
1157
|
+
ensureConversation('conv-guardian-role-mismatch');
|
|
1158
|
+
const session = createCallSession({
|
|
1159
|
+
conversationId: 'conv-guardian-role-mismatch',
|
|
1160
|
+
provider: 'twilio',
|
|
1161
|
+
fromNumber: '+15550002222',
|
|
1162
|
+
toNumber: '+15551111111',
|
|
1163
|
+
assistantId: 'test-assistant',
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
createBinding({
|
|
1167
|
+
assistantId: 'test-assistant',
|
|
1168
|
+
channel: 'voice',
|
|
1169
|
+
guardianExternalUserId: '+15550009999',
|
|
1170
|
+
guardianDeliveryChatId: '+15550009999',
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
mockSendMessage.mockImplementation(createMockProviderResponse(['Hello there.']));
|
|
1174
|
+
|
|
1175
|
+
const { relay } = createMockWs(session.id);
|
|
1176
|
+
|
|
1177
|
+
await relay.handleMessage(JSON.stringify({
|
|
1178
|
+
type: 'setup',
|
|
1179
|
+
callSid: 'CA_guardian_role_mismatch',
|
|
1180
|
+
from: '+15550002222',
|
|
1181
|
+
to: '+15551111111',
|
|
1182
|
+
}));
|
|
1183
|
+
|
|
1184
|
+
const runtimeContext = (relay.getOrchestrator() as unknown as {
|
|
1185
|
+
guardianContext?: {
|
|
1186
|
+
sourceChannel?: string;
|
|
1187
|
+
actorRole?: string;
|
|
1188
|
+
guardianExternalUserId?: string;
|
|
1189
|
+
requesterExternalUserId?: string;
|
|
1190
|
+
};
|
|
1191
|
+
})?.guardianContext;
|
|
1192
|
+
expect(runtimeContext?.sourceChannel).toBe('voice');
|
|
1193
|
+
expect(runtimeContext?.actorRole).toBe('non-guardian');
|
|
1194
|
+
expect(runtimeContext?.guardianExternalUserId).toBe('+15550009999');
|
|
1195
|
+
expect(runtimeContext?.requesterExternalUserId).toBe('+15550002222');
|
|
1196
|
+
|
|
1197
|
+
relay.destroy();
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
test('inbound guardian verification updates orchestrator context to guardian', async () => {
|
|
1201
|
+
ensureConversation('conv-guardian-context-upgrade');
|
|
1202
|
+
const session = createCallSession({
|
|
1203
|
+
conversationId: 'conv-guardian-context-upgrade',
|
|
1204
|
+
provider: 'twilio',
|
|
1205
|
+
fromNumber: '+15550003333',
|
|
1206
|
+
toNumber: '+15551111111',
|
|
1207
|
+
assistantId: 'test-assistant',
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
const challenge = createVerificationChallenge('test-assistant', 'voice');
|
|
1211
|
+
const spokenCode = challenge.secret.split('').join(' ');
|
|
1212
|
+
|
|
1213
|
+
const { relay } = createMockWs(session.id);
|
|
1214
|
+
|
|
1215
|
+
await relay.handleMessage(JSON.stringify({
|
|
1216
|
+
type: 'setup',
|
|
1217
|
+
callSid: 'CA_guardian_context_upgrade',
|
|
1218
|
+
from: session.fromNumber,
|
|
1219
|
+
to: session.toNumber,
|
|
1220
|
+
}));
|
|
1221
|
+
|
|
1222
|
+
const preVerify = (relay.getOrchestrator() as unknown as {
|
|
1223
|
+
guardianContext?: { actorRole?: string };
|
|
1224
|
+
})?.guardianContext;
|
|
1225
|
+
expect(preVerify?.actorRole).toBe('unverified_channel');
|
|
1226
|
+
|
|
1227
|
+
await relay.handleMessage(JSON.stringify({
|
|
1228
|
+
type: 'prompt',
|
|
1229
|
+
voicePrompt: spokenCode,
|
|
1230
|
+
lang: 'en-US',
|
|
1231
|
+
last: true,
|
|
1232
|
+
}));
|
|
1233
|
+
|
|
1234
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1235
|
+
|
|
1236
|
+
const postVerify = (relay.getOrchestrator() as unknown as {
|
|
1237
|
+
guardianContext?: { sourceChannel?: string; actorRole?: string; guardianExternalUserId?: string };
|
|
1238
|
+
})?.guardianContext;
|
|
1239
|
+
expect(postVerify?.sourceChannel).toBe('voice');
|
|
1240
|
+
expect(postVerify?.actorRole).toBe('guardian');
|
|
1241
|
+
expect(postVerify?.guardianExternalUserId).toBe(session.fromNumber);
|
|
1242
|
+
|
|
1243
|
+
relay.destroy();
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
test('inbound guardian verification: invalid code triggers retry prompt', async () => {
|
|
1247
|
+
ensureConversation('conv-guardian-retry');
|
|
1248
|
+
const session = createCallSession({
|
|
1249
|
+
conversationId: 'conv-guardian-retry',
|
|
1250
|
+
provider: 'twilio',
|
|
1251
|
+
fromNumber: '+15559999999',
|
|
1252
|
+
toNumber: '+15551111111',
|
|
1253
|
+
assistantId: 'test-assistant',
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
createVerificationChallenge('test-assistant', 'voice');
|
|
1257
|
+
|
|
1258
|
+
const { ws, relay } = createMockWs(session.id);
|
|
1259
|
+
|
|
1260
|
+
await relay.handleMessage(JSON.stringify({
|
|
1261
|
+
type: 'setup',
|
|
1262
|
+
callSid: 'CA_guardian_retry',
|
|
1263
|
+
from: '+15559999999',
|
|
1264
|
+
to: '+15551111111',
|
|
1265
|
+
}));
|
|
1266
|
+
|
|
1267
|
+
expect(relay.isGuardianVerificationActive()).toBe(true);
|
|
1268
|
+
|
|
1269
|
+
// Enter a wrong code via DTMF
|
|
1270
|
+
for (const digit of '000000') {
|
|
1271
|
+
await relay.handleMessage(JSON.stringify({ type: 'dtmf', digit }));
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// Should still be in verification-pending state (retry allowed)
|
|
1275
|
+
expect(relay.isGuardianVerificationActive()).toBe(true);
|
|
1276
|
+
expect(relay.getConnectionState()).toBe('verification_pending');
|
|
1277
|
+
|
|
1278
|
+
// Should have sent a retry prompt
|
|
1279
|
+
const textMessages = ws.sentMessages
|
|
1280
|
+
.map((raw) => JSON.parse(raw) as { type: string; token?: string })
|
|
1281
|
+
.filter((m) => m.type === 'text');
|
|
1282
|
+
expect(textMessages.some((m) => (m.token ?? '').includes('incorrect'))).toBe(true);
|
|
1283
|
+
|
|
1284
|
+
relay.destroy();
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
test('inbound guardian verification: max attempts exhaustion terminates call', async () => {
|
|
1288
|
+
ensureConversation('conv-guardian-max-attempts');
|
|
1289
|
+
const session = createCallSession({
|
|
1290
|
+
conversationId: 'conv-guardian-max-attempts',
|
|
1291
|
+
provider: 'twilio',
|
|
1292
|
+
fromNumber: '+15559999999',
|
|
1293
|
+
toNumber: '+15551111111',
|
|
1294
|
+
assistantId: 'test-assistant',
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
createVerificationChallenge('test-assistant', 'voice');
|
|
1298
|
+
|
|
1299
|
+
const { ws, relay } = createMockWs(session.id);
|
|
1300
|
+
|
|
1301
|
+
await relay.handleMessage(JSON.stringify({
|
|
1302
|
+
type: 'setup',
|
|
1303
|
+
callSid: 'CA_guardian_max_attempts',
|
|
1304
|
+
from: '+15559999999',
|
|
1305
|
+
to: '+15551111111',
|
|
1306
|
+
}));
|
|
1307
|
+
|
|
1308
|
+
expect(relay.isGuardianVerificationActive()).toBe(true);
|
|
1309
|
+
|
|
1310
|
+
// Enter wrong codes 3 times (max attempts = 3)
|
|
1311
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1312
|
+
for (const digit of '000000') {
|
|
1313
|
+
await relay.handleMessage(JSON.stringify({ type: 'dtmf', digit }));
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// Call should be marked as failed
|
|
1318
|
+
const updated = getCallSession(session.id);
|
|
1319
|
+
expect(updated).not.toBeNull();
|
|
1320
|
+
expect(updated!.status).toBe('failed');
|
|
1321
|
+
expect(updated!.lastError).toContain('Guardian voice verification failed');
|
|
1322
|
+
|
|
1323
|
+
// Should have sent goodbye message
|
|
1324
|
+
const textMessages = ws.sentMessages
|
|
1325
|
+
.map((raw) => JSON.parse(raw) as { type: string; token?: string })
|
|
1326
|
+
.filter((m) => m.type === 'text');
|
|
1327
|
+
expect(textMessages.some((m) => (m.token ?? '').includes('Verification failed. Goodbye.'))).toBe(true);
|
|
1328
|
+
|
|
1329
|
+
// Verify events
|
|
1330
|
+
const events = getCallEvents(session.id);
|
|
1331
|
+
expect(events.some((e) => e.eventType === 'guardian_voice_verification_failed')).toBe(true);
|
|
1332
|
+
|
|
1333
|
+
// Let the delayed endSession callback flush
|
|
1334
|
+
await new Promise((resolve) => setTimeout(resolve, 2100));
|
|
1335
|
+
|
|
1336
|
+
// Verify end message was sent
|
|
1337
|
+
const endMessages = ws.sentMessages
|
|
1338
|
+
.map((raw) => JSON.parse(raw) as { type: string })
|
|
1339
|
+
.filter((m) => m.type === 'end');
|
|
1340
|
+
expect(endMessages.length).toBe(1);
|
|
1341
|
+
|
|
1342
|
+
relay.destroy();
|
|
1343
|
+
});
|
|
1344
|
+
|
|
1345
|
+
test('inbound guardian verification: no pending challenge proceeds with normal flow', async () => {
|
|
1346
|
+
ensureConversation('conv-guardian-no-challenge');
|
|
1347
|
+
const session = createCallSession({
|
|
1348
|
+
conversationId: 'conv-guardian-no-challenge',
|
|
1349
|
+
provider: 'twilio',
|
|
1350
|
+
fromNumber: '+15559999999',
|
|
1351
|
+
toNumber: '+15551111111',
|
|
1352
|
+
assistantId: 'test-assistant',
|
|
1353
|
+
// no task — inbound call
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
// Do NOT create any pending challenge
|
|
1357
|
+
|
|
1358
|
+
mockSendMessage.mockImplementation(createMockProviderResponse(['Welcome to the line.']));
|
|
1359
|
+
|
|
1360
|
+
const { ws, relay } = createMockWs(session.id);
|
|
1361
|
+
|
|
1362
|
+
await relay.handleMessage(JSON.stringify({
|
|
1363
|
+
type: 'setup',
|
|
1364
|
+
callSid: 'CA_guardian_no_challenge',
|
|
1365
|
+
from: '+15559999999',
|
|
1366
|
+
to: '+15551111111',
|
|
1367
|
+
}));
|
|
1368
|
+
|
|
1369
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1370
|
+
|
|
1371
|
+
// Should NOT be in guardian verification state
|
|
1372
|
+
expect(relay.isGuardianVerificationActive()).toBe(false);
|
|
1373
|
+
expect(relay.getConnectionState()).toBe('connected');
|
|
1374
|
+
|
|
1375
|
+
// Should have started normal greeting
|
|
1376
|
+
const textMessages = ws.sentMessages
|
|
1377
|
+
.map((raw) => JSON.parse(raw) as { type: string; token?: string })
|
|
1378
|
+
.filter((m) => m.type === 'text');
|
|
1379
|
+
expect(textMessages.some((m) => (m.token ?? '').includes('Welcome to the line'))).toBe(true);
|
|
1380
|
+
|
|
1381
|
+
relay.destroy();
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
test('inbound guardian verification: speech with partial digits prompts for more', async () => {
|
|
1385
|
+
ensureConversation('conv-guardian-partial-speech');
|
|
1386
|
+
const session = createCallSession({
|
|
1387
|
+
conversationId: 'conv-guardian-partial-speech',
|
|
1388
|
+
provider: 'twilio',
|
|
1389
|
+
fromNumber: '+15559999999',
|
|
1390
|
+
toNumber: '+15551111111',
|
|
1391
|
+
assistantId: 'test-assistant',
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
createVerificationChallenge('test-assistant', 'voice');
|
|
1395
|
+
|
|
1396
|
+
const { ws, relay } = createMockWs(session.id);
|
|
1397
|
+
|
|
1398
|
+
await relay.handleMessage(JSON.stringify({
|
|
1399
|
+
type: 'setup',
|
|
1400
|
+
callSid: 'CA_guardian_partial_speech',
|
|
1401
|
+
from: '+15559999999',
|
|
1402
|
+
to: '+15551111111',
|
|
1403
|
+
}));
|
|
1404
|
+
|
|
1405
|
+
expect(relay.isGuardianVerificationActive()).toBe(true);
|
|
1406
|
+
|
|
1407
|
+
// Speak only 3 digits
|
|
1408
|
+
await relay.handleMessage(JSON.stringify({
|
|
1409
|
+
type: 'prompt',
|
|
1410
|
+
voicePrompt: 'one two three',
|
|
1411
|
+
lang: 'en-US',
|
|
1412
|
+
last: true,
|
|
1413
|
+
}));
|
|
1414
|
+
|
|
1415
|
+
// Should still be in verification state
|
|
1416
|
+
expect(relay.isGuardianVerificationActive()).toBe(true);
|
|
1417
|
+
|
|
1418
|
+
// Should have prompted for more digits
|
|
1419
|
+
const textMessages = ws.sentMessages
|
|
1420
|
+
.map((raw) => JSON.parse(raw) as { type: string; token?: string })
|
|
1421
|
+
.filter((m) => m.type === 'text');
|
|
1422
|
+
expect(textMessages.some((m) => (m.token ?? '').includes('3 digits'))).toBe(true);
|
|
1423
|
+
expect(textMessages.some((m) => (m.token ?? '').includes('all 6 digits'))).toBe(true);
|
|
1424
|
+
|
|
1425
|
+
relay.destroy();
|
|
1426
|
+
});
|
|
688
1427
|
});
|