@vellumai/assistant 0.10.4 → 0.10.5-dev.202607022329.d89bf2f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +94 -91
- package/Dockerfile +5 -16
- package/README.md +7 -10
- package/bun.lock +0 -4
- package/docs/architecture/security.md +20 -20
- package/docs/stt-provider-onboarding.md +4 -5
- package/docs/trusted-contact-access.md +7 -7
- package/eslint-rules/cli-no-daemon-internals.js +4 -0
- package/eslint.config.mjs +7 -1
- package/knip.json +0 -1
- package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +36 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/invite-contract.test.ts +233 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +40 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +20 -8
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +33 -0
- package/node_modules/@vellumai/gateway-client/src/invite-contract.ts +216 -0
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +91 -0
- package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +39 -3
- package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +0 -8
- package/node_modules/@vellumai/service-contracts/src/__tests__/package-boundary.test.ts +1 -5
- package/node_modules/@vellumai/service-contracts/src/twilio-ingress.ts +3 -14
- package/openapi.yaml +1073 -225
- package/package.json +1 -4
- package/scripts/test.sh +35 -13
- package/src/__tests__/access-request-seed-content-blocks.test.ts +60 -4
- package/src/__tests__/acp-session.test.ts +4 -1
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +0 -6
- package/src/__tests__/add-message-preassigned-id.test.ts +56 -0
- package/src/__tests__/agent-loop-set-system-prompt.test.ts +139 -0
- package/src/__tests__/app-conversation-ids-backfill.test.ts +3 -0
- package/src/__tests__/app-source-watcher.test.ts +11 -0
- package/src/__tests__/approval-interception-trust-gates.test.ts +148 -3
- package/src/__tests__/assistant-attachments.test.ts +62 -4
- package/src/__tests__/assistant-id-boundary-guard.test.ts +0 -1
- package/src/__tests__/attachments-store-heic-normalize.test.ts +230 -0
- package/src/__tests__/attachments-store.test.ts +3 -0
- package/src/__tests__/background-shell-bash.test.ts +34 -0
- package/src/__tests__/background-shell-host-bash.test.ts +12 -0
- package/src/__tests__/background-tool-registry.test.ts +97 -0
- package/src/__tests__/background-tool-routes.test.ts +72 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
- package/src/__tests__/btw-routes.test.ts +57 -3
- package/src/__tests__/call-controller.test.ts +474 -84
- package/src/__tests__/call-domain.test.ts +48 -1
- package/src/__tests__/call-pointer-messages.test.ts +57 -19
- package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +2 -6
- package/src/__tests__/call-routes-http.test.ts +15 -14
- package/src/__tests__/call-setup-flow-invite.test.ts +568 -0
- package/src/__tests__/call-setup-flow-midcall-trust.test.ts +231 -0
- package/src/__tests__/call-setup-flow-name-capture.test.ts +754 -0
- package/src/__tests__/call-setup-flow-verification.test.ts +925 -0
- package/src/__tests__/call-setup-flow.test.ts +311 -0
- package/src/__tests__/cancel-resolves-conversation-key.test.ts +0 -1
- package/src/__tests__/channel-guardian.test.ts +4 -7
- package/src/__tests__/channel-readiness-service.test.ts +0 -6
- package/src/__tests__/channel-readiness-slack-remote.test.ts +0 -4
- package/src/__tests__/channel-retry-sweep.test.ts +76 -64
- package/src/__tests__/channel-setup-panel-ack.test.ts +184 -0
- package/src/__tests__/clear-all-lexical.test.ts +90 -0
- package/src/__tests__/compactor-media-strip.test.ts +177 -0
- package/src/__tests__/config-loader-backfill.test.ts +5 -5
- package/src/__tests__/config-schema.test.ts +17 -0
- package/src/__tests__/contact-store-interaction-info.test.ts +27 -70
- package/src/__tests__/contacts-relay-reads.test.ts +188 -23
- package/src/__tests__/contacts-tools.test.ts +42 -1
- package/src/__tests__/contacts-write.test.ts +181 -0
- package/src/__tests__/context-search-conversations-source.test.ts +220 -81
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop.test.ts +39 -0
- package/src/__tests__/conversation-analysis-routes.test.ts +1 -1
- package/src/__tests__/conversation-app-control-instantiation.test.ts +2 -0
- package/src/__tests__/conversation-clear-safety.test.ts +0 -1
- package/src/__tests__/conversation-crud-enabled-plugins.test.ts +60 -0
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -1
- package/src/__tests__/conversation-disk-view.test.ts +1 -0
- package/src/__tests__/conversation-enabledplugins-route.test.ts +184 -0
- package/src/__tests__/conversation-error.test.ts +0 -18
- package/src/__tests__/conversation-fork-crud.test.ts +67 -0
- package/src/__tests__/conversation-history-web-search.test.ts +7 -137
- package/src/__tests__/conversation-initial-prompt.test.ts +110 -0
- package/src/__tests__/conversation-loop-db-lock-resilience.test.ts +191 -0
- package/src/__tests__/conversation-process-callsite.test.ts +37 -0
- package/src/__tests__/conversation-queue.test.ts +149 -0
- package/src/__tests__/conversation-routes-enabled-plugins.test.ts +392 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +84 -0
- package/src/__tests__/conversation-routes-hidden-queue.test.ts +407 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +36 -16
- package/src/__tests__/conversation-skill-tools.test.ts +73 -0
- package/src/__tests__/conversation-wipe.test.ts +20 -5
- package/src/__tests__/credential-prompt-route.test.ts +69 -0
- package/src/__tests__/credential-security-invariants.test.ts +1 -3
- package/src/__tests__/db-drop-messages-fts.test.ts +115 -0
- package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -62
- package/src/__tests__/edit-propagation.test.ts +56 -2
- package/src/__tests__/empty-state-greeting-cache.test.ts +9 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +15 -110
- package/src/__tests__/guardian-expiry-notifier.test.ts +1 -0
- package/src/__tests__/guardian-question-mode.test.ts +57 -0
- package/src/__tests__/guardian-verification-voice-binding.test.ts +1 -1
- package/src/__tests__/guardian-wait-controller.test.ts +619 -0
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +33 -2
- package/src/__tests__/heic-fixture.ts +55 -0
- package/src/__tests__/helpers/seed-contact-channel.ts +0 -2
- package/src/__tests__/http-conversation-lineage.test.ts +6 -1
- package/src/__tests__/image-conversion.test.ts +170 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +72 -341
- package/src/__tests__/introduction-card-resolver.test.ts +354 -0
- package/src/__tests__/invite-redeemed-ipc.test.ts +97 -0
- package/src/__tests__/invite-routes-http.test.ts +246 -420
- package/src/__tests__/job-handler-registration-guard.test.ts +104 -0
- package/src/__tests__/lexical-index-dual-write.test.ts +426 -0
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +2 -2
- package/src/__tests__/list-messages-attachments.test.ts +159 -0
- package/src/__tests__/list-messages-background-tool-completion.test.ts +141 -0
- package/src/__tests__/list-messages-queued.test.ts +25 -0
- package/src/__tests__/managed-store.test.ts +129 -1
- package/src/__tests__/media-stream-audio-transcode.test.ts +113 -0
- package/src/__tests__/media-stream-output.test.ts +350 -19
- package/src/__tests__/media-stream-server-integration.test.ts +943 -169
- package/src/__tests__/media-stream-stt-session.test.ts +498 -1
- package/src/__tests__/memory-jobs-worker-lanes.test.ts +2 -2
- package/src/__tests__/model-intents.test.ts +1 -1
- package/src/__tests__/mtime-cache.test.ts +19 -0
- package/src/__tests__/non-member-access-request.test.ts +349 -3
- package/src/__tests__/notification-decision-fallback.test.ts +104 -8
- package/src/__tests__/notification-decision-strategy.test.ts +57 -8
- package/src/__tests__/onboarding-template-contract.test.ts +27 -43
- package/src/__tests__/openai-provider.test.ts +33 -0
- package/src/__tests__/openai-responses-provider.test.ts +40 -0
- package/src/__tests__/persistence-layering-guard.test.ts +29 -20
- package/src/__tests__/persona-resolver.test.ts +56 -2
- package/src/__tests__/personal-memory-auth-bypass.test.ts +62 -0
- package/src/__tests__/phone.test.ts +1 -15
- package/src/__tests__/plugin-bootstrap.test.ts +25 -196
- package/src/__tests__/plugin-disabled-state.test.ts +33 -0
- package/src/__tests__/plugin-effective-enabled-set.test.ts +227 -0
- package/src/__tests__/plugin-event-hub-facade.test.ts +323 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +156 -42
- package/src/__tests__/plugin-injector-contribution.test.ts +4 -20
- package/src/__tests__/plugin-pipeline.test.ts +63 -1
- package/src/__tests__/plugin-route-contribution.test.ts +62 -115
- package/src/__tests__/plugin-tool-contribution.test.ts +0 -22
- package/src/__tests__/plugin-types.test.ts +0 -1
- package/src/__tests__/pointer-conversation-trust.test.ts +38 -6
- package/src/__tests__/pre-model-call-sanitize.test.ts +1 -1
- package/src/__tests__/prune-old-tool-invocations-job.test.ts +97 -0
- package/src/__tests__/public-ingress-urls.test.ts +0 -48
- package/src/__tests__/qdrant-collection-migration.test.ts +145 -2
- package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +0 -1
- package/src/__tests__/resolve-trust-class.test.ts +28 -10
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +137 -0
- package/src/__tests__/schedule-retry.test.ts +38 -14
- package/src/__tests__/schedule-routes-workflow-validation.test.ts +3 -3
- package/src/__tests__/schedule-routes.test.ts +14 -6
- package/src/__tests__/schedule-store.test.ts +3 -1
- package/src/__tests__/scheduler-disk-pressure.test.ts +17 -6
- package/src/__tests__/scheduler-recurrence.test.ts +57 -107
- package/src/__tests__/scheduler-reuse-conversation.test.ts +51 -46
- package/src/__tests__/scheduler-wake.test.ts +13 -23
- package/src/__tests__/scheduler-worker-standdown.test.ts +220 -0
- package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
- package/src/__tests__/shell-observability.test.ts +1 -0
- package/src/__tests__/skill-boundary-guard.test.ts +3 -4
- package/src/__tests__/skill-load-plugin-scope.test.ts +238 -0
- package/src/__tests__/skill-projection-feature-flag.test.ts +2 -0
- package/src/__tests__/skill-script-runner-host.test.ts +53 -0
- package/src/__tests__/skill-script-runner-sandbox.test.ts +5 -2
- package/src/__tests__/skill-tool-factory.test.ts +73 -1
- package/src/__tests__/slack-app-setup-skill-regression.test.ts +37 -14
- package/src/__tests__/slack-inbound-verification.test.ts +36 -0
- package/src/__tests__/slack-notification-approval-card.test.ts +89 -2
- package/src/__tests__/slack-reaction-canonical-approval.test.ts +51 -1
- package/src/__tests__/stt-hints.test.ts +2 -256
- package/src/__tests__/subagent-allowlist-validation.test.ts +47 -0
- package/src/__tests__/subagent-call-site-routing.test.ts +72 -0
- package/src/__tests__/subagent-fork-notifications.test.ts +14 -2
- package/src/__tests__/subagent-persistence.test.ts +122 -0
- package/src/__tests__/subagent-spawn-and-await.test.ts +1 -0
- package/src/__tests__/subagent-terminal-message.test.ts +99 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +78 -1
- package/src/__tests__/system-prompt.test.ts +17 -118
- package/src/__tests__/task-scheduler.test.ts +68 -89
- package/src/__tests__/telephony-credential-preflight.test.ts +312 -0
- package/src/__tests__/telephony-tts-guard.test.ts +538 -0
- package/src/__tests__/title-generate-hook.test.ts +16 -0
- package/src/__tests__/tool-approval-handler.test.ts +87 -1
- package/src/__tests__/tool-approval-seed-content-blocks.test.ts +45 -20
- package/src/__tests__/tool-grant-request-escalation.test.ts +39 -0
- package/src/__tests__/trust-context-guards.test.ts +21 -10
- package/src/__tests__/trusted-contact-multichannel.test.ts +4 -0
- package/src/__tests__/twilio-provider.test.ts +17 -17
- package/src/__tests__/twilio-routes-twiml.test.ts +6 -543
- package/src/__tests__/twilio-routes.test.ts +257 -387
- package/src/__tests__/twilio-validation.test.ts +1 -1
- package/src/__tests__/unified-turn-context-client-os.test.ts +45 -0
- package/src/__tests__/usage-cache-backfill-migration.test.ts +5 -0
- package/src/__tests__/voice-approval-resolver-relay.test.ts +150 -0
- package/src/__tests__/voice-ingress-preflight.test.ts +61 -0
- package/src/__tests__/voice-quality.test.ts +2 -2
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +1 -7
- package/src/__tests__/voice-session-bridge-processing-wait.test.ts +27 -0
- package/src/__tests__/voice-session-bridge.test.ts +1 -10
- package/src/__tests__/workspace-git-service.test.ts +1 -1
- package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +1 -0
- package/src/__tests__/workspace-migration-118-seed-now-md.test.ts +120 -0
- package/src/__tests__/workspace-migration-120-revise-onboarding-threads.test.ts +175 -0
- package/src/__tests__/workspace-migration-121-seed-default-user-guardrails.test.ts +118 -0
- package/src/__tests__/workspace-migration-122-relocate-default-user-boundary.test.ts +119 -0
- package/src/__tests__/workspace-migration-123-swap-quality-profile-to-fable.test.ts +191 -0
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +235 -241
- package/src/acp/__tests__/prepare-agent-env.test.ts +0 -97
- package/src/acp/__tests__/session-manager-persistence.test.ts +2 -0
- package/src/acp/__tests__/session-manager-resume.test.ts +8 -0
- package/src/acp/__tests__/session-manager-usage.test.ts +6 -0
- package/src/acp/__tests__/session-manager.test.ts +6 -0
- package/src/acp/agent-process.test.ts +116 -0
- package/src/acp/agent-process.ts +71 -0
- package/src/acp/auto-install.ts +12 -8
- package/src/acp/failure-error.test.ts +109 -0
- package/src/acp/failure-error.ts +111 -0
- package/src/acp/prepare-agent-env.ts +0 -12
- package/src/acp/resolve-agent.test.ts +6 -45
- package/src/acp/resolve-agent.ts +2 -4
- package/src/acp/session-manager.test.ts +312 -0
- package/src/acp/session-manager.ts +102 -46
- package/src/agent/ax-tree-compaction.test.ts +4 -1
- package/src/agent/image-optimize.ts +17 -127
- package/src/agent/loop.ts +45 -192
- package/src/api/events/background-tool-completed.test.ts +51 -0
- package/src/api/events/background-tool-completed.ts +29 -0
- package/src/api/events/background-tool-started.test.ts +36 -0
- package/src/api/events/background-tool-started.ts +28 -0
- package/src/api/events/conversation-error.ts +0 -1
- package/src/api/events/open-panel.ts +33 -0
- package/src/api/events/subagent-status-changed.ts +4 -1
- package/src/api/index.ts +19 -0
- package/src/api/responses/conversation-message.ts +34 -0
- package/src/approvals/guardian-decision-primitive.ts +74 -24
- package/src/approvals/guardian-request-resolvers.ts +450 -122
- package/src/apps/app-store.ts +1 -0
- package/src/apps/shared-app-links-store.ts +1 -0
- package/src/avatar/identity-avatar.ts +3 -7
- package/src/background-wake/wake-intent-hooks.test.ts +1 -1
- package/src/calls/__tests__/fish-audio-client.test.ts +73 -0
- package/src/calls/__tests__/gateway-invite-reader.test.ts +170 -0
- package/src/calls/__tests__/relay-setup-router.test.ts +126 -102
- package/src/calls/call-controller.ts +134 -48
- package/src/calls/call-domain.ts +18 -42
- package/src/calls/call-pointer-messages.ts +51 -67
- package/src/calls/call-setup-flow-types.ts +76 -0
- package/src/calls/call-setup-flow.ts +1743 -0
- package/src/calls/call-speech-output.ts +87 -23
- package/src/calls/call-store.ts +4 -0
- package/src/calls/call-transport.ts +22 -36
- package/src/calls/fish-audio-client.ts +3 -0
- package/src/calls/gateway-invite-reader.ts +98 -0
- package/src/calls/guardian-wait-controller.ts +451 -0
- package/src/calls/media-stream-audio-transcode.ts +82 -2
- package/src/calls/media-stream-output.ts +142 -36
- package/src/calls/media-stream-server.ts +297 -178
- package/src/calls/media-stream-stt-session.ts +393 -50
- package/src/calls/media-turn-detector.ts +4 -6
- package/src/calls/relay-access-wait.ts +3 -4
- package/src/calls/relay-setup-router.ts +22 -43
- package/src/calls/relay-verification.ts +17 -24
- package/src/calls/resolve-call-tts-provider.ts +96 -29
- package/src/calls/stt-hints.ts +24 -87
- package/src/calls/telephony-credential-preflight.ts +159 -0
- package/src/calls/telephony-tts-capability.ts +183 -0
- package/src/calls/tts-call-strategy.ts +18 -20
- package/src/calls/twilio-provider.ts +3 -2
- package/src/calls/twilio-routes.ts +47 -351
- package/src/calls/types.ts +5 -3
- package/src/calls/voice-ingress-preflight.ts +9 -0
- package/src/calls/voice-quality.ts +13 -18
- package/src/calls/voice-session-bridge.ts +25 -35
- package/src/channels/__tests__/types.test.ts +28 -0
- package/src/channels/channel-verification-sessions.ts +7 -2
- package/src/channels/config.ts +0 -42
- package/src/channels/slack-thread-store.ts +12 -0
- package/src/channels/types.ts +23 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -1
- package/src/cli/commands/__tests__/schedules.test.ts +83 -34
- package/src/cli/commands/__tests__/status.test.ts +42 -2
- package/src/cli/commands/contacts.ts +2 -2
- package/src/cli/commands/credentials.ts +17 -0
- package/src/cli/commands/monitoring.ts +191 -0
- package/src/cli/commands/schedules-worker.ts +175 -0
- package/src/cli/commands/schedules.ts +169 -43
- package/src/cli/commands/status.ts +12 -1
- package/src/cli/commands/tools.ts +5 -1
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +1 -0
- package/src/cli/lib/__tests__/search-plugins.test.ts +29 -1
- package/src/cli/lib/__tests__/toggle-plugin.test.ts +7 -0
- package/src/cli/lib/search-plugins.ts +6 -0
- package/src/cli/lib/toggle-plugin.ts +21 -2
- package/src/cli/program.ts +3 -1
- package/src/config/__tests__/deployment-context-defaults.test.ts +23 -0
- package/src/config/acp-defaults.test.ts +0 -10
- package/src/config/acp-defaults.ts +0 -6
- package/src/config/bundled-skills/acp/SKILL.md +13 -45
- package/src/config/bundled-skills/acp/TOOLS.json +3 -3
- package/src/config/bundled-skills/app-builder/SKILL.md +3 -3
- package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +9 -9
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +1 -1
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +1 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +9 -9
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +10 -7
- package/src/config/bundled-skills/skill-management/TOOLS.json +10 -0
- package/src/config/call-site-defaults.ts +0 -2
- package/src/config/feature-flag-registry.json +16 -8
- package/src/config/loader.ts +11 -0
- package/src/config/schema.ts +9 -1
- package/src/config/schemas/__tests__/memory-v2.test.ts +2 -2
- package/src/config/schemas/__tests__/memory-v3.test.ts +64 -0
- package/src/config/schemas/call-site-catalog.ts +0 -12
- package/src/config/schemas/calls.ts +8 -0
- package/src/config/schemas/elevenlabs.ts +1 -1
- package/src/config/schemas/llm.ts +0 -2
- package/src/config/schemas/memory-v2.ts +1 -1
- package/src/config/schemas/memory-v3.ts +82 -0
- package/src/config/schemas/monitoring.ts +61 -0
- package/src/config/schemas/schedules.ts +35 -0
- package/src/config/seed-inference-profiles.ts +2 -2
- package/src/config/skills-plugin-filter.test.ts +96 -0
- package/src/config/skills.ts +26 -0
- package/src/contacts/__tests__/member-write-relay.test.ts +156 -4
- package/src/contacts/canonical-guardian-store.ts +5 -0
- package/src/contacts/contact-store.ts +81 -35
- package/src/contacts/contacts-write.ts +50 -25
- package/src/contacts/member-write-relay.ts +115 -31
- package/src/contacts/types.ts +0 -8
- package/src/context/compactor.ts +29 -15
- package/src/context/outbound-sanitize.ts +202 -0
- package/src/daemon/__tests__/abort-null-controller.test.ts +167 -0
- package/src/daemon/__tests__/conversation-tool-setup-plugin-scope.test.ts +122 -0
- package/src/daemon/__tests__/embedding-reconcile.test.ts +99 -16
- package/src/daemon/__tests__/wake-conversation-ops-completion.test.ts +107 -0
- package/src/daemon/abort-watchdog.ts +7 -0
- package/src/daemon/app-source-watcher.ts +9 -0
- package/src/daemon/assistant-attachments.ts +42 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +256 -147
- package/src/daemon/conversation-agent-loop.ts +61 -68
- package/src/daemon/conversation-error.ts +1 -15
- package/src/daemon/conversation-history.ts +32 -236
- package/src/daemon/conversation-initial-prompt.ts +77 -0
- package/src/daemon/conversation-lifecycle.ts +25 -1
- package/src/daemon/conversation-messaging.ts +2 -2
- package/src/daemon/conversation-plugin-scope.ts +38 -0
- package/src/daemon/conversation-process.ts +50 -14
- package/src/daemon/conversation-runtime-assembly.ts +36 -6
- package/src/daemon/conversation-skill-tools.ts +19 -3
- package/src/daemon/conversation-store.ts +3 -3
- package/src/daemon/conversation-surfaces.ts +171 -0
- package/src/daemon/conversation-tool-setup.test.ts +95 -0
- package/src/daemon/conversation-tool-setup.ts +116 -1
- package/src/daemon/conversation-turn-finalize.ts +211 -0
- package/src/daemon/conversation.ts +92 -1
- package/src/daemon/disk-pressure-policy.ts +2 -1
- package/src/daemon/event-loop-watchdog.ts +22 -6
- package/src/daemon/external-plugins-bootstrap.ts +47 -134
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +1 -1
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +26 -14
- package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +27 -14
- package/src/daemon/handlers/__tests__/config-channels.test.ts +0 -5
- package/src/daemon/handlers/config-a2a.ts +7 -14
- package/src/daemon/handlers/config-channels.ts +2 -10
- package/src/daemon/handlers/conversation-history.ts +3 -1
- package/src/daemon/handlers/conversations.ts +0 -52
- package/src/daemon/handlers/shared.ts +4 -24
- package/src/daemon/handlers/skills.ts +4 -1
- package/src/daemon/host-cu-proxy.ts +1 -1
- package/src/daemon/lifecycle.ts +59 -472
- package/src/daemon/message-protocol.ts +3 -3
- package/src/daemon/message-provenance.ts +12 -14
- package/src/daemon/message-types/background-tools.ts +28 -0
- package/src/daemon/message-types/contacts.ts +2 -10
- package/src/daemon/message-types/conversations.ts +10 -0
- package/src/daemon/message-types/integrations.ts +2 -0
- package/src/daemon/message-types/surfaces.ts +1 -0
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/pointer-conversation-trust.ts +23 -9
- package/src/daemon/pointer-turn-runner.ts +174 -0
- package/src/daemon/providers-setup.ts +16 -0
- package/src/daemon/shutdown-handlers.ts +139 -47
- package/src/daemon/skill-memory-refresh.ts +2 -2
- package/src/daemon/sqlite-corruption-watchdog.test.ts +217 -0
- package/src/daemon/sqlite-corruption-watchdog.ts +155 -0
- package/src/daemon/tool-setup-types.ts +8 -0
- package/src/daemon/trust-context.ts +21 -6
- package/src/daemon/turn-latency-tracker.test.ts +93 -0
- package/src/daemon/turn-latency-tracker.ts +38 -0
- package/src/daemon/wake-conversation-ops.ts +10 -8
- package/src/documents/document-comments-store.test.ts +7 -1
- package/src/documents/document-comments-store.ts +8 -0
- package/src/documents/document-store.ts +15 -0
- package/src/export/__tests__/transcript-formatter.test.ts +11 -0
- package/src/export/transcript-formatter.ts +49 -67
- package/src/home/__tests__/feed-writer.test.ts +247 -1
- package/src/home/conversation-starter-checkpoints.ts +1 -0
- package/src/home/feed-writer.ts +61 -0
- package/src/home/job-handlers/conversation-starters.ts +1 -0
- package/src/hooks/hook-loader.ts +12 -0
- package/src/hooks/registry.ts +32 -4
- package/src/inbound/public-ingress-urls.ts +0 -17
- package/src/ipc/__tests__/contacts-info-ipc.test.ts +285 -0
- package/src/ipc/assistant-server.ts +28 -7
- package/src/ipc/routes/__tests__/contacts-mirror-apply-atomicity.test.ts +62 -0
- package/src/ipc/routes/__tests__/contacts-mirror-apply.test.ts +272 -0
- package/src/ipc/routes/__tests__/contacts-mirror-ipc-routes.test.ts +322 -0
- package/src/ipc/routes/__tests__/invite-ipc-routes.test.ts +60 -4
- package/src/ipc/routes/contacts-info-ipc-routes.ts +239 -0
- package/src/ipc/routes/contacts-mirror-ipc-routes.ts +202 -0
- package/src/ipc/routes/db-proxy.ts +5 -2
- package/src/ipc/routes/invite-ipc-routes.ts +53 -39
- package/src/jobs/worker.ts +9 -2
- package/src/live-voice/__tests__/live-voice-archive.test.ts +10 -1
- package/src/live-voice/live-voice-archive.ts +5 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +22 -6
- package/src/messaging/providers/channel-transport.ts +6 -0
- package/src/messaging/providers/index.ts +3 -0
- package/src/messaging/providers/slack/api.test.ts +49 -1
- package/src/messaging/providers/slack/api.ts +110 -0
- package/src/messaging/providers/slack/send.test.ts +85 -0
- package/src/messaging/providers/slack/send.ts +100 -28
- package/src/messaging/providers/slack/transport.ts +7 -13
- package/src/messaging/providers/whatsapp/send.ts +29 -9
- package/src/monitoring/__tests__/control.test.ts +235 -0
- package/src/monitoring/__tests__/sample-ring-buffer.test.ts +96 -0
- package/src/monitoring/control.ts +105 -0
- package/src/monitoring/resource-sampler.ts +258 -0
- package/src/monitoring/sample-ring-buffer.ts +108 -0
- package/src/monitoring/worker.ts +94 -0
- package/src/notifications/README.md +1 -1
- package/src/notifications/access-request-copy.ts +94 -23
- package/src/notifications/adapters/slack.ts +89 -21
- package/src/notifications/approval-card-builder.ts +33 -18
- package/src/notifications/approval-card-data.ts +48 -15
- package/src/notifications/broadcaster.ts +25 -16
- package/src/notifications/decision-engine.ts +131 -32
- package/src/notifications/guardian-question-mode.ts +58 -10
- package/src/permissions/approval-policy.ts +8 -1
- package/src/permissions/checker.ts +2 -0
- package/src/permissions/gateway-threshold-reader.ts +7 -6
- package/src/permissions/ipc-risk-types.ts +1 -0
- package/src/permissions/secret-prompter.ts +11 -3
- package/src/permissions/types.ts +2 -2
- package/src/persistence/__tests__/conversation-queries-search.test.ts +397 -0
- package/src/persistence/__tests__/conversation-search-lexical.test.ts +103 -0
- package/src/persistence/__tests__/jobs-worker-memory-disabled.test.ts +95 -0
- package/src/persistence/__tests__/slow-query-log.test.ts +266 -0
- package/src/persistence/__tests__/slow-sync-log.test.ts +155 -0
- package/src/persistence/a2a-invite-store.ts +205 -0
- package/src/persistence/attachments-store.ts +94 -10
- package/src/persistence/checkpoints.ts +45 -0
- package/src/persistence/conversation-crud.ts +453 -260
- package/src/persistence/conversation-display-order-migration.ts +8 -2
- package/src/persistence/conversation-group-migration.test.ts +8 -0
- package/src/persistence/conversation-group-migration.ts +9 -0
- package/src/persistence/conversation-queries.ts +244 -125
- package/src/persistence/conversation-row-batch-delete.ts +9 -5
- package/src/persistence/conversation-search-lexical.ts +49 -0
- package/src/persistence/db-async-query.ts +42 -0
- package/src/persistence/db-connection.ts +13 -0
- package/src/persistence/delivery-channels.ts +0 -82
- package/src/persistence/delivery-crud.ts +27 -6
- package/src/persistence/embeddings/__tests__/embedding-identity.test.ts +18 -5
- package/src/persistence/embeddings/__tests__/messages-lexical-index.test.ts +334 -0
- package/src/persistence/embeddings/embedding-identity.ts +17 -5
- package/src/persistence/embeddings/embedding-runtime-manager.ts +31 -0
- package/src/persistence/embeddings/messages-lexical-index.ts +396 -0
- package/src/persistence/embeddings/qdrant-client.ts +81 -5
- package/src/persistence/group-crud.ts +13 -1
- package/src/persistence/job-handlers/__tests__/message-lexical-backfill.test.ts +506 -0
- package/src/persistence/job-handlers/__tests__/message-lexical-enqueue.test.ts +164 -0
- package/src/persistence/job-handlers/__tests__/message-lexical.test.ts +259 -0
- package/src/persistence/job-handlers/cleanup.ts +57 -9
- package/src/persistence/job-handlers/message-lexical-backfill.ts +187 -0
- package/src/persistence/job-handlers/message-lexical.ts +251 -0
- package/src/persistence/jobs-store.ts +102 -8
- package/src/persistence/jobs-worker.ts +68 -28
- package/src/persistence/llm-request-log-store.ts +35 -19
- package/src/persistence/llm-usage-store.ts +11 -0
- package/src/persistence/memory-lifecycle-hooks.test.ts +65 -0
- package/src/persistence/memory-lifecycle-hooks.ts +55 -1
- package/src/persistence/migrations/028-call-session-mode.ts +1 -1
- package/src/persistence/migrations/116-messages-fts.ts +8 -1
- package/src/persistence/migrations/311-create-subagents-table.ts +50 -0
- package/src/persistence/migrations/312-drop-inbox-conversation-state-table.ts +15 -0
- package/src/persistence/migrations/313-drop-messages-fts.ts +17 -0
- package/src/persistence/migrations/314-add-conversation-enabled-plugins.ts +25 -0
- package/src/persistence/migrations/315-create-a2a-invites.ts +58 -0
- package/src/persistence/migrations/316-drop-contact-channels-invite-id.ts +28 -0
- package/src/persistence/migrations/317-canonical-guardian-requester-signals.ts +24 -0
- package/src/persistence/migrations/318-drop-contact-channel-telemetry.ts +32 -0
- package/src/persistence/migrations/__tests__/315-create-a2a-invites.test.ts +161 -0
- package/src/persistence/migrations/__tests__/316-drop-contact-channels-invite-id.test.ts +156 -0
- package/src/persistence/migrations/__tests__/318-drop-contact-channel-telemetry.test.ts +134 -0
- package/src/persistence/migrations/__tests__/migration-prefix-guard.test.ts +99 -0
- package/src/persistence/raw-query.ts +50 -11
- package/src/persistence/schema/a2a.ts +14 -0
- package/src/persistence/schema/contacts.ts +0 -62
- package/src/persistence/schema/conversation-groups.ts +12 -0
- package/src/persistence/schema/conversations.ts +2 -0
- package/src/persistence/schema/documents.ts +65 -0
- package/src/persistence/schema/guardian.ts +4 -0
- package/src/persistence/schema/index.ts +4 -0
- package/src/persistence/schema/memory-injection.ts +62 -0
- package/src/persistence/schema/workflows.ts +55 -0
- package/src/persistence/slow-query-log.ts +437 -0
- package/src/persistence/slow-sync-log.ts +204 -0
- package/src/persistence/steps.ts +16 -0
- package/src/persistence/subagent-store.ts +125 -0
- package/src/persistence/worker-control.ts +45 -238
- package/src/plugin-api/event-hub-facade.ts +172 -0
- package/src/plugin-api/index.ts +9 -1
- package/src/plugin-api/types.ts +40 -9
- package/src/plugins/defaults/default-plugin-names.ts +60 -0
- package/src/plugins/defaults/index.ts +19 -29
- package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +46 -67
- package/src/plugins/defaults/memory/__tests__/db-async-query.test.ts +36 -1
- package/src/plugins/defaults/memory/__tests__/find-most-recent-retrospective-for.test.ts +2 -4
- package/src/plugins/defaults/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +2 -2
- package/src/plugins/defaults/memory/__tests__/jobs-worker-v2-schedule.test.ts +9 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +7 -4
- package/src/plugins/defaults/memory/config.ts +11 -0
- package/src/plugins/defaults/memory/context-search/agent-runner.ts +1 -1
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +124 -113
- package/src/plugins/defaults/memory/embeddings.ts +77 -0
- package/src/plugins/defaults/memory/find-most-recent-retrospective-for.ts +66 -0
- package/src/plugins/defaults/memory/frontmatter.ts +50 -0
- package/src/plugins/defaults/memory/graph/bootstrap.test.ts +36 -10
- package/src/plugins/defaults/memory/graph/bootstrap.ts +6 -5
- package/src/plugins/defaults/memory/graph/consolidation.ts +3 -5
- package/src/plugins/defaults/memory/graph/extraction.ts +3 -6
- package/src/plugins/defaults/memory/graph/graph-search.ts +12 -23
- package/src/plugins/defaults/memory/graph/narrative.ts +3 -5
- package/src/plugins/defaults/memory/graph/pattern-scan.ts +3 -5
- package/src/plugins/defaults/memory/graph/retriever.test.ts +1 -6
- package/src/plugins/defaults/memory/graph/retriever.ts +5 -10
- package/src/plugins/defaults/memory/graph/tools.ts +1 -1
- package/src/plugins/defaults/memory/hooks/init.ts +31 -5
- package/src/plugins/defaults/memory/indexer.ts +2 -3
- package/src/plugins/defaults/memory/injectors.ts +2 -2
- package/src/plugins/defaults/memory/job-handler-registration.ts +96 -0
- package/src/plugins/defaults/memory/job-handlers/backfill.ts +4 -26
- package/src/plugins/defaults/memory/job-handlers/embedding.test.ts +6 -19
- package/src/plugins/defaults/memory/job-handlers/embedding.ts +9 -22
- package/src/plugins/defaults/memory/job-handlers/index-maintenance.ts +2 -3
- package/src/plugins/defaults/memory/job-handlers.ts +13 -12
- package/src/plugins/defaults/memory/jobs/embed-concept-page.ts +1 -1
- package/src/plugins/defaults/memory/llm-helpers.ts +76 -0
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +8 -5
- package/src/plugins/defaults/memory/memory-retrospective-startup-cleanup.ts +2 -2
- package/src/plugins/defaults/memory/memory-v2-concept-frequency.ts +2 -0
- package/src/plugins/defaults/memory/persistence-hooks.ts +46 -1
- package/src/plugins/defaults/memory/pkb/pkb-index.ts +1 -5
- package/src/plugins/defaults/memory/pkb/pkb-search.ts +2 -2
- package/src/plugins/defaults/memory/routes/__tests__/memory-v2-simulate-route.test.ts +4 -6
- package/src/plugins/defaults/memory/routes/memory-item-routes.ts +1 -1
- package/src/plugins/defaults/memory/search/semantic.ts +2 -2
- package/src/plugins/defaults/memory/startup.ts +244 -0
- package/src/plugins/defaults/memory/task-memory-cleanup.ts +8 -0
- package/src/{daemon → plugins/defaults/memory/v2}/__tests__/memory-v2-startup.test.ts +6 -6
- package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +2 -13
- package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +4 -5
- package/src/plugins/defaults/memory/v2/__tests__/sweep-job.test.ts +1 -8
- package/src/plugins/defaults/memory/v2/activation.ts +2 -4
- package/src/plugins/defaults/memory/v2/cli-command-store.ts +2 -4
- package/src/plugins/defaults/memory/v2/frontmatter-sweep.ts +1 -1
- package/src/{daemon → plugins/defaults/memory/v2}/memory-v2-startup.test.ts +8 -8
- package/src/{daemon → plugins/defaults/memory/v2}/memory-v2-startup.ts +20 -22
- package/src/plugins/defaults/memory/v2/migration.ts +2 -5
- package/src/plugins/defaults/memory/v2/page-store.ts +1 -1
- package/src/plugins/defaults/memory/v2/qdrant.ts +2 -3
- package/src/plugins/defaults/memory/v2/router.ts +5 -5
- package/src/plugins/defaults/memory/v2/sim.ts +2 -4
- package/src/plugins/defaults/memory/v2/skill-store.ts +2 -4
- package/src/plugins/defaults/memory/v2/sweep-job.ts +6 -6
- package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +11 -9
- package/src/plugins/defaults/memory/v3/__tests__/dense.test.ts +107 -2
- package/src/plugins/defaults/memory/v3/__tests__/gate-flag.test.ts +69 -0
- package/src/plugins/defaults/memory/v3/__tests__/gate.test.ts +181 -0
- package/src/plugins/defaults/memory/v3/__tests__/live-integration.test.ts +7 -3
- package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +345 -8
- package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -4
- package/src/plugins/defaults/memory/v3/__tests__/section-needle.test.ts +64 -0
- package/src/plugins/defaults/memory/v3/__tests__/shadow-integration.test.ts +18 -13
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +62 -1
- package/src/plugins/defaults/memory/v3/card.ts +1 -4
- package/src/plugins/defaults/memory/v3/dense.ts +60 -25
- package/src/plugins/defaults/memory/v3/edge.ts +1 -1
- package/src/plugins/defaults/memory/v3/gate-flag.ts +20 -0
- package/src/plugins/defaults/memory/v3/gate.ts +158 -0
- package/src/plugins/defaults/memory/v3/maintain-job.ts +2 -4
- package/src/plugins/defaults/memory/v3/orchestrate.ts +186 -25
- package/src/plugins/defaults/memory/v3/pool-select.test.ts +7 -9
- package/src/plugins/defaults/memory/v3/pool-select.ts +6 -6
- package/src/plugins/defaults/memory/v3/prune.ts +2 -2
- package/src/plugins/defaults/memory/v3/section-dense-store.ts +8 -11
- package/src/plugins/defaults/memory/v3/section-needle.ts +27 -7
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +10 -0
- package/src/plugins/defaults/memory/v3-eval/eval-packets.ts +1 -4
- package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +5 -0
- package/src/plugins/defaults/turn-context/injectors.ts +1 -0
- package/src/plugins/defaults/turn-context/unified-turn-context.ts +12 -0
- package/src/plugins/injector-registry.ts +13 -2
- package/src/plugins/mtime-cache.ts +36 -47
- package/src/plugins/pipeline.ts +15 -1
- package/src/plugins/types.ts +14 -23
- package/src/prompts/__tests__/system-prompt.test.ts +170 -32
- package/src/prompts/persona-resolver.ts +13 -9
- package/src/prompts/system-prompt.ts +30 -9
- package/src/prompts/templates/BOOTSTRAP.md +16 -119
- package/src/prompts/templates/SOUL.md +43 -13
- package/src/prompts/templates/system-sections.ts +32 -27
- package/src/prompts/templates/users/default.md +19 -0
- package/src/providers/__tests__/placeholder-sentinels.test.ts +56 -0
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +96 -1
- package/src/providers/model-intents.ts +2 -2
- package/src/providers/openai/chat-completions-provider.ts +14 -1
- package/src/providers/openai/responses-provider.ts +18 -0
- package/src/providers/placeholder-sentinels.ts +18 -8
- package/src/providers/retry.ts +1 -0
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +0 -64
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +92 -79
- package/src/providers/speech-to-text/deepgram-realtime.test.ts +98 -0
- package/src/providers/speech-to-text/deepgram-realtime.ts +62 -3
- package/src/providers/speech-to-text/provider-catalog.ts +0 -89
- package/src/providers/speech-to-text/resolve.ts +55 -0
- package/src/providers/tool-progress-events.test.ts +50 -0
- package/src/providers/tool-progress-events.ts +99 -0
- package/src/providers/types.ts +1 -1
- package/src/providers/vellum-model-routing.test.ts +51 -0
- package/src/providers/vellum-model-routing.ts +84 -0
- package/src/runtime/AGENTS.md +5 -4
- package/src/runtime/__tests__/agent-wake.test.ts +158 -1
- package/src/runtime/__tests__/channel-verification-service.test.ts +2 -2
- package/src/runtime/__tests__/trust-verdict-consumer.test.ts +37 -8
- package/src/runtime/access-request-helper.ts +154 -3
- package/src/runtime/actor-trust-resolver.ts +7 -21
- package/src/runtime/agent-wake.ts +41 -1
- package/src/runtime/assistant-event.ts +86 -12
- package/src/runtime/auth/__tests__/route-policy.test.ts +31 -0
- package/src/runtime/channel-approval-types.ts +52 -2
- package/src/runtime/channel-approvals.ts +23 -8
- package/src/runtime/channel-invite-transport.ts +2 -2
- package/src/runtime/channel-invite-transports/voice.ts +1 -1
- package/src/runtime/channel-readiness-service.ts +3 -5
- package/src/runtime/channel-retry-sweep.ts +18 -24
- package/src/runtime/channel-verification-service.ts +9 -11
- package/src/runtime/finalize-event-delivery.ts +30 -16
- package/src/runtime/guardian-action-service.ts +10 -11
- package/src/runtime/guardian-reply-router.ts +122 -21
- package/src/runtime/http-server.ts +44 -101
- package/src/runtime/introduction-policy.test.ts +214 -0
- package/src/runtime/introduction-policy.ts +284 -0
- package/src/runtime/invite-service.ts +104 -361
- package/src/runtime/local-actor-identity.test.ts +38 -0
- package/src/runtime/local-actor-identity.ts +14 -0
- package/src/runtime/middleware/__tests__/rate-limiter.test.ts +22 -0
- package/src/runtime/middleware/rate-limiter.ts +30 -0
- package/src/runtime/middleware/twilio-validation.ts +5 -7
- package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +28 -0
- package/src/runtime/migrations/origin-mode.ts +6 -6
- package/src/runtime/pre-first-message-gate.ts +1 -0
- package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +6 -35
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +24 -3
- package/src/runtime/routes/__tests__/contact-routes.test.ts +80 -15
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +7 -1
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +0 -3
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +142 -1
- package/src/runtime/routes/__tests__/invite-relay-routes.test.ts +119 -30
- package/src/runtime/routes/__tests__/migration-import-allowed-hosts-env.test.ts +70 -0
- package/src/runtime/routes/__tests__/monitoring-routes.test.ts +194 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +755 -29
- package/src/runtime/routes/__tests__/retrospective-routes.test.ts +4 -0
- package/src/runtime/routes/__tests__/schedule-routes-create.test.ts +70 -0
- package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +161 -0
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/surface-content-routes.test.ts +1 -1
- package/src/runtime/routes/acp-routes.ts +15 -3
- package/src/runtime/routes/app-management-routes.ts +4 -7
- package/src/runtime/routes/approval-routes.ts +4 -0
- package/src/runtime/routes/attachment-routes.ts +54 -3
- package/src/runtime/routes/avatar-routes.ts +0 -4
- package/src/runtime/routes/background-tool-routes.ts +34 -5
- package/src/runtime/routes/btw-routes.ts +32 -4
- package/src/runtime/routes/channel-route-shared.ts +18 -14
- package/src/runtime/routes/channel-verification-routes.ts +0 -3
- package/src/runtime/routes/contact-routes.ts +298 -117
- package/src/runtime/routes/conversation-list-routes.ts +6 -0
- package/src/runtime/routes/conversation-management-routes.ts +74 -37
- package/src/runtime/routes/conversation-query-routes.ts +5 -2
- package/src/runtime/routes/conversation-routes.ts +222 -82
- package/src/runtime/routes/conversations-import-routes.ts +8 -1
- package/src/runtime/routes/credential-prompt-routes.ts +20 -5
- package/src/runtime/routes/debug-routes.ts +1 -0
- package/src/runtime/routes/documents-routes.ts +5 -2
- package/src/runtime/routes/empty-state-greeting-cache.ts +37 -10
- package/src/runtime/routes/global-search-routes.ts +2 -1
- package/src/runtime/routes/guardian-approval-interception.ts +51 -0
- package/src/runtime/routes/home-feed-routes.ts +60 -1
- package/src/runtime/routes/identity-routes.ts +4 -87
- package/src/runtime/routes/inbound-message-handler.ts +82 -41
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +430 -57
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +302 -572
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +76 -270
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +21 -116
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +9 -1
- package/src/runtime/routes/inbound-stages/escalation-intercept.test.ts +158 -0
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +22 -1
- package/src/runtime/routes/index.ts +6 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +22 -16
- package/src/runtime/routes/internal-twilio-routes.ts +0 -15
- package/src/runtime/routes/messages-lexical-routes.ts +75 -0
- package/src/runtime/routes/migration-routes.ts +69 -6
- package/src/runtime/routes/monitoring-routes.ts +237 -0
- package/src/runtime/routes/plugins-routes.ts +329 -8
- package/src/runtime/routes/schedule-routes.ts +49 -8
- package/src/runtime/routes/schedule-worker-routes.ts +210 -0
- package/src/runtime/routes/secret-routes.ts +1 -1
- package/src/runtime/routes/surface-conversation-resolver.ts +1 -0
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +54 -9
- package/src/runtime/services/__tests__/auto-analysis-enqueue.test.ts +30 -6
- package/src/runtime/services/__tests__/conversation-analyze-job.test.ts +43 -8
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +20 -0
- package/src/runtime/services/analyze-conversation.ts +14 -4
- package/src/runtime/services/conversation-serializer.ts +5 -0
- package/src/runtime/slack-no-response.ts +47 -0
- package/src/runtime/slack-reply-session.test.ts +633 -0
- package/src/runtime/slack-reply-session.ts +382 -0
- package/src/runtime/slack-task-progress.ts +95 -0
- package/src/runtime/sync/resource-sync-events.ts +25 -0
- package/src/runtime/tool-grant-request-helper.ts +17 -1
- package/src/runtime/trust-class.test.ts +141 -0
- package/src/runtime/trust-class.ts +121 -0
- package/src/runtime/trust-verdict-consumer.ts +7 -6
- package/src/schedule/__tests__/run-script.test.ts +21 -0
- package/src/schedule/__tests__/worker-control.test.ts +241 -0
- package/src/schedule/run-script.ts +13 -6
- package/src/schedule/schedule-usage-store.ts +1 -0
- package/src/schedule/scheduler.ts +178 -127
- package/src/schedule/worker-control.ts +118 -0
- package/src/schedule/worker.ts +131 -0
- package/src/skills/__tests__/categories-cache.test.ts +38 -0
- package/src/skills/categories-cache.ts +40 -5
- package/src/skills/managed-store.ts +41 -6
- package/src/skills/validate-input.ts +47 -0
- package/src/stt/types.ts +5 -9
- package/src/subagent/manager.ts +267 -39
- package/src/subagent/types.ts +6 -2
- package/src/subagent/validate-allowlists.ts +65 -0
- package/src/telemetry/watchdog-direct-emit.test.ts +109 -0
- package/src/telemetry/watchdog-direct-emit.ts +89 -0
- package/src/tools/acp/list-agents.test.ts +0 -6
- package/src/tools/background-tool-registry.ts +61 -0
- package/src/tools/document/document-comment-tool.test.ts +4 -0
- package/src/tools/host-terminal/host-shell.test.ts +397 -0
- package/src/tools/host-terminal/host-shell.ts +203 -14
- package/src/tools/skills/find-similar-skills.test.ts +152 -1
- package/src/tools/skills/find-similar-skills.ts +30 -4
- package/src/tools/skills/load.ts +36 -0
- package/src/tools/skills/scaffold-managed.ts +66 -34
- package/src/tools/skills/skill-script-runner.ts +17 -0
- package/src/tools/skills/skill-tool-factory.ts +5 -1
- package/src/tools/terminal/safe-env.ts +3 -0
- package/src/tools/terminal/shell.test.ts +177 -0
- package/src/tools/terminal/shell.ts +98 -4
- package/src/tools/tool-approval-handler.ts +106 -29
- package/{node_modules/@vellumai/skill-host-contracts/src → src/tools}/tool-types.ts +16 -17
- package/src/tools/types.ts +29 -17
- package/src/tools/ui-surface/definitions.ts +3 -1
- package/src/tts/__tests__/provider-adapters.test.ts +37 -0
- package/src/tts/__tests__/provider-catalog.test.ts +21 -0
- package/src/tts/provider-catalog.ts +40 -9
- package/src/tts/providers/fish-audio-provider.ts +19 -10
- package/src/tts/types.ts +3 -4
- package/src/util/__tests__/process-tree.test.ts +10 -2
- package/src/util/__tests__/text-spacing.test.ts +53 -0
- package/src/util/cgroup-memory.ts +142 -0
- package/src/util/image-conversion.ts +281 -0
- package/src/util/phone.ts +1 -8
- package/src/util/platform.ts +33 -0
- package/src/util/process-tree.ts +10 -2
- package/src/util/text-spacing.ts +53 -0
- package/src/util/worker-process.ts +271 -0
- package/src/workflows/journal-store.ts +16 -1
- package/src/workflows/library.test.ts +35 -20
- package/src/workflows/library.ts +105 -66
- package/src/workspace/git-service.ts +1 -0
- package/src/workspace/migrations/011-backfill-installation-id.ts +83 -31
- package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +41 -22
- package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +1 -1
- package/src/workspace/migrations/118-seed-now-md.ts +64 -0
- package/src/workspace/migrations/119-strip-persisted-memory-v3-tuning-defaults.ts +169 -0
- package/src/workspace/migrations/120-revise-onboarding-threads.ts +47 -0
- package/src/workspace/migrations/121-seed-default-user-guardrails.ts +82 -0
- package/src/workspace/migrations/122-relocate-default-user-boundary.ts +111 -0
- package/src/workspace/migrations/123-swap-quality-profile-to-fable.ts +85 -0
- package/src/workspace/migrations/__tests__/119-strip-persisted-memory-v3-tuning-defaults.test.ts +181 -0
- package/src/workspace/migrations/registry.ts +12 -0
- package/drizzle/0000_dizzy_maggott.sql +0 -301
- package/drizzle/meta/0000_snapshot.json +0 -1933
- package/drizzle/meta/_journal.json +0 -13
- package/node_modules/@vellumai/skill-host-contracts/__tests__/client.test.ts +0 -887
- package/node_modules/@vellumai/skill-host-contracts/bun.lock +0 -24
- package/node_modules/@vellumai/skill-host-contracts/package.json +0 -18
- package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -97
- package/node_modules/@vellumai/skill-host-contracts/src/client.ts +0 -1349
- package/node_modules/@vellumai/skill-host-contracts/src/index.ts +0 -6
- package/node_modules/@vellumai/skill-host-contracts/src/runtime-mode.ts +0 -11
- package/node_modules/@vellumai/skill-host-contracts/src/server-message.ts +0 -32
- package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +0 -333
- package/node_modules/@vellumai/skill-host-contracts/tsconfig.json +0 -20
- package/node_modules/@vellumai/skill-host-contracts/tsconfig.test.json +0 -12
- package/src/__tests__/invite-redemption-service.test.ts +0 -1180
- package/src/__tests__/invite-service-ipc.test.ts +0 -350
- package/src/__tests__/job-handler-registry-guard.test.ts +0 -125
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +0 -203
- package/src/__tests__/relay-server.test.ts +0 -6153
- package/src/__tests__/telephony-stt-routing.test.ts +0 -329
- package/src/__tests__/voice-invite-redemption.test.ts +0 -702
- package/src/calls/relay-server.ts +0 -2463
- package/src/calls/telephony-stt-routing.ts +0 -145
- package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +0 -128
- package/src/daemon/__tests__/daemon-skill-host.test.ts +0 -273
- package/src/daemon/daemon-skill-host.ts +0 -262
- package/src/daemon/message-types/meet.ts +0 -143
- package/src/daemon/shutdown-registry.ts +0 -40
- package/src/ipc/__tests__/skill-server-bidirectional.test.ts +0 -253
- package/src/ipc/__tests__/skill-server.test.ts +0 -182
- package/src/ipc/skill-ipc-types.ts +0 -54
- package/src/ipc/skill-routes/__tests__/config.test.ts +0 -146
- package/src/ipc/skill-routes/__tests__/events-ipc.test.ts +0 -448
- package/src/ipc/skill-routes/__tests__/identity.test.ts +0 -62
- package/src/ipc/skill-routes/__tests__/log.test.ts +0 -133
- package/src/ipc/skill-routes/__tests__/memory.test.ts +0 -203
- package/src/ipc/skill-routes/__tests__/platform.test.ts +0 -111
- package/src/ipc/skill-routes/__tests__/providers.test.ts +0 -265
- package/src/ipc/skill-routes/__tests__/registries.test.ts +0 -400
- package/src/ipc/skill-routes/config.ts +0 -47
- package/src/ipc/skill-routes/events.ts +0 -147
- package/src/ipc/skill-routes/identity.ts +0 -21
- package/src/ipc/skill-routes/index.ts +0 -37
- package/src/ipc/skill-routes/log.ts +0 -40
- package/src/ipc/skill-routes/memory.ts +0 -92
- package/src/ipc/skill-routes/platform.ts +0 -39
- package/src/ipc/skill-routes/providers.ts +0 -162
- package/src/ipc/skill-routes/registries.ts +0 -338
- package/src/ipc/skill-server.ts +0 -831
- package/src/ipc/skill-socket-path.ts +0 -20
- package/src/jobs/register-job-handlers.ts +0 -74
- package/src/persistence/invite-store.ts +0 -481
- package/src/plugins/job-handler-registry.ts +0 -94
- package/src/providers/cache-control.ts +0 -26
- package/src/runtime/__tests__/runtime-mode.test.ts +0 -62
- package/src/runtime/invite-redemption-service.ts +0 -828
- package/src/runtime/invite-redemption-templates.ts +0 -46
- package/src/runtime/runtime-mode.ts +0 -40
- package/src/runtime/slack-dm-text-delivery.ts +0 -177
- package/src/util/voice-code.ts +0 -31
|
@@ -12,10 +12,13 @@ import {
|
|
|
12
12
|
// Module mocks — declared before imports of the module under test.
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
|
|
15
|
-
// Mock the STT resolve module (used by MediaStreamSttSession)
|
|
15
|
+
// Mock the STT resolve module (used by MediaStreamSttSession).
|
|
16
|
+
// resolveStreamingTranscriber yields no transcriber, so sessions settle on
|
|
17
|
+
// the batch path regardless of the calls.voice.telephonyStreaming default.
|
|
16
18
|
mock.module("../providers/speech-to-text/resolve.js", () => ({
|
|
17
19
|
resolveTelephonySttCapability: jest.fn(),
|
|
18
20
|
resolveBatchTranscriber: jest.fn(),
|
|
21
|
+
resolveStreamingTranscriber: jest.fn(async () => null),
|
|
19
22
|
}));
|
|
20
23
|
|
|
21
24
|
// Mock the logger to suppress output during tests
|
|
@@ -98,28 +101,33 @@ mock.module("../calls/finalize-call.js", () => ({
|
|
|
98
101
|
|
|
99
102
|
// Mock the call pointer messages
|
|
100
103
|
mock.module("../calls/call-pointer-messages.js", () => ({
|
|
101
|
-
|
|
104
|
+
postPointerMessageSafe: jest.fn(),
|
|
102
105
|
formatDuration: jest.fn((ms: number) => `${Math.round(ms / 1000)}s`),
|
|
103
106
|
}));
|
|
104
107
|
|
|
105
108
|
// Mock the CallController to avoid pulling in the full conversation pipeline
|
|
106
109
|
const mockStartInitialGreeting = jest.fn(async () => {});
|
|
110
|
+
const mockStartPostVerificationGreeting = jest.fn(async () => {});
|
|
111
|
+
const mockMarkNextCallerTurnAsOpeningAck = jest.fn();
|
|
107
112
|
const mockHandleCallerUtterance = jest.fn(async () => {});
|
|
108
113
|
const mockHandleInterrupt = jest.fn();
|
|
109
114
|
const mockDestroy = jest.fn();
|
|
110
115
|
|
|
111
|
-
|
|
116
|
+
// Mirrors CallController.handleBargeIn: invokes onAccepted only when the
|
|
117
|
+
// barge-in passes the speaking gate.
|
|
118
|
+
const mockHandleBargeIn = jest.fn((_onAccepted?: () => void) => false);
|
|
112
119
|
|
|
113
120
|
mock.module("../calls/call-controller.js", () => ({
|
|
114
121
|
CallController: jest.fn().mockImplementation(() => ({
|
|
115
122
|
startInitialGreeting: mockStartInitialGreeting,
|
|
123
|
+
startPostVerificationGreeting: mockStartPostVerificationGreeting,
|
|
124
|
+
markNextCallerTurnAsOpeningAck: mockMarkNextCallerTurnAsOpeningAck,
|
|
116
125
|
handleCallerUtterance: mockHandleCallerUtterance,
|
|
117
126
|
handleInterrupt: mockHandleInterrupt,
|
|
118
127
|
handleBargeIn: mockHandleBargeIn,
|
|
119
128
|
destroy: mockDestroy,
|
|
120
129
|
getState: jest.fn(() => "idle"),
|
|
121
130
|
setTrustContext: jest.fn(),
|
|
122
|
-
markNextCallerTurnAsOpeningAck: jest.fn(),
|
|
123
131
|
getPendingConsultationQuestionId: jest.fn(),
|
|
124
132
|
handleUserAnswer: jest.fn(),
|
|
125
133
|
handleUserInstruction: jest.fn(),
|
|
@@ -177,11 +185,20 @@ mock.module("../calls/channel-admission-reader.js", () => ({
|
|
|
177
185
|
}));
|
|
178
186
|
|
|
179
187
|
// Mock the inbound trust reader. handleStart awaits this and threads the
|
|
180
|
-
// verdict into routeSetup so the media-stream transport enforces the
|
|
181
|
-
// gateway ACL
|
|
188
|
+
// verdict into routeSetup so the media-stream transport enforces the
|
|
189
|
+
// gateway ACL. Tests override mockInboundVerdict.
|
|
190
|
+
// The optional gate lets a test hold mid-setup trust re-resolution open
|
|
191
|
+
// (the setup flow's default resolver reads the verdict first) so it can
|
|
192
|
+
// deliver transcripts during the deferral window.
|
|
182
193
|
let mockInboundVerdict: unknown = null;
|
|
194
|
+
let mockVerdictGate: Promise<void> | null = null;
|
|
183
195
|
const mockGetInboundTrustVerdict = jest.fn(
|
|
184
|
-
async (_args?: Record<string, unknown>) =>
|
|
196
|
+
async (_args?: Record<string, unknown>) => {
|
|
197
|
+
if (mockVerdictGate) {
|
|
198
|
+
await mockVerdictGate;
|
|
199
|
+
}
|
|
200
|
+
return mockInboundVerdict;
|
|
201
|
+
},
|
|
185
202
|
);
|
|
186
203
|
mock.module("../calls/inbound-trust-reader.js", () => ({
|
|
187
204
|
getInboundTrustVerdict: mockGetInboundTrustVerdict,
|
|
@@ -225,12 +242,152 @@ mock.module("../calls/resolve-call-tts-provider.js", () => ({
|
|
|
225
242
|
})),
|
|
226
243
|
}));
|
|
227
244
|
|
|
245
|
+
// ── Setup-flow dependency mocks ─────────────────────────────────────────
|
|
246
|
+
// MediaStreamCallSession drives every routeSetup outcome through a real
|
|
247
|
+
// CallSetupFlow (and, for name capture, a real GuardianWaitController).
|
|
248
|
+
// Mock the side-effectful modules those pull in: config-backed timing
|
|
249
|
+
// constants, guardian labels, verification/invite services, the guardian
|
|
250
|
+
// notifier, canonical-request polling, and wait heartbeats.
|
|
251
|
+
|
|
252
|
+
// Config-backed call constants — small real-timer-friendly values.
|
|
253
|
+
let mockTtsPlaybackDelayMs = 5;
|
|
254
|
+
let mockAccessRequestPollIntervalMs = 5;
|
|
255
|
+
let mockUserConsultationTimeoutMs = 500;
|
|
256
|
+
mock.module("../calls/call-constants.js", () => ({
|
|
257
|
+
isDeniedNumber: jest.fn(() => false),
|
|
258
|
+
getMaxCallDurationMs: jest.fn(() => 3_600_000),
|
|
259
|
+
getUserConsultationTimeoutMs: jest.fn(() => mockUserConsultationTimeoutMs),
|
|
260
|
+
getTtsPlaybackDelayMs: jest.fn(() => mockTtsPlaybackDelayMs),
|
|
261
|
+
getAccessRequestPollIntervalMs: jest.fn(
|
|
262
|
+
() => mockAccessRequestPollIntervalMs,
|
|
263
|
+
),
|
|
264
|
+
getGuardianWaitUpdateInitialIntervalMs: jest.fn(() => 10_000),
|
|
265
|
+
getGuardianWaitUpdateInitialWindowMs: jest.fn(() => 60_000),
|
|
266
|
+
getGuardianWaitUpdateSteadyMinIntervalMs: jest.fn(() => 20_000),
|
|
267
|
+
getGuardianWaitUpdateSteadyMaxIntervalMs: jest.fn(() => 40_000),
|
|
268
|
+
getSilenceTimeoutMs: jest.fn(() => 30_000),
|
|
269
|
+
getEndCallListenWindowMs: jest.fn(() => 15_000),
|
|
270
|
+
}));
|
|
271
|
+
|
|
272
|
+
// Guardian delivery reader (IPC-backed): label priming + cache warming.
|
|
273
|
+
mock.module("../contacts/guardian-delivery-reader.js", () => ({
|
|
274
|
+
getGuardianDelivery: jest.fn(async () => [
|
|
275
|
+
{ channelType: "phone", status: "active", displayName: "Alex" },
|
|
276
|
+
]),
|
|
277
|
+
getGuardianDeliveryFresh: jest.fn(async () => []),
|
|
278
|
+
peekCachedGuardianDelivery: jest.fn(() => null),
|
|
279
|
+
voiceGuardianDisplayName: jest.fn(() => "Alex"),
|
|
280
|
+
guardianForChannel: jest.fn(),
|
|
281
|
+
anyGuardian: jest.fn(),
|
|
282
|
+
invalidateGuardianDeliveryCache: jest.fn(),
|
|
283
|
+
}));
|
|
284
|
+
|
|
285
|
+
// Guardian/assistant display labels (filesystem-backed).
|
|
286
|
+
mock.module("../prompts/user-reference.js", () => ({
|
|
287
|
+
DEFAULT_USER_REFERENCE: "my human",
|
|
288
|
+
resolveGuardianName: jest.fn(
|
|
289
|
+
(primed?: string | null) => primed ?? "my human",
|
|
290
|
+
),
|
|
291
|
+
}));
|
|
292
|
+
mock.module("../daemon/identity-helpers.js", () => ({
|
|
293
|
+
getAssistantName: jest.fn(() => "Aria"),
|
|
294
|
+
resolveUserName: jest.fn(() => null),
|
|
295
|
+
}));
|
|
296
|
+
|
|
297
|
+
// Conversation persistence (used by callee verification code posting).
|
|
298
|
+
const mockAddMessage = jest.fn(async () => ({}));
|
|
299
|
+
mock.module("../persistence/conversation-crud.js", () => ({
|
|
300
|
+
addMessage: mockAddMessage,
|
|
301
|
+
}));
|
|
302
|
+
|
|
303
|
+
// Verification / invite services (gateway + verification-store backed).
|
|
304
|
+
type MockVerificationResult =
|
|
305
|
+
| {
|
|
306
|
+
outcome: "success";
|
|
307
|
+
verificationType: "guardian" | "trusted_contact";
|
|
308
|
+
eventName: string;
|
|
309
|
+
ttsMessage?: string;
|
|
310
|
+
}
|
|
311
|
+
| {
|
|
312
|
+
outcome: "failure";
|
|
313
|
+
eventName: string;
|
|
314
|
+
ttsMessage: string;
|
|
315
|
+
attempts: number;
|
|
316
|
+
}
|
|
317
|
+
| {
|
|
318
|
+
outcome: "retry";
|
|
319
|
+
ttsMessage: string;
|
|
320
|
+
attempt: number;
|
|
321
|
+
maxAttempts: number;
|
|
322
|
+
};
|
|
323
|
+
let mockVerificationResult: MockVerificationResult = {
|
|
324
|
+
outcome: "success",
|
|
325
|
+
verificationType: "guardian",
|
|
326
|
+
eventName: "voice_verification_succeeded",
|
|
327
|
+
};
|
|
328
|
+
const mockAttemptVerificationCode = jest.fn(async () => mockVerificationResult);
|
|
329
|
+
type MockInviteResult =
|
|
330
|
+
| { outcome: "success"; memberId: string; inviteId: string; type: string }
|
|
331
|
+
| { outcome: "failure"; ttsMessage: string };
|
|
332
|
+
let mockInviteResult: MockInviteResult = {
|
|
333
|
+
outcome: "success",
|
|
334
|
+
memberId: "member-1",
|
|
335
|
+
inviteId: "invite-1",
|
|
336
|
+
type: "trusted_contact",
|
|
337
|
+
};
|
|
338
|
+
const mockAttemptInviteCodeRedemption = jest.fn(async () => mockInviteResult);
|
|
339
|
+
mock.module("../calls/relay-verification.js", () => ({
|
|
340
|
+
attemptVerificationCode: mockAttemptVerificationCode,
|
|
341
|
+
attemptInviteCodeRedemption: mockAttemptInviteCodeRedemption,
|
|
342
|
+
parseDigitsFromSpeech: jest.fn((text: string) => text.replace(/\D+/g, "")),
|
|
343
|
+
}));
|
|
344
|
+
|
|
345
|
+
// Guardian access-request notifier (name-capture sub-flow).
|
|
346
|
+
type MockNotifyResult =
|
|
347
|
+
| { notified: true; created: boolean; requestId: string }
|
|
348
|
+
| { notified: false; reason: string };
|
|
349
|
+
let mockNotifyResult: MockNotifyResult = {
|
|
350
|
+
notified: true,
|
|
351
|
+
created: true,
|
|
352
|
+
requestId: "req-1",
|
|
353
|
+
};
|
|
354
|
+
const mockNotifyGuardianOfAccessRequest = jest.fn(async () => mockNotifyResult);
|
|
355
|
+
mock.module("../runtime/access-request-helper.js", () => ({
|
|
356
|
+
notifyGuardianOfAccessRequest: mockNotifyGuardianOfAccessRequest,
|
|
357
|
+
}));
|
|
358
|
+
|
|
359
|
+
// Canonical guardian-request store polled by the guardian wait controller.
|
|
360
|
+
let mockCanonicalRequest: { status: string } | null = null;
|
|
361
|
+
const mockGetCanonicalGuardianRequest = jest.fn(() => mockCanonicalRequest);
|
|
362
|
+
mock.module("../contacts/canonical-guardian-store.js", () => ({
|
|
363
|
+
getCanonicalGuardianRequest: mockGetCanonicalGuardianRequest,
|
|
364
|
+
}));
|
|
365
|
+
|
|
366
|
+
// Wait-state helpers: no heartbeats in tests; capture callback handoffs.
|
|
367
|
+
const mockEmitCallbackHandoff = jest.fn(
|
|
368
|
+
(args: { callbackHandoffNotified: boolean }) => ({
|
|
369
|
+
callbackHandoffNotified: args.callbackHandoffNotified,
|
|
370
|
+
notified: false,
|
|
371
|
+
}),
|
|
372
|
+
);
|
|
373
|
+
mock.module("../calls/relay-access-wait.js", () => ({
|
|
374
|
+
classifyWaitUtterance: jest.fn(() => "neutral"),
|
|
375
|
+
scheduleNextHeartbeat: jest.fn(() => null),
|
|
376
|
+
emitAccessRequestCallbackHandoff: mockEmitCallbackHandoff,
|
|
377
|
+
}));
|
|
378
|
+
|
|
228
379
|
// ---------------------------------------------------------------------------
|
|
229
380
|
// Now import the module under test.
|
|
230
381
|
// ---------------------------------------------------------------------------
|
|
231
382
|
|
|
383
|
+
import { revokeScopedApprovalGrantsForContext } from "../approvals/scoped-approval-grants.js";
|
|
384
|
+
import { CallController } from "../calls/call-controller.js";
|
|
385
|
+
import { postPointerMessageSafe } from "../calls/call-pointer-messages.js";
|
|
232
386
|
import { speakSystemPrompt } from "../calls/call-speech-output.js";
|
|
233
|
-
import {
|
|
387
|
+
import {
|
|
388
|
+
fireCallTranscriptNotifier,
|
|
389
|
+
registerCallController,
|
|
390
|
+
} from "../calls/call-state.js";
|
|
234
391
|
import { recordCallEvent, updateCallSession } from "../calls/call-store.js";
|
|
235
392
|
import { finalizeCall } from "../calls/finalize-call.js";
|
|
236
393
|
import {
|
|
@@ -358,22 +515,51 @@ beforeEach(() => {
|
|
|
358
515
|
mockControllers.clear();
|
|
359
516
|
activeMediaStreamSessions.clear();
|
|
360
517
|
mockStartInitialGreeting.mockClear();
|
|
518
|
+
mockStartPostVerificationGreeting.mockClear();
|
|
519
|
+
mockMarkNextCallerTurnAsOpeningAck.mockClear();
|
|
361
520
|
mockHandleCallerUtterance.mockClear();
|
|
362
521
|
mockHandleInterrupt.mockClear();
|
|
363
522
|
mockHandleBargeIn.mockClear();
|
|
364
523
|
mockHandleBargeIn.mockReturnValue(false);
|
|
365
524
|
mockDestroy.mockClear();
|
|
525
|
+
(CallController as unknown as jest.Mock).mockClear();
|
|
366
526
|
(registerCallController as jest.Mock).mockClear();
|
|
527
|
+
(fireCallTranscriptNotifier as jest.Mock).mockClear();
|
|
367
528
|
(recordCallEvent as jest.Mock).mockClear();
|
|
368
529
|
(updateCallSession as jest.Mock).mockClear();
|
|
369
530
|
(finalizeCall as jest.Mock).mockClear();
|
|
531
|
+
(revokeScopedApprovalGrantsForContext as jest.Mock).mockClear();
|
|
370
532
|
(speakSystemPrompt as jest.Mock).mockClear();
|
|
533
|
+
(postPointerMessageSafe as jest.Mock).mockClear();
|
|
371
534
|
(routeSetup as jest.Mock).mockClear();
|
|
372
535
|
mockGetChannelAdmissionPolicy.mockClear();
|
|
373
536
|
mockAdmissionPolicy = null;
|
|
374
537
|
mockAdmissionGate = null;
|
|
375
538
|
mockGetInboundTrustVerdict.mockClear();
|
|
376
539
|
mockInboundVerdict = null;
|
|
540
|
+
mockVerdictGate = null;
|
|
541
|
+
mockAddMessage.mockClear();
|
|
542
|
+
mockAttemptVerificationCode.mockClear();
|
|
543
|
+
mockVerificationResult = {
|
|
544
|
+
outcome: "success",
|
|
545
|
+
verificationType: "guardian",
|
|
546
|
+
eventName: "voice_verification_succeeded",
|
|
547
|
+
};
|
|
548
|
+
mockAttemptInviteCodeRedemption.mockClear();
|
|
549
|
+
mockInviteResult = {
|
|
550
|
+
outcome: "success",
|
|
551
|
+
memberId: "member-1",
|
|
552
|
+
inviteId: "invite-1",
|
|
553
|
+
type: "trusted_contact",
|
|
554
|
+
};
|
|
555
|
+
mockNotifyGuardianOfAccessRequest.mockClear();
|
|
556
|
+
mockNotifyResult = { notified: true, created: true, requestId: "req-1" };
|
|
557
|
+
mockGetCanonicalGuardianRequest.mockClear();
|
|
558
|
+
mockCanonicalRequest = null;
|
|
559
|
+
mockEmitCallbackHandoff.mockClear();
|
|
560
|
+
mockTtsPlaybackDelayMs = 5;
|
|
561
|
+
mockAccessRequestPollIntervalMs = 5;
|
|
562
|
+
mockUserConsultationTimeoutMs = 500;
|
|
377
563
|
// Reset routeSetup to default normal_call
|
|
378
564
|
mockRouteSetupResult = {
|
|
379
565
|
outcome: { action: "normal_call" as const, isInbound: true },
|
|
@@ -404,7 +590,6 @@ describe("MediaStreamCallSession", () => {
|
|
|
404
590
|
const session = new MediaStreamCallSession(ws, "call-1");
|
|
405
591
|
expect(session.callSessionId).toBe("call-1");
|
|
406
592
|
expect(session.getOutput()).toBeDefined();
|
|
407
|
-
expect(session.getOutput().getConnectionState()).toBe("connected");
|
|
408
593
|
});
|
|
409
594
|
|
|
410
595
|
describe("start event handling", () => {
|
|
@@ -472,7 +657,7 @@ describe("MediaStreamCallSession", () => {
|
|
|
472
657
|
});
|
|
473
658
|
|
|
474
659
|
describe("transport close handling", () => {
|
|
475
|
-
test("normal close (1000) marks session as completed", () => {
|
|
660
|
+
test("normal close (1000) marks session as completed and writes the completed pointer", () => {
|
|
476
661
|
const mock = createMockWs();
|
|
477
662
|
mockSessions.set("call-1", {
|
|
478
663
|
id: "call-1",
|
|
@@ -480,6 +665,7 @@ describe("MediaStreamCallSession", () => {
|
|
|
480
665
|
status: "in_progress",
|
|
481
666
|
startedAt: Date.now() - 60000,
|
|
482
667
|
toNumber: "+15551234567",
|
|
668
|
+
initiatedFromConversationId: "conv-origin",
|
|
483
669
|
});
|
|
484
670
|
|
|
485
671
|
const session = new MediaStreamCallSession(mock.ws, "call-1");
|
|
@@ -490,9 +676,15 @@ describe("MediaStreamCallSession", () => {
|
|
|
490
676
|
expect.objectContaining({ status: "completed" }),
|
|
491
677
|
);
|
|
492
678
|
expect(finalizeCall).toHaveBeenCalledWith("call-1", "conv-1");
|
|
679
|
+
expect(postPointerMessageSafe).toHaveBeenCalledWith(
|
|
680
|
+
"conv-origin",
|
|
681
|
+
"completed",
|
|
682
|
+
"+15551234567",
|
|
683
|
+
expect.objectContaining({ duration: expect.any(String) }),
|
|
684
|
+
);
|
|
493
685
|
});
|
|
494
686
|
|
|
495
|
-
test("abnormal close marks session as failed", () => {
|
|
687
|
+
test("abnormal close marks session as failed and writes the failed pointer", () => {
|
|
496
688
|
const mock = createMockWs();
|
|
497
689
|
mockSessions.set("call-1", {
|
|
498
690
|
id: "call-1",
|
|
@@ -500,6 +692,7 @@ describe("MediaStreamCallSession", () => {
|
|
|
500
692
|
status: "in_progress",
|
|
501
693
|
startedAt: Date.now() - 60000,
|
|
502
694
|
toNumber: "+15551234567",
|
|
695
|
+
initiatedFromConversationId: "conv-origin",
|
|
503
696
|
});
|
|
504
697
|
|
|
505
698
|
const session = new MediaStreamCallSession(mock.ws, "call-1");
|
|
@@ -513,6 +706,12 @@ describe("MediaStreamCallSession", () => {
|
|
|
513
706
|
}),
|
|
514
707
|
);
|
|
515
708
|
expect(finalizeCall).toHaveBeenCalledWith("call-1", "conv-1");
|
|
709
|
+
expect(postPointerMessageSafe).toHaveBeenCalledWith(
|
|
710
|
+
"conv-origin",
|
|
711
|
+
"failed",
|
|
712
|
+
"+15551234567",
|
|
713
|
+
expect.objectContaining({ reason: expect.any(String) }),
|
|
714
|
+
);
|
|
516
715
|
});
|
|
517
716
|
|
|
518
717
|
test("close on already-terminal session is a no-op", () => {
|
|
@@ -552,7 +751,6 @@ describe("MediaStreamCallSession", () => {
|
|
|
552
751
|
|
|
553
752
|
session.destroy();
|
|
554
753
|
expect(mockDestroy).toHaveBeenCalled();
|
|
555
|
-
expect(session.getOutput().getConnectionState()).toBe("closed");
|
|
556
754
|
});
|
|
557
755
|
|
|
558
756
|
test("destroy is idempotent", () => {
|
|
@@ -955,157 +1153,7 @@ describe("media-stream setup outcome scenarios", () => {
|
|
|
955
1153
|
});
|
|
956
1154
|
});
|
|
957
1155
|
|
|
958
|
-
describe("
|
|
959
|
-
test("verification outcome records call_failed with preflight-bypass reason", async () => {
|
|
960
|
-
mockRouteSetupResult = {
|
|
961
|
-
outcome: {
|
|
962
|
-
action: "verification",
|
|
963
|
-
assistantId: "self",
|
|
964
|
-
fromNumber: "+14155551234",
|
|
965
|
-
},
|
|
966
|
-
resolved: {
|
|
967
|
-
assistantId: "self",
|
|
968
|
-
isInbound: true,
|
|
969
|
-
otherPartyNumber: "+14155551234",
|
|
970
|
-
actorTrust: { trustClass: "unknown", memberRecord: null },
|
|
971
|
-
},
|
|
972
|
-
};
|
|
973
|
-
|
|
974
|
-
const mockWs = createMockWs();
|
|
975
|
-
mockSessions.set("call-unsup-verify-1", {
|
|
976
|
-
id: "call-unsup-verify-1",
|
|
977
|
-
conversationId: "conv-unsup-verify-1",
|
|
978
|
-
status: "initiated",
|
|
979
|
-
task: null,
|
|
980
|
-
startedAt: null,
|
|
981
|
-
fromNumber: "+14155551234",
|
|
982
|
-
toNumber: "+15550001111",
|
|
983
|
-
});
|
|
984
|
-
|
|
985
|
-
const session = new MediaStreamCallSession(
|
|
986
|
-
mockWs.ws,
|
|
987
|
-
"call-unsup-verify-1",
|
|
988
|
-
);
|
|
989
|
-
session.handleMessage(makeStartMessage());
|
|
990
|
-
await session.whenSetupSettled();
|
|
991
|
-
|
|
992
|
-
// Should record call_failed event with preflight-bypass note
|
|
993
|
-
expect(recordCallEvent).toHaveBeenCalledWith(
|
|
994
|
-
"call-unsup-verify-1",
|
|
995
|
-
"call_failed",
|
|
996
|
-
expect.objectContaining({
|
|
997
|
-
reason: expect.stringContaining("verification"),
|
|
998
|
-
transport: "media-stream",
|
|
999
|
-
}),
|
|
1000
|
-
);
|
|
1001
|
-
|
|
1002
|
-
// Should set session status to failed
|
|
1003
|
-
expect(updateCallSession).toHaveBeenCalledWith(
|
|
1004
|
-
"call-unsup-verify-1",
|
|
1005
|
-
expect.objectContaining({
|
|
1006
|
-
status: "failed",
|
|
1007
|
-
lastError: expect.stringContaining("preflight guard"),
|
|
1008
|
-
}),
|
|
1009
|
-
);
|
|
1010
|
-
|
|
1011
|
-
// Should NOT register a controller
|
|
1012
|
-
expect(registerCallController).not.toHaveBeenCalled();
|
|
1013
|
-
});
|
|
1014
|
-
|
|
1015
|
-
test("name_capture outcome speaks generic apology and tears down", async () => {
|
|
1016
|
-
mockRouteSetupResult = {
|
|
1017
|
-
outcome: {
|
|
1018
|
-
action: "name_capture",
|
|
1019
|
-
assistantId: "self",
|
|
1020
|
-
fromNumber: "+14155551234",
|
|
1021
|
-
},
|
|
1022
|
-
resolved: {
|
|
1023
|
-
assistantId: "self",
|
|
1024
|
-
isInbound: true,
|
|
1025
|
-
otherPartyNumber: "+14155551234",
|
|
1026
|
-
actorTrust: { trustClass: "unknown", memberRecord: null },
|
|
1027
|
-
},
|
|
1028
|
-
};
|
|
1029
|
-
|
|
1030
|
-
const mockWs = createMockWs();
|
|
1031
|
-
mockSessions.set("call-unsup-name-1", {
|
|
1032
|
-
id: "call-unsup-name-1",
|
|
1033
|
-
conversationId: "conv-unsup-name-1",
|
|
1034
|
-
status: "initiated",
|
|
1035
|
-
task: null,
|
|
1036
|
-
startedAt: null,
|
|
1037
|
-
fromNumber: "+14155551234",
|
|
1038
|
-
toNumber: "+15550001111",
|
|
1039
|
-
});
|
|
1040
|
-
|
|
1041
|
-
const session = new MediaStreamCallSession(
|
|
1042
|
-
mockWs.ws,
|
|
1043
|
-
"call-unsup-name-1",
|
|
1044
|
-
);
|
|
1045
|
-
session.handleMessage(makeStartMessage());
|
|
1046
|
-
await session.whenSetupSettled();
|
|
1047
|
-
|
|
1048
|
-
// speakSystemPrompt should be called with the generic apology
|
|
1049
|
-
expect(speakSystemPrompt).toHaveBeenCalledWith(
|
|
1050
|
-
expect.anything(),
|
|
1051
|
-
expect.stringContaining("additional verification"),
|
|
1052
|
-
);
|
|
1053
|
-
|
|
1054
|
-
// Should run finalization inline
|
|
1055
|
-
expect(finalizeCall).toHaveBeenCalledWith(
|
|
1056
|
-
"call-unsup-name-1",
|
|
1057
|
-
"conv-unsup-name-1",
|
|
1058
|
-
);
|
|
1059
|
-
});
|
|
1060
|
-
|
|
1061
|
-
test("callee_verification outcome fails with explicit reason", async () => {
|
|
1062
|
-
mockRouteSetupResult = {
|
|
1063
|
-
outcome: {
|
|
1064
|
-
action: "callee_verification",
|
|
1065
|
-
verificationConfig: { maxAttempts: 3, codeLength: 6 },
|
|
1066
|
-
},
|
|
1067
|
-
resolved: {
|
|
1068
|
-
assistantId: "self",
|
|
1069
|
-
isInbound: false,
|
|
1070
|
-
otherPartyNumber: "+14155551234",
|
|
1071
|
-
actorTrust: { trustClass: "guardian", memberRecord: null },
|
|
1072
|
-
},
|
|
1073
|
-
};
|
|
1074
|
-
|
|
1075
|
-
const mockWs = createMockWs();
|
|
1076
|
-
mockSessions.set("call-unsup-callee-1", {
|
|
1077
|
-
id: "call-unsup-callee-1",
|
|
1078
|
-
conversationId: "conv-unsup-callee-1",
|
|
1079
|
-
status: "initiated",
|
|
1080
|
-
task: null,
|
|
1081
|
-
startedAt: null,
|
|
1082
|
-
fromNumber: "+15550001111",
|
|
1083
|
-
toNumber: "+14155551234",
|
|
1084
|
-
});
|
|
1085
|
-
|
|
1086
|
-
const session = new MediaStreamCallSession(
|
|
1087
|
-
mockWs.ws,
|
|
1088
|
-
"call-unsup-callee-1",
|
|
1089
|
-
);
|
|
1090
|
-
session.handleMessage(makeStartMessage());
|
|
1091
|
-
await session.whenSetupSettled();
|
|
1092
|
-
|
|
1093
|
-
// Should record the failure with the specific action
|
|
1094
|
-
expect(recordCallEvent).toHaveBeenCalledWith(
|
|
1095
|
-
"call-unsup-callee-1",
|
|
1096
|
-
"call_failed",
|
|
1097
|
-
expect.objectContaining({
|
|
1098
|
-
reason: expect.stringContaining("callee_verification"),
|
|
1099
|
-
}),
|
|
1100
|
-
);
|
|
1101
|
-
|
|
1102
|
-
// Session should be failed
|
|
1103
|
-
expect(updateCallSession).toHaveBeenCalledWith(
|
|
1104
|
-
"call-unsup-callee-1",
|
|
1105
|
-
expect.objectContaining({ status: "failed" }),
|
|
1106
|
-
);
|
|
1107
|
-
});
|
|
1108
|
-
|
|
1156
|
+
describe("normal_call reset after deny", () => {
|
|
1109
1157
|
test("normal_call after deny scenario still creates controller", async () => {
|
|
1110
1158
|
// Verify that after a deny-scenario test, resetting to normal_call
|
|
1111
1159
|
// properly creates a controller (no cross-test pollution).
|
|
@@ -1179,6 +1227,15 @@ describe("media-stream setup outcome scenarios", () => {
|
|
|
1179
1227
|
expect(mockHandleBargeIn).toHaveBeenCalled();
|
|
1180
1228
|
expect(mockHandleInterrupt).not.toHaveBeenCalled();
|
|
1181
1229
|
|
|
1230
|
+
// An ignored barge-in flushes only Twilio's buffered audio (to
|
|
1231
|
+
// stop a completed turn's tail talking over the caller). The
|
|
1232
|
+
// internal playback queue is untouched, so the queued greeting
|
|
1233
|
+
// still plays — handleInterrupt must not have run.
|
|
1234
|
+
const clearCommands = mockWs.sent.filter(
|
|
1235
|
+
(s) => JSON.parse(s).event === "clear",
|
|
1236
|
+
);
|
|
1237
|
+
expect(clearCommands.length).toBeGreaterThan(0);
|
|
1238
|
+
|
|
1182
1239
|
// voice_session_aborted should NOT appear in recorded events
|
|
1183
1240
|
const abortEvents = mockEvents.filter(
|
|
1184
1241
|
(e) =>
|
|
@@ -1191,8 +1248,12 @@ describe("media-stream setup outcome scenarios", () => {
|
|
|
1191
1248
|
});
|
|
1192
1249
|
|
|
1193
1250
|
test("barge-in is accepted when controller is speaking", async () => {
|
|
1194
|
-
// Configure mock to indicate the controller is speaking
|
|
1195
|
-
|
|
1251
|
+
// Configure mock to indicate the controller is speaking; like the
|
|
1252
|
+
// real controller, it fires onAccepted before interrupting.
|
|
1253
|
+
mockHandleBargeIn.mockImplementation((onAccepted?: () => void) => {
|
|
1254
|
+
onAccepted?.();
|
|
1255
|
+
return true;
|
|
1256
|
+
});
|
|
1196
1257
|
|
|
1197
1258
|
const mockWs = createMockWs();
|
|
1198
1259
|
mockSessions.set("call-bargein-2", {
|
|
@@ -1213,8 +1274,13 @@ describe("media-stream setup outcome scenarios", () => {
|
|
|
1213
1274
|
const speechPayload = Buffer.alloc(160, 0x00).toString("base64");
|
|
1214
1275
|
session.handleMessage(makeMediaMessage(speechPayload, "1"));
|
|
1215
1276
|
|
|
1216
|
-
// handleBargeIn should have been called (returning true)
|
|
1277
|
+
// handleBargeIn should have been called (returning true), and the
|
|
1278
|
+
// accepted barge-in flushes outbound audio via the onAccepted hook.
|
|
1217
1279
|
expect(mockHandleBargeIn).toHaveBeenCalled();
|
|
1280
|
+
const clearCommands = mockWs.sent.filter(
|
|
1281
|
+
(s) => JSON.parse(s).event === "clear",
|
|
1282
|
+
);
|
|
1283
|
+
expect(clearCommands.length).toBe(1);
|
|
1218
1284
|
|
|
1219
1285
|
session.destroy();
|
|
1220
1286
|
});
|
|
@@ -1556,9 +1622,9 @@ describe("media-stream setup outcome scenarios", () => {
|
|
|
1556
1622
|
|
|
1557
1623
|
// ── Gateway trust verdict ──────────────────────────────────────────
|
|
1558
1624
|
// handleStart fetches getInboundTrustVerdict for the inbound caller and
|
|
1559
|
-
// threads it into routeSetup, so the media-stream transport enforces
|
|
1560
|
-
//
|
|
1561
|
-
//
|
|
1625
|
+
// threads it into routeSetup, so the media-stream transport enforces
|
|
1626
|
+
// the gateway ACL. routeSetup itself decides verdict-vs-local; these
|
|
1627
|
+
// tests assert the verdict is fetched and passed.
|
|
1562
1628
|
|
|
1563
1629
|
describe("gateway trust verdict", () => {
|
|
1564
1630
|
test("fetches the inbound caller's verdict and threads it into routeSetup", async () => {
|
|
@@ -1663,3 +1729,711 @@ describe("media-stream setup outcome scenarios", () => {
|
|
|
1663
1729
|
});
|
|
1664
1730
|
});
|
|
1665
1731
|
});
|
|
1732
|
+
|
|
1733
|
+
// ---------------------------------------------------------------------------
|
|
1734
|
+
// Setup-flow integration scenarios
|
|
1735
|
+
// ---------------------------------------------------------------------------
|
|
1736
|
+
// Drive every interactive routeSetup outcome end to end through the real
|
|
1737
|
+
// CallSetupFlow (and, for name capture, the real GuardianWaitController)
|
|
1738
|
+
// over a fake WebSocket. These use real timers with small mocked timing
|
|
1739
|
+
// constants so terminal hangups and guardian-wait polling actually fire.
|
|
1740
|
+
|
|
1741
|
+
describe("setup flows over the media-stream transport", () => {
|
|
1742
|
+
beforeEach(() => {
|
|
1743
|
+
jest.useRealTimers();
|
|
1744
|
+
});
|
|
1745
|
+
|
|
1746
|
+
const FROM = "+14155550100";
|
|
1747
|
+
const TO = "+15550001111";
|
|
1748
|
+
|
|
1749
|
+
const sleep = (ms = 15) => Bun.sleep(ms);
|
|
1750
|
+
|
|
1751
|
+
function setupCall(opts: {
|
|
1752
|
+
callId: string;
|
|
1753
|
+
outcome: { action: string; [key: string]: unknown };
|
|
1754
|
+
resolved?: Partial<(typeof mockRouteSetupResult)["resolved"]>;
|
|
1755
|
+
session?: Record<string, unknown>;
|
|
1756
|
+
}) {
|
|
1757
|
+
mockRouteSetupResult = {
|
|
1758
|
+
outcome: opts.outcome,
|
|
1759
|
+
resolved: {
|
|
1760
|
+
assistantId: "self",
|
|
1761
|
+
isInbound: true,
|
|
1762
|
+
otherPartyNumber: FROM,
|
|
1763
|
+
actorTrust: { trustClass: "unknown", memberRecord: null },
|
|
1764
|
+
...opts.resolved,
|
|
1765
|
+
},
|
|
1766
|
+
};
|
|
1767
|
+
mockSessions.set(opts.callId, {
|
|
1768
|
+
id: opts.callId,
|
|
1769
|
+
conversationId: `conv-${opts.callId}`,
|
|
1770
|
+
status: "initiated",
|
|
1771
|
+
task: null,
|
|
1772
|
+
startedAt: null,
|
|
1773
|
+
fromNumber: FROM,
|
|
1774
|
+
toNumber: TO,
|
|
1775
|
+
...opts.session,
|
|
1776
|
+
});
|
|
1777
|
+
const mockWs = createMockWs();
|
|
1778
|
+
const session = new MediaStreamCallSession(mockWs.ws, opts.callId);
|
|
1779
|
+
session.handleMessage(makeStartMessage());
|
|
1780
|
+
return { session, mockWs };
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
/**
|
|
1784
|
+
* Deliver a final caller transcript, mirroring the STT session's
|
|
1785
|
+
* onTranscriptFinal callback wiring (driving real audio through the
|
|
1786
|
+
* batch transcriber is out of scope for these tests).
|
|
1787
|
+
*/
|
|
1788
|
+
function deliverTranscript(
|
|
1789
|
+
session: MediaStreamCallSession,
|
|
1790
|
+
text: string,
|
|
1791
|
+
): void {
|
|
1792
|
+
(
|
|
1793
|
+
session as unknown as {
|
|
1794
|
+
handleTranscriptFinal(text: string, durationMs: number): void;
|
|
1795
|
+
}
|
|
1796
|
+
).handleTranscriptFinal(text, 500);
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
function enterDigits(session: MediaStreamCallSession, digits: string): void {
|
|
1800
|
+
for (const digit of digits) {
|
|
1801
|
+
session.handleMessage(makeDtmfMessage(digit));
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
function callerSpokeEvents(callId: string) {
|
|
1806
|
+
return mockEvents.filter(
|
|
1807
|
+
(e) => e.callSessionId === callId && e.eventType === "caller_spoke",
|
|
1808
|
+
);
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
describe("inbound verification", () => {
|
|
1812
|
+
test("success routes DTMF to the flow, then proceeds to the initial greeting", async () => {
|
|
1813
|
+
const { session } = setupCall({
|
|
1814
|
+
callId: "call-verif-ok",
|
|
1815
|
+
outcome: {
|
|
1816
|
+
action: "verification",
|
|
1817
|
+
assistantId: "self",
|
|
1818
|
+
fromNumber: FROM,
|
|
1819
|
+
},
|
|
1820
|
+
});
|
|
1821
|
+
await session.whenSetupSettled();
|
|
1822
|
+
|
|
1823
|
+
// The flow prompts for the code; no controller exists during setup.
|
|
1824
|
+
expect(speakSystemPrompt).toHaveBeenCalledWith(
|
|
1825
|
+
expect.anything(),
|
|
1826
|
+
expect.stringContaining("six-digit verification code"),
|
|
1827
|
+
);
|
|
1828
|
+
expect(session.getSetupFlow()?.getState()).toBe("collecting_code");
|
|
1829
|
+
expect(session.getController()).toBeNull();
|
|
1830
|
+
expect(registerCallController).not.toHaveBeenCalled();
|
|
1831
|
+
|
|
1832
|
+
enterDigits(session, "123456");
|
|
1833
|
+
await sleep();
|
|
1834
|
+
|
|
1835
|
+
expect(mockAttemptVerificationCode).toHaveBeenCalledWith(
|
|
1836
|
+
expect.objectContaining({ enteredCode: "123456", isOutbound: false }),
|
|
1837
|
+
);
|
|
1838
|
+
// The server records per-digit caller_spoke events for the flow.
|
|
1839
|
+
expect(callerSpokeEvents("call-verif-ok")).toHaveLength(6);
|
|
1840
|
+
|
|
1841
|
+
// Flow completed: controller created + initial greeting fired.
|
|
1842
|
+
expect(registerCallController).toHaveBeenCalledWith(
|
|
1843
|
+
"call-verif-ok",
|
|
1844
|
+
expect.anything(),
|
|
1845
|
+
);
|
|
1846
|
+
expect(
|
|
1847
|
+
(CallController as unknown as jest.Mock).mock.calls[0]?.[3],
|
|
1848
|
+
).toMatchObject({ assistantId: "self" });
|
|
1849
|
+
expect(mockStartInitialGreeting).toHaveBeenCalled();
|
|
1850
|
+
expect(session.getSetupFlow()).toBeNull();
|
|
1851
|
+
expect(session.getController()).not.toBeNull();
|
|
1852
|
+
expect(finalizeCall).not.toHaveBeenCalled();
|
|
1853
|
+
|
|
1854
|
+
// Post-setup transcripts route to the controller.
|
|
1855
|
+
deliverTranscript(session, "hello there");
|
|
1856
|
+
await sleep(5);
|
|
1857
|
+
expect(mockHandleCallerUtterance).toHaveBeenCalledWith("hello there");
|
|
1858
|
+
|
|
1859
|
+
session.destroy();
|
|
1860
|
+
});
|
|
1861
|
+
|
|
1862
|
+
test("failure fails the session, finalizes exactly once, and hangs up", async () => {
|
|
1863
|
+
mockVerificationResult = {
|
|
1864
|
+
outcome: "failure",
|
|
1865
|
+
eventName: "voice_verification_failed",
|
|
1866
|
+
ttsMessage: "Verification failed. Goodbye.",
|
|
1867
|
+
attempts: 3,
|
|
1868
|
+
};
|
|
1869
|
+
const { session, mockWs } = setupCall({
|
|
1870
|
+
callId: "call-verif-fail",
|
|
1871
|
+
outcome: {
|
|
1872
|
+
action: "verification",
|
|
1873
|
+
assistantId: "self",
|
|
1874
|
+
fromNumber: FROM,
|
|
1875
|
+
},
|
|
1876
|
+
});
|
|
1877
|
+
await session.whenSetupSettled();
|
|
1878
|
+
|
|
1879
|
+
enterDigits(session, "000000");
|
|
1880
|
+
await sleep();
|
|
1881
|
+
|
|
1882
|
+
expect(speakSystemPrompt).toHaveBeenCalledWith(
|
|
1883
|
+
expect.anything(),
|
|
1884
|
+
"Verification failed. Goodbye.",
|
|
1885
|
+
);
|
|
1886
|
+
expect(updateCallSession).toHaveBeenCalledWith(
|
|
1887
|
+
"call-verif-fail",
|
|
1888
|
+
expect.objectContaining({ status: "failed" }),
|
|
1889
|
+
);
|
|
1890
|
+
expect(finalizeCall).toHaveBeenCalledTimes(1);
|
|
1891
|
+
expect(finalizeCall).toHaveBeenCalledWith(
|
|
1892
|
+
"call-verif-fail",
|
|
1893
|
+
"conv-call-verif-fail",
|
|
1894
|
+
);
|
|
1895
|
+
expect(registerCallController).not.toHaveBeenCalled();
|
|
1896
|
+
|
|
1897
|
+
// The flow schedules its own delayed endSession.
|
|
1898
|
+
await sleep(30);
|
|
1899
|
+
expect(mockWs.closed).toBe(true);
|
|
1900
|
+
|
|
1901
|
+
// Transport close after flow-side finalization must not double-finalize.
|
|
1902
|
+
session.handleTransportClosed(1000, "session-ended");
|
|
1903
|
+
expect(finalizeCall).toHaveBeenCalledTimes(1);
|
|
1904
|
+
|
|
1905
|
+
session.destroy();
|
|
1906
|
+
});
|
|
1907
|
+
});
|
|
1908
|
+
|
|
1909
|
+
describe("outbound verification", () => {
|
|
1910
|
+
test("success posts the pointer and fires the post-verification greeting", async () => {
|
|
1911
|
+
const { session } = setupCall({
|
|
1912
|
+
callId: "call-outverif",
|
|
1913
|
+
outcome: {
|
|
1914
|
+
action: "outbound_verification",
|
|
1915
|
+
assistantId: "self",
|
|
1916
|
+
sessionId: "verif-sess-1",
|
|
1917
|
+
toNumber: TO,
|
|
1918
|
+
},
|
|
1919
|
+
resolved: {
|
|
1920
|
+
isInbound: false,
|
|
1921
|
+
otherPartyNumber: TO,
|
|
1922
|
+
actorTrust: { trustClass: "guardian", memberRecord: null },
|
|
1923
|
+
},
|
|
1924
|
+
session: { initiatedFromConversationId: "conv-origin-1" },
|
|
1925
|
+
});
|
|
1926
|
+
await session.whenSetupSettled();
|
|
1927
|
+
expect(registerCallController).not.toHaveBeenCalled();
|
|
1928
|
+
|
|
1929
|
+
enterDigits(session, "654321");
|
|
1930
|
+
await sleep();
|
|
1931
|
+
|
|
1932
|
+
expect(mockAttemptVerificationCode).toHaveBeenCalledWith(
|
|
1933
|
+
expect.objectContaining({ enteredCode: "654321", isOutbound: true }),
|
|
1934
|
+
);
|
|
1935
|
+
expect(postPointerMessageSafe).toHaveBeenCalledWith(
|
|
1936
|
+
"conv-origin-1",
|
|
1937
|
+
"verification_succeeded",
|
|
1938
|
+
TO,
|
|
1939
|
+
expect.objectContaining({ channel: "phone" }),
|
|
1940
|
+
);
|
|
1941
|
+
expect(mockStartPostVerificationGreeting).toHaveBeenCalled();
|
|
1942
|
+
expect(mockStartInitialGreeting).not.toHaveBeenCalled();
|
|
1943
|
+
expect(registerCallController).toHaveBeenCalledWith(
|
|
1944
|
+
"call-outverif",
|
|
1945
|
+
expect.anything(),
|
|
1946
|
+
);
|
|
1947
|
+
|
|
1948
|
+
session.destroy();
|
|
1949
|
+
});
|
|
1950
|
+
});
|
|
1951
|
+
|
|
1952
|
+
describe("callee verification", () => {
|
|
1953
|
+
test("posts the code to the origin conversation, retries a wrong code, greets on a match", async () => {
|
|
1954
|
+
const { session } = setupCall({
|
|
1955
|
+
callId: "call-callee",
|
|
1956
|
+
outcome: {
|
|
1957
|
+
action: "callee_verification",
|
|
1958
|
+
verificationConfig: { maxAttempts: 3, codeLength: 4 },
|
|
1959
|
+
},
|
|
1960
|
+
resolved: {
|
|
1961
|
+
isInbound: false,
|
|
1962
|
+
otherPartyNumber: TO,
|
|
1963
|
+
actorTrust: { trustClass: "guardian", memberRecord: null },
|
|
1964
|
+
},
|
|
1965
|
+
session: { initiatedFromConversationId: "conv-origin-2" },
|
|
1966
|
+
});
|
|
1967
|
+
await session.whenSetupSettled();
|
|
1968
|
+
|
|
1969
|
+
// The generated code is posted to the initiating conversation.
|
|
1970
|
+
expect(mockAddMessage).toHaveBeenCalledTimes(1);
|
|
1971
|
+
const [postedConvId, , postedContent] = mockAddMessage.mock
|
|
1972
|
+
.calls[0] as unknown as [string, string, string];
|
|
1973
|
+
expect(postedConvId).toBe("conv-origin-2");
|
|
1974
|
+
const code = /: (\d{4})/.exec(postedContent)?.[1];
|
|
1975
|
+
expect(code).toBeDefined();
|
|
1976
|
+
|
|
1977
|
+
// A wrong code re-prompts without ending the call.
|
|
1978
|
+
const wrongCode = code === "0000" ? "1111" : "0000";
|
|
1979
|
+
enterDigits(session, wrongCode);
|
|
1980
|
+
await sleep();
|
|
1981
|
+
expect(speakSystemPrompt).toHaveBeenCalledWith(
|
|
1982
|
+
expect.anything(),
|
|
1983
|
+
"That code was incorrect. Please try again.",
|
|
1984
|
+
);
|
|
1985
|
+
expect(registerCallController).not.toHaveBeenCalled();
|
|
1986
|
+
|
|
1987
|
+
// The right code proceeds to the initial greeting.
|
|
1988
|
+
enterDigits(session, code!);
|
|
1989
|
+
await sleep();
|
|
1990
|
+
expect(registerCallController).toHaveBeenCalledWith(
|
|
1991
|
+
"call-callee",
|
|
1992
|
+
expect.anything(),
|
|
1993
|
+
);
|
|
1994
|
+
expect(mockStartInitialGreeting).toHaveBeenCalled();
|
|
1995
|
+
expect(finalizeCall).not.toHaveBeenCalled();
|
|
1996
|
+
|
|
1997
|
+
session.destroy();
|
|
1998
|
+
});
|
|
1999
|
+
});
|
|
2000
|
+
|
|
2001
|
+
describe("invite redemption", () => {
|
|
2002
|
+
test("success speaks the handoff, marks the opening ack, and replays deferred transcripts", async () => {
|
|
2003
|
+
const { session } = setupCall({
|
|
2004
|
+
callId: "call-invite",
|
|
2005
|
+
outcome: {
|
|
2006
|
+
action: "invite_redemption",
|
|
2007
|
+
assistantId: "self",
|
|
2008
|
+
fromNumber: FROM,
|
|
2009
|
+
inviteeName: "Casey Example",
|
|
2010
|
+
},
|
|
2011
|
+
});
|
|
2012
|
+
await session.whenSetupSettled();
|
|
2013
|
+
|
|
2014
|
+
expect(speakSystemPrompt).toHaveBeenCalledWith(
|
|
2015
|
+
expect.anything(),
|
|
2016
|
+
expect.stringContaining("Welcome Casey."),
|
|
2017
|
+
);
|
|
2018
|
+
|
|
2019
|
+
// Hold mid-setup trust re-resolution open so a transcript arrives
|
|
2020
|
+
// during the deferral window.
|
|
2021
|
+
let releaseVerdict!: () => void;
|
|
2022
|
+
mockVerdictGate = new Promise<void>((resolve) => {
|
|
2023
|
+
releaseVerdict = resolve;
|
|
2024
|
+
});
|
|
2025
|
+
|
|
2026
|
+
enterDigits(session, "111222");
|
|
2027
|
+
await sleep();
|
|
2028
|
+
expect(mockAttemptInviteCodeRedemption).toHaveBeenCalledWith(
|
|
2029
|
+
expect.objectContaining({ enteredCode: "111222" }),
|
|
2030
|
+
);
|
|
2031
|
+
|
|
2032
|
+
// Caller speaks while the trust upgrade is in flight — deferred, but
|
|
2033
|
+
// the transcript notifier still fires for UI subscribers.
|
|
2034
|
+
deliverTranscript(session, "hi can you help me book a flight");
|
|
2035
|
+
expect(fireCallTranscriptNotifier).toHaveBeenCalledWith(
|
|
2036
|
+
"conv-call-invite",
|
|
2037
|
+
"call-invite",
|
|
2038
|
+
"caller",
|
|
2039
|
+
"hi can you help me book a flight",
|
|
2040
|
+
);
|
|
2041
|
+
expect(mockHandleCallerUtterance).not.toHaveBeenCalled();
|
|
2042
|
+
|
|
2043
|
+
releaseVerdict();
|
|
2044
|
+
await sleep();
|
|
2045
|
+
|
|
2046
|
+
expect(speakSystemPrompt).toHaveBeenCalledWith(
|
|
2047
|
+
expect.anything(),
|
|
2048
|
+
expect.stringContaining("verified that you are Casey"),
|
|
2049
|
+
);
|
|
2050
|
+
expect(mockMarkNextCallerTurnAsOpeningAck).toHaveBeenCalled();
|
|
2051
|
+
expect(mockStartInitialGreeting).not.toHaveBeenCalled();
|
|
2052
|
+
expect(registerCallController).toHaveBeenCalledWith(
|
|
2053
|
+
"call-invite",
|
|
2054
|
+
expect.anything(),
|
|
2055
|
+
);
|
|
2056
|
+
// The deferred transcript replays into the controller after the opener.
|
|
2057
|
+
expect(mockHandleCallerUtterance).toHaveBeenCalledWith(
|
|
2058
|
+
"hi can you help me book a flight",
|
|
2059
|
+
);
|
|
2060
|
+
|
|
2061
|
+
session.destroy();
|
|
2062
|
+
});
|
|
2063
|
+
|
|
2064
|
+
test("failure speaks the failure copy and finalizes exactly once", async () => {
|
|
2065
|
+
mockInviteResult = {
|
|
2066
|
+
outcome: "failure",
|
|
2067
|
+
ttsMessage: "That code is not valid. Goodbye.",
|
|
2068
|
+
};
|
|
2069
|
+
const { session, mockWs } = setupCall({
|
|
2070
|
+
callId: "call-invite-fail",
|
|
2071
|
+
outcome: {
|
|
2072
|
+
action: "invite_redemption",
|
|
2073
|
+
assistantId: "self",
|
|
2074
|
+
fromNumber: FROM,
|
|
2075
|
+
inviteeName: null,
|
|
2076
|
+
},
|
|
2077
|
+
});
|
|
2078
|
+
await session.whenSetupSettled();
|
|
2079
|
+
|
|
2080
|
+
enterDigits(session, "999999");
|
|
2081
|
+
await sleep();
|
|
2082
|
+
|
|
2083
|
+
expect(speakSystemPrompt).toHaveBeenCalledWith(
|
|
2084
|
+
expect.anything(),
|
|
2085
|
+
"That code is not valid. Goodbye.",
|
|
2086
|
+
);
|
|
2087
|
+
expect(updateCallSession).toHaveBeenCalledWith(
|
|
2088
|
+
"call-invite-fail",
|
|
2089
|
+
expect.objectContaining({ status: "failed" }),
|
|
2090
|
+
);
|
|
2091
|
+
expect(finalizeCall).toHaveBeenCalledTimes(1);
|
|
2092
|
+
expect(registerCallController).not.toHaveBeenCalled();
|
|
2093
|
+
|
|
2094
|
+
await sleep(30);
|
|
2095
|
+
expect(mockWs.closed).toBe(true);
|
|
2096
|
+
session.handleTransportClosed(1000, "session-ended");
|
|
2097
|
+
expect(finalizeCall).toHaveBeenCalledTimes(1);
|
|
2098
|
+
|
|
2099
|
+
session.destroy();
|
|
2100
|
+
});
|
|
2101
|
+
});
|
|
2102
|
+
|
|
2103
|
+
describe("name capture and guardian wait", () => {
|
|
2104
|
+
function nameCaptureOutcome() {
|
|
2105
|
+
return {
|
|
2106
|
+
action: "name_capture",
|
|
2107
|
+
assistantId: "self",
|
|
2108
|
+
fromNumber: FROM,
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
test("waits for the guardian with NO controller, then approval hands off", async () => {
|
|
2113
|
+
const { session } = setupCall({
|
|
2114
|
+
callId: "call-name-ok",
|
|
2115
|
+
outcome: nameCaptureOutcome(),
|
|
2116
|
+
});
|
|
2117
|
+
await session.whenSetupSettled();
|
|
2118
|
+
|
|
2119
|
+
expect(speakSystemPrompt).toHaveBeenCalledWith(
|
|
2120
|
+
expect.anything(),
|
|
2121
|
+
expect.stringContaining("Can I get your name?"),
|
|
2122
|
+
);
|
|
2123
|
+
expect(session.getSetupFlow()?.getState()).toBe("capturing_name");
|
|
2124
|
+
|
|
2125
|
+
mockCanonicalRequest = { status: "pending" };
|
|
2126
|
+
deliverTranscript(session, "Casey Example");
|
|
2127
|
+
await sleep();
|
|
2128
|
+
|
|
2129
|
+
expect(mockNotifyGuardianOfAccessRequest).toHaveBeenCalledWith(
|
|
2130
|
+
expect.objectContaining({ actorDisplayName: "Casey Example" }),
|
|
2131
|
+
);
|
|
2132
|
+
// The transcript notifier fires for caller turns during setup.
|
|
2133
|
+
expect(fireCallTranscriptNotifier).toHaveBeenCalledWith(
|
|
2134
|
+
"conv-call-name-ok",
|
|
2135
|
+
"call-name-ok",
|
|
2136
|
+
"caller",
|
|
2137
|
+
"Casey Example",
|
|
2138
|
+
);
|
|
2139
|
+
expect(updateCallSession).toHaveBeenCalledWith("call-name-ok", {
|
|
2140
|
+
status: "waiting_on_user",
|
|
2141
|
+
});
|
|
2142
|
+
// No controller exists during the wait — no silence nudges can fire.
|
|
2143
|
+
expect(session.getSetupFlow()?.getState()).toBe(
|
|
2144
|
+
"awaiting_guardian_decision",
|
|
2145
|
+
);
|
|
2146
|
+
expect(session.getController()).toBeNull();
|
|
2147
|
+
expect(CallController as unknown as jest.Mock).not.toHaveBeenCalled();
|
|
2148
|
+
expect(registerCallController).not.toHaveBeenCalled();
|
|
2149
|
+
|
|
2150
|
+
// The guardian approves; the wait poll picks it up.
|
|
2151
|
+
mockCanonicalRequest = { status: "approved" };
|
|
2152
|
+
await sleep(40);
|
|
2153
|
+
|
|
2154
|
+
expect(speakSystemPrompt).toHaveBeenCalledWith(
|
|
2155
|
+
expect.anything(),
|
|
2156
|
+
"Great! Alex said I can speak with you. How can I help?",
|
|
2157
|
+
);
|
|
2158
|
+
expect(mockMarkNextCallerTurnAsOpeningAck).toHaveBeenCalled();
|
|
2159
|
+
expect(registerCallController).toHaveBeenCalledWith(
|
|
2160
|
+
"call-name-ok",
|
|
2161
|
+
expect.anything(),
|
|
2162
|
+
);
|
|
2163
|
+
expect(session.getSetupFlow()).toBeNull();
|
|
2164
|
+
expect(finalizeCall).not.toHaveBeenCalled();
|
|
2165
|
+
|
|
2166
|
+
session.destroy();
|
|
2167
|
+
});
|
|
2168
|
+
|
|
2169
|
+
test("guardian denial speaks the goodbye copy and finalizes exactly once", async () => {
|
|
2170
|
+
const { session, mockWs } = setupCall({
|
|
2171
|
+
callId: "call-name-deny",
|
|
2172
|
+
outcome: nameCaptureOutcome(),
|
|
2173
|
+
});
|
|
2174
|
+
await session.whenSetupSettled();
|
|
2175
|
+
|
|
2176
|
+
mockCanonicalRequest = { status: "pending" };
|
|
2177
|
+
deliverTranscript(session, "Casey Example");
|
|
2178
|
+
await sleep();
|
|
2179
|
+
|
|
2180
|
+
mockCanonicalRequest = { status: "denied" };
|
|
2181
|
+
await sleep(40);
|
|
2182
|
+
|
|
2183
|
+
expect(speakSystemPrompt).toHaveBeenCalledWith(
|
|
2184
|
+
expect.anything(),
|
|
2185
|
+
"Sorry, Alex says I'm not allowed to speak with you. Goodbye.",
|
|
2186
|
+
);
|
|
2187
|
+
expect(updateCallSession).toHaveBeenCalledWith(
|
|
2188
|
+
"call-name-deny",
|
|
2189
|
+
expect.objectContaining({ status: "failed" }),
|
|
2190
|
+
);
|
|
2191
|
+
expect(finalizeCall).toHaveBeenCalledTimes(1);
|
|
2192
|
+
expect(registerCallController).not.toHaveBeenCalled();
|
|
2193
|
+
|
|
2194
|
+
await sleep(30);
|
|
2195
|
+
expect(mockWs.closed).toBe(true);
|
|
2196
|
+
session.handleTransportClosed(1000, "session-ended");
|
|
2197
|
+
expect(finalizeCall).toHaveBeenCalledTimes(1);
|
|
2198
|
+
|
|
2199
|
+
session.destroy();
|
|
2200
|
+
});
|
|
2201
|
+
|
|
2202
|
+
test("guardian-wait timeout speaks the timeout copy and finalizes exactly once", async () => {
|
|
2203
|
+
mockUserConsultationTimeoutMs = 40;
|
|
2204
|
+
const { session } = setupCall({
|
|
2205
|
+
callId: "call-name-timeout",
|
|
2206
|
+
outcome: nameCaptureOutcome(),
|
|
2207
|
+
});
|
|
2208
|
+
await session.whenSetupSettled();
|
|
2209
|
+
|
|
2210
|
+
mockCanonicalRequest = { status: "pending" };
|
|
2211
|
+
deliverTranscript(session, "Casey Example");
|
|
2212
|
+
await sleep();
|
|
2213
|
+
|
|
2214
|
+
await sleep(80);
|
|
2215
|
+
|
|
2216
|
+
expect(mockEmitCallbackHandoff).toHaveBeenCalledWith(
|
|
2217
|
+
expect.objectContaining({ reason: "timeout" }),
|
|
2218
|
+
);
|
|
2219
|
+
expect(speakSystemPrompt).toHaveBeenCalledWith(
|
|
2220
|
+
expect.anything(),
|
|
2221
|
+
expect.stringContaining("Sorry, I can't get ahold of Alex right now."),
|
|
2222
|
+
);
|
|
2223
|
+
expect(updateCallSession).toHaveBeenCalledWith(
|
|
2224
|
+
"call-name-timeout",
|
|
2225
|
+
expect.objectContaining({ status: "failed" }),
|
|
2226
|
+
);
|
|
2227
|
+
expect(finalizeCall).toHaveBeenCalledTimes(1);
|
|
2228
|
+
expect(registerCallController).not.toHaveBeenCalled();
|
|
2229
|
+
|
|
2230
|
+
session.destroy();
|
|
2231
|
+
});
|
|
2232
|
+
|
|
2233
|
+
test("disconnect mid-wait disposes the flow, emits the callback handoff, and finalizes once", async () => {
|
|
2234
|
+
const { session } = setupCall({
|
|
2235
|
+
callId: "call-name-drop",
|
|
2236
|
+
outcome: nameCaptureOutcome(),
|
|
2237
|
+
});
|
|
2238
|
+
await session.whenSetupSettled();
|
|
2239
|
+
|
|
2240
|
+
mockCanonicalRequest = { status: "pending" };
|
|
2241
|
+
deliverTranscript(session, "Casey Example");
|
|
2242
|
+
await sleep();
|
|
2243
|
+
expect(session.getSetupFlow()?.getState()).toBe(
|
|
2244
|
+
"awaiting_guardian_decision",
|
|
2245
|
+
);
|
|
2246
|
+
|
|
2247
|
+
session.handleTransportClosed(1006, "network dropped");
|
|
2248
|
+
|
|
2249
|
+
expect(mockEmitCallbackHandoff).toHaveBeenCalledWith(
|
|
2250
|
+
expect.objectContaining({ reason: "transport_closed" }),
|
|
2251
|
+
);
|
|
2252
|
+
expect(updateCallSession).toHaveBeenCalledWith(
|
|
2253
|
+
"call-name-drop",
|
|
2254
|
+
expect.objectContaining({ status: "failed" }),
|
|
2255
|
+
);
|
|
2256
|
+
expect(finalizeCall).toHaveBeenCalledTimes(1);
|
|
2257
|
+
expect(session.getSetupFlow()).toBeNull();
|
|
2258
|
+
|
|
2259
|
+
// A late approval must not resurrect the torn-down call.
|
|
2260
|
+
mockCanonicalRequest = { status: "approved" };
|
|
2261
|
+
await sleep(40);
|
|
2262
|
+
expect(registerCallController).not.toHaveBeenCalled();
|
|
2263
|
+
expect(finalizeCall).toHaveBeenCalledTimes(1);
|
|
2264
|
+
|
|
2265
|
+
session.destroy();
|
|
2266
|
+
});
|
|
2267
|
+
});
|
|
2268
|
+
|
|
2269
|
+
describe("unverified caller", () => {
|
|
2270
|
+
test("hears the verification guidance and the call ends without a controller", async () => {
|
|
2271
|
+
const { session, mockWs } = setupCall({
|
|
2272
|
+
callId: "call-unverified",
|
|
2273
|
+
outcome: {
|
|
2274
|
+
action: "unverified_caller",
|
|
2275
|
+
assistantId: "self",
|
|
2276
|
+
fromNumber: FROM,
|
|
2277
|
+
displayName: "Sam Example",
|
|
2278
|
+
isGuardian: false,
|
|
2279
|
+
},
|
|
2280
|
+
});
|
|
2281
|
+
await session.whenSetupSettled();
|
|
2282
|
+
|
|
2283
|
+
expect(speakSystemPrompt).toHaveBeenCalledWith(
|
|
2284
|
+
expect.anything(),
|
|
2285
|
+
expect.stringContaining("registered as Sam Example's phone"),
|
|
2286
|
+
);
|
|
2287
|
+
expect(
|
|
2288
|
+
mockEvents.some(
|
|
2289
|
+
(e) =>
|
|
2290
|
+
e.callSessionId === "call-unverified" &&
|
|
2291
|
+
e.eventType === "inbound_acl_unverified_caller",
|
|
2292
|
+
),
|
|
2293
|
+
).toBe(true);
|
|
2294
|
+
expect(updateCallSession).toHaveBeenCalledWith(
|
|
2295
|
+
"call-unverified",
|
|
2296
|
+
expect.objectContaining({ status: "failed" }),
|
|
2297
|
+
);
|
|
2298
|
+
expect(finalizeCall).toHaveBeenCalledTimes(1);
|
|
2299
|
+
expect(registerCallController).not.toHaveBeenCalled();
|
|
2300
|
+
|
|
2301
|
+
await sleep(30);
|
|
2302
|
+
expect(mockWs.closed).toBe(true);
|
|
2303
|
+
|
|
2304
|
+
session.destroy();
|
|
2305
|
+
});
|
|
2306
|
+
});
|
|
2307
|
+
|
|
2308
|
+
describe("hangup during terminal setup speech", () => {
|
|
2309
|
+
/** Gate the next speakSystemPrompt call on a manually released promise. */
|
|
2310
|
+
function gateNextSpeech(): () => void {
|
|
2311
|
+
let release!: () => void;
|
|
2312
|
+
(speakSystemPrompt as jest.Mock).mockImplementationOnce(
|
|
2313
|
+
() =>
|
|
2314
|
+
new Promise<void>((resolve) => {
|
|
2315
|
+
release = resolve;
|
|
2316
|
+
}),
|
|
2317
|
+
);
|
|
2318
|
+
return () => release();
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
test("hangup during the deny goodbye finalizes exactly once and revokes grants", async () => {
|
|
2322
|
+
const releaseSpeech = gateNextSpeech();
|
|
2323
|
+
const { session } = setupCall({
|
|
2324
|
+
callId: "call-deny-hangup",
|
|
2325
|
+
outcome: {
|
|
2326
|
+
action: "deny",
|
|
2327
|
+
message: "Not authorized.",
|
|
2328
|
+
logReason: "ACL deny",
|
|
2329
|
+
},
|
|
2330
|
+
});
|
|
2331
|
+
await sleep();
|
|
2332
|
+
|
|
2333
|
+
// The deny path sets terminal status before its goodbye finishes.
|
|
2334
|
+
expect(updateCallSession).toHaveBeenCalledWith(
|
|
2335
|
+
"call-deny-hangup",
|
|
2336
|
+
expect.objectContaining({ status: "failed" }),
|
|
2337
|
+
);
|
|
2338
|
+
expect(finalizeCall).not.toHaveBeenCalled();
|
|
2339
|
+
|
|
2340
|
+
// Caller hangs up while the goodbye is still speaking.
|
|
2341
|
+
session.handleTransportClosed(1006, "caller hung up");
|
|
2342
|
+
|
|
2343
|
+
expect(finalizeCall).toHaveBeenCalledTimes(1);
|
|
2344
|
+
expect(finalizeCall).toHaveBeenCalledWith(
|
|
2345
|
+
"call-deny-hangup",
|
|
2346
|
+
"conv-call-deny-hangup",
|
|
2347
|
+
);
|
|
2348
|
+
expect(revokeScopedApprovalGrantsForContext).toHaveBeenCalledWith({
|
|
2349
|
+
callSessionId: "call-deny-hangup",
|
|
2350
|
+
});
|
|
2351
|
+
expect(revokeScopedApprovalGrantsForContext).toHaveBeenCalledWith({
|
|
2352
|
+
conversationId: "conv-call-deny-hangup",
|
|
2353
|
+
});
|
|
2354
|
+
expect(session.getSetupFlow()).toBeNull();
|
|
2355
|
+
|
|
2356
|
+
// The speech promise settling later must not double-finalize.
|
|
2357
|
+
releaseSpeech();
|
|
2358
|
+
await session.whenSetupSettled();
|
|
2359
|
+
await sleep();
|
|
2360
|
+
expect(finalizeCall).toHaveBeenCalledTimes(1);
|
|
2361
|
+
expect(registerCallController).not.toHaveBeenCalled();
|
|
2362
|
+
|
|
2363
|
+
session.destroy();
|
|
2364
|
+
});
|
|
2365
|
+
|
|
2366
|
+
test("hangup during the guardian-denial goodbye finalizes exactly once and revokes grants", async () => {
|
|
2367
|
+
const { session } = setupCall({
|
|
2368
|
+
callId: "call-guard-deny-hangup",
|
|
2369
|
+
outcome: {
|
|
2370
|
+
action: "name_capture",
|
|
2371
|
+
assistantId: "self",
|
|
2372
|
+
fromNumber: FROM,
|
|
2373
|
+
},
|
|
2374
|
+
});
|
|
2375
|
+
await session.whenSetupSettled();
|
|
2376
|
+
|
|
2377
|
+
mockCanonicalRequest = { status: "pending" };
|
|
2378
|
+
deliverTranscript(session, "Casey Example");
|
|
2379
|
+
await sleep();
|
|
2380
|
+
|
|
2381
|
+
const releaseSpeech = gateNextSpeech();
|
|
2382
|
+
mockCanonicalRequest = { status: "denied" };
|
|
2383
|
+
await sleep(40);
|
|
2384
|
+
|
|
2385
|
+
expect(updateCallSession).toHaveBeenCalledWith(
|
|
2386
|
+
"call-guard-deny-hangup",
|
|
2387
|
+
expect.objectContaining({ status: "failed" }),
|
|
2388
|
+
);
|
|
2389
|
+
expect(finalizeCall).not.toHaveBeenCalled();
|
|
2390
|
+
|
|
2391
|
+
// Caller hangs up while the denial goodbye is still speaking.
|
|
2392
|
+
session.handleTransportClosed(1006, "caller hung up");
|
|
2393
|
+
|
|
2394
|
+
expect(finalizeCall).toHaveBeenCalledTimes(1);
|
|
2395
|
+
expect(finalizeCall).toHaveBeenCalledWith(
|
|
2396
|
+
"call-guard-deny-hangup",
|
|
2397
|
+
"conv-call-guard-deny-hangup",
|
|
2398
|
+
);
|
|
2399
|
+
expect(revokeScopedApprovalGrantsForContext).toHaveBeenCalledWith({
|
|
2400
|
+
callSessionId: "call-guard-deny-hangup",
|
|
2401
|
+
});
|
|
2402
|
+
expect(session.getSetupFlow()).toBeNull();
|
|
2403
|
+
|
|
2404
|
+
releaseSpeech();
|
|
2405
|
+
await sleep();
|
|
2406
|
+
expect(finalizeCall).toHaveBeenCalledTimes(1);
|
|
2407
|
+
expect(registerCallController).not.toHaveBeenCalled();
|
|
2408
|
+
|
|
2409
|
+
session.destroy();
|
|
2410
|
+
});
|
|
2411
|
+
|
|
2412
|
+
test("deny completion without a hangup finalizes exactly once; a later close does not re-finalize", async () => {
|
|
2413
|
+
const { session, mockWs } = setupCall({
|
|
2414
|
+
callId: "call-deny-normal",
|
|
2415
|
+
outcome: {
|
|
2416
|
+
action: "deny",
|
|
2417
|
+
message: "Not authorized.",
|
|
2418
|
+
logReason: "ACL deny",
|
|
2419
|
+
},
|
|
2420
|
+
});
|
|
2421
|
+
await session.whenSetupSettled();
|
|
2422
|
+
|
|
2423
|
+
expect(finalizeCall).toHaveBeenCalledTimes(1);
|
|
2424
|
+
expect(finalizeCall).toHaveBeenCalledWith(
|
|
2425
|
+
"call-deny-normal",
|
|
2426
|
+
"conv-call-deny-normal",
|
|
2427
|
+
);
|
|
2428
|
+
|
|
2429
|
+
// The flow schedules its own delayed endSession.
|
|
2430
|
+
await sleep(30);
|
|
2431
|
+
expect(mockWs.closed).toBe(true);
|
|
2432
|
+
|
|
2433
|
+
session.handleTransportClosed(1000, "session-ended");
|
|
2434
|
+
expect(finalizeCall).toHaveBeenCalledTimes(1);
|
|
2435
|
+
|
|
2436
|
+
session.destroy();
|
|
2437
|
+
});
|
|
2438
|
+
});
|
|
2439
|
+
});
|