@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
|
@@ -0,0 +1,1743 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport-agnostic call setup flow.
|
|
3
|
+
*
|
|
4
|
+
* Runs the pre-conversation phase of a phone call — acting on the routing
|
|
5
|
+
* outcome produced by `routeSetup` (relay-setup-router.ts) — against any
|
|
6
|
+
* {@link SetupFlowTransport}. All side effects (speech, call-store writes,
|
|
7
|
+
* completion) flow through injected deps so the flow is unit-testable and
|
|
8
|
+
* independent of any wire protocol.
|
|
9
|
+
*
|
|
10
|
+
* Handles `normal_call`, `deny`, the three verification actions
|
|
11
|
+
* (`verification`, `outbound_verification`, `callee_verification`),
|
|
12
|
+
* `invite_redemption`, `name_capture` (delegating the guardian-decision
|
|
13
|
+
* wait to {@link GuardianWaitController}), and `unverified_caller`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { randomInt } from "node:crypto";
|
|
17
|
+
|
|
18
|
+
import { getGuardianDeliveryFresh } from "../contacts/guardian-delivery-reader.js";
|
|
19
|
+
import type { TrustContext } from "../daemon/trust-context.js";
|
|
20
|
+
import type { addMessage as addMessageFn } from "../persistence/conversation-crud.js";
|
|
21
|
+
import { notifyGuardianOfAccessRequest as notifyGuardianOfAccessRequestImpl } from "../runtime/access-request-helper.js";
|
|
22
|
+
import {
|
|
23
|
+
resolveActorTrust,
|
|
24
|
+
toTrustContext,
|
|
25
|
+
} from "../runtime/actor-trust-resolver.js";
|
|
26
|
+
import {
|
|
27
|
+
trustContextFromVerdict,
|
|
28
|
+
verdictHasMemberIdentity,
|
|
29
|
+
verdictMemberUnresolvable,
|
|
30
|
+
} from "../runtime/trust-verdict-consumer.js";
|
|
31
|
+
import {
|
|
32
|
+
composeVerificationVoice,
|
|
33
|
+
GUARDIAN_VERIFY_TEMPLATE_KEYS,
|
|
34
|
+
} from "../runtime/verification-templates.js";
|
|
35
|
+
import { getLogger } from "../util/logger.js";
|
|
36
|
+
import { getTtsPlaybackDelayMs } from "./call-constants.js";
|
|
37
|
+
import type { postPointerMessageSafe as postPointerMessageSafeFn } from "./call-pointer-messages.js";
|
|
38
|
+
import type {
|
|
39
|
+
SetupFlowResult,
|
|
40
|
+
SetupFlowState,
|
|
41
|
+
SetupFlowTransport,
|
|
42
|
+
} from "./call-setup-flow-types.js";
|
|
43
|
+
import type { fireCallTranscriptNotifier as fireCallTranscriptNotifierFn } from "./call-state.js";
|
|
44
|
+
import type {
|
|
45
|
+
recordCallEvent as recordCallEventFn,
|
|
46
|
+
updateCallSession as updateCallSessionFn,
|
|
47
|
+
} from "./call-store.js";
|
|
48
|
+
import type { finalizeCall as finalizeCallFn } from "./finalize-call.js";
|
|
49
|
+
import {
|
|
50
|
+
GuardianWaitController,
|
|
51
|
+
type GuardianWaitControllerDeps,
|
|
52
|
+
type GuardianWaitDisposeReason,
|
|
53
|
+
type GuardianWaitResolutionContext,
|
|
54
|
+
} from "./guardian-wait-controller.js";
|
|
55
|
+
import { getPhoneCallerVerdict } from "./inbound-trust-reader.js";
|
|
56
|
+
import type { SetupOutcome, SetupResolved } from "./relay-setup-router.js";
|
|
57
|
+
import {
|
|
58
|
+
attemptInviteCodeRedemption as attemptInviteCodeRedemptionImpl,
|
|
59
|
+
attemptVerificationCode as attemptVerificationCodeImpl,
|
|
60
|
+
parseDigitsFromSpeech,
|
|
61
|
+
} from "./relay-verification.js";
|
|
62
|
+
|
|
63
|
+
const log = getLogger("call-setup-flow");
|
|
64
|
+
|
|
65
|
+
const INVITE_CODE_LENGTH = 6;
|
|
66
|
+
|
|
67
|
+
// 30-second name-capture window — enough time to speak a name but short
|
|
68
|
+
// enough to avoid keeping the call open for callers who never respond.
|
|
69
|
+
const NAME_CAPTURE_TIMEOUT_MS = 30_000;
|
|
70
|
+
|
|
71
|
+
// ── Errors ───────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
/** Thrown when `start()` receives a setup action the flow does not implement. */
|
|
74
|
+
export class UnsupportedSetupFlowError extends Error {
|
|
75
|
+
constructor(action: string) {
|
|
76
|
+
super(`Setup action '${action}' is not supported by CallSetupFlow`);
|
|
77
|
+
this.name = "UnsupportedSetupFlowError";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Dependencies ─────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
/** Structural subset of the call session that setup sub-flows read. */
|
|
84
|
+
export interface SetupFlowCallSession {
|
|
85
|
+
conversationId: string;
|
|
86
|
+
initiatedFromConversationId?: string | null;
|
|
87
|
+
toNumber?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Side-effect surface injected into the flow. Sub-flows extend this
|
|
92
|
+
* interface with additional accessors rather than reshaping the
|
|
93
|
+
* constructor.
|
|
94
|
+
*/
|
|
95
|
+
export interface CallSetupFlowDeps {
|
|
96
|
+
/** Speak a deterministic system prompt through the call transport. */
|
|
97
|
+
speakSystemPrompt(text: string): Promise<void>;
|
|
98
|
+
updateCallSession(
|
|
99
|
+
id: string,
|
|
100
|
+
updates: Parameters<typeof updateCallSessionFn>[1],
|
|
101
|
+
): void;
|
|
102
|
+
recordCallEvent(
|
|
103
|
+
callSessionId: string,
|
|
104
|
+
eventType: Parameters<typeof recordCallEventFn>[1],
|
|
105
|
+
payload?: Record<string, unknown>,
|
|
106
|
+
): void;
|
|
107
|
+
/** Invoked exactly once when the flow reaches a terminal result. */
|
|
108
|
+
onComplete(result: SetupFlowResult): void;
|
|
109
|
+
/**
|
|
110
|
+
* Delay between speaking terminal copy and ending the session.
|
|
111
|
+
* Defaults to the configured TTS playback delay.
|
|
112
|
+
*/
|
|
113
|
+
ttsPlaybackDelayMs?: number;
|
|
114
|
+
|
|
115
|
+
// ── Verification / invite sub-flow deps ────────────────────────────
|
|
116
|
+
// Optional so callers exercising only normal_call/deny need not supply
|
|
117
|
+
// them. Each sub-flow resolves the deps it needs up front at start()
|
|
118
|
+
// via requireDeps, which applies defaults for the pure logic and throws
|
|
119
|
+
// on a missing side-effect dep.
|
|
120
|
+
/** Look up the call session (used for its conversation/routing fields). */
|
|
121
|
+
getCallSession?(id: string): SetupFlowCallSession | null;
|
|
122
|
+
finalizeCall?: typeof finalizeCallFn;
|
|
123
|
+
addMessage?: typeof addMessageFn;
|
|
124
|
+
/** Fire-and-forget pointer write tolerating an evicted conversation. */
|
|
125
|
+
postPointerMessage?: typeof postPointerMessageSafeFn;
|
|
126
|
+
fireCallTranscriptNotifier?: typeof fireCallTranscriptNotifierFn;
|
|
127
|
+
/** Human-readable guardian label for prompts and handoff copy. */
|
|
128
|
+
resolveGuardianLabel?(): string;
|
|
129
|
+
/** Assistant display name, or null when unavailable. */
|
|
130
|
+
resolveAssistantLabel?(): string | null;
|
|
131
|
+
/** Defaults to {@link attemptVerificationCodeImpl} (relay-verification). */
|
|
132
|
+
attemptVerificationCode?: typeof attemptVerificationCodeImpl;
|
|
133
|
+
/** Gateway-native invite claim. Defaults to relay-verification's. */
|
|
134
|
+
attemptInviteCodeRedemption?: typeof attemptInviteCodeRedemptionImpl;
|
|
135
|
+
/**
|
|
136
|
+
* Re-resolve caller trust after a successful mid-setup activation
|
|
137
|
+
* (verdict-first with local fallback). Defaults to
|
|
138
|
+
* {@link resolveMidCallTrustContext}; errors fail soft to the
|
|
139
|
+
* setup-time trust.
|
|
140
|
+
*/
|
|
141
|
+
resolveMidCallTrustContext?(
|
|
142
|
+
assistantId: string,
|
|
143
|
+
fromNumber: string,
|
|
144
|
+
): Promise<TrustContext>;
|
|
145
|
+
|
|
146
|
+
// ── Name-capture sub-flow deps ──────────────────────────────────────
|
|
147
|
+
/**
|
|
148
|
+
* Create the canonical access request and notify the guardian. Defaults
|
|
149
|
+
* to access-request-helper's {@link notifyGuardianOfAccessRequestImpl}.
|
|
150
|
+
*/
|
|
151
|
+
notifyGuardianOfAccessRequest?: typeof notifyGuardianOfAccessRequestImpl;
|
|
152
|
+
/**
|
|
153
|
+
* Guardian wait controller factory, injectable so tests can substitute
|
|
154
|
+
* a fake. Defaults to constructing a real {@link GuardianWaitController}.
|
|
155
|
+
*/
|
|
156
|
+
createGuardianWaitController?(
|
|
157
|
+
callSessionId: string,
|
|
158
|
+
deps: GuardianWaitControllerDeps,
|
|
159
|
+
): GuardianWaitHandle;
|
|
160
|
+
/** Overrides the 30s name-capture response timeout. */
|
|
161
|
+
nameCaptureTimeoutMs?: number;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Structural subset of {@link GuardianWaitController} the flow drives. */
|
|
165
|
+
export type GuardianWaitHandle = Pick<
|
|
166
|
+
GuardianWaitController,
|
|
167
|
+
"start" | "handleTranscript" | "dispose"
|
|
168
|
+
>;
|
|
169
|
+
|
|
170
|
+
/** Verification deps with defaults applied and optionality removed. */
|
|
171
|
+
type VerificationDeps = Required<
|
|
172
|
+
Pick<
|
|
173
|
+
CallSetupFlowDeps,
|
|
174
|
+
| "getCallSession"
|
|
175
|
+
| "finalizeCall"
|
|
176
|
+
| "addMessage"
|
|
177
|
+
| "postPointerMessage"
|
|
178
|
+
| "fireCallTranscriptNotifier"
|
|
179
|
+
| "resolveGuardianLabel"
|
|
180
|
+
| "resolveAssistantLabel"
|
|
181
|
+
| "attemptVerificationCode"
|
|
182
|
+
| "resolveMidCallTrustContext"
|
|
183
|
+
>
|
|
184
|
+
>;
|
|
185
|
+
|
|
186
|
+
/** Invite-redemption deps with defaults applied and optionality removed. */
|
|
187
|
+
type InviteDeps = Required<
|
|
188
|
+
Pick<
|
|
189
|
+
CallSetupFlowDeps,
|
|
190
|
+
| "getCallSession"
|
|
191
|
+
| "finalizeCall"
|
|
192
|
+
| "fireCallTranscriptNotifier"
|
|
193
|
+
| "resolveGuardianLabel"
|
|
194
|
+
| "resolveAssistantLabel"
|
|
195
|
+
| "attemptInviteCodeRedemption"
|
|
196
|
+
| "resolveMidCallTrustContext"
|
|
197
|
+
>
|
|
198
|
+
>;
|
|
199
|
+
|
|
200
|
+
/** Name-capture deps with defaults applied and optionality removed. */
|
|
201
|
+
type NameCaptureDeps = Required<
|
|
202
|
+
Pick<
|
|
203
|
+
CallSetupFlowDeps,
|
|
204
|
+
| "getCallSession"
|
|
205
|
+
| "fireCallTranscriptNotifier"
|
|
206
|
+
| "resolveGuardianLabel"
|
|
207
|
+
| "resolveAssistantLabel"
|
|
208
|
+
| "notifyGuardianOfAccessRequest"
|
|
209
|
+
| "createGuardianWaitController"
|
|
210
|
+
| "resolveMidCallTrustContext"
|
|
211
|
+
>
|
|
212
|
+
>;
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Deps needed by the shared trusted-contact activation continuation —
|
|
216
|
+
* the common subset of {@link VerificationDeps} and {@link InviteDeps}.
|
|
217
|
+
*/
|
|
218
|
+
type ActivationDeps = Pick<
|
|
219
|
+
VerificationDeps,
|
|
220
|
+
| "getCallSession"
|
|
221
|
+
| "fireCallTranscriptNotifier"
|
|
222
|
+
| "resolveGuardianLabel"
|
|
223
|
+
| "resolveAssistantLabel"
|
|
224
|
+
| "resolveMidCallTrustContext"
|
|
225
|
+
>;
|
|
226
|
+
|
|
227
|
+
// ── Module helpers ───────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Real-implementation defaults for the optional pure-logic deps. Built per
|
|
231
|
+
* call so the module live bindings are read at resolution time.
|
|
232
|
+
*/
|
|
233
|
+
function setupFlowDepDefaults(): Partial<CallSetupFlowDeps> {
|
|
234
|
+
return {
|
|
235
|
+
attemptVerificationCode: attemptVerificationCodeImpl,
|
|
236
|
+
attemptInviteCodeRedemption: attemptInviteCodeRedemptionImpl,
|
|
237
|
+
notifyGuardianOfAccessRequest: notifyGuardianOfAccessRequestImpl,
|
|
238
|
+
resolveMidCallTrustContext,
|
|
239
|
+
createGuardianWaitController: (callSessionId, deps) =>
|
|
240
|
+
new GuardianWaitController(callSessionId, deps),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Resolve a sub-flow's dep subset, applying {@link setupFlowDepDefaults}
|
|
246
|
+
* for omitted pure-logic deps and throwing a descriptive error naming any
|
|
247
|
+
* missing side-effect dep.
|
|
248
|
+
*/
|
|
249
|
+
function requireDeps<K extends keyof CallSetupFlowDeps>(
|
|
250
|
+
kind: string,
|
|
251
|
+
deps: CallSetupFlowDeps,
|
|
252
|
+
names: readonly K[],
|
|
253
|
+
): Required<Pick<CallSetupFlowDeps, K>> {
|
|
254
|
+
const defaults = setupFlowDepDefaults();
|
|
255
|
+
const resolved: Partial<Record<keyof CallSetupFlowDeps, unknown>> = {};
|
|
256
|
+
const missing: string[] = [];
|
|
257
|
+
for (const name of names) {
|
|
258
|
+
const value = deps[name] ?? defaults[name];
|
|
259
|
+
if (value == null) {
|
|
260
|
+
missing.push(name);
|
|
261
|
+
} else {
|
|
262
|
+
resolved[name] = value;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (missing.length > 0) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
`CallSetupFlow ${kind} deps missing: ${missing.join(", ")}`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
return resolved as Required<Pick<CallSetupFlowDeps, K>>;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Re-resolve caller trust after a mid-setup verification/activation. Prefers
|
|
275
|
+
* the gateway verdict (authoritative right after the gateway updated the
|
|
276
|
+
* binding); falls back to local resolution on a missing/failed/unusable
|
|
277
|
+
* verdict so a blip never drops the call. Mirrors the setup path's
|
|
278
|
+
* verdict-first-with-fallback condition.
|
|
279
|
+
*/
|
|
280
|
+
export async function resolveMidCallTrustContext(
|
|
281
|
+
assistantId: string,
|
|
282
|
+
fromNumber: string,
|
|
283
|
+
): Promise<TrustContext> {
|
|
284
|
+
const verdict = await getPhoneCallerVerdict(fromNumber);
|
|
285
|
+
|
|
286
|
+
// Only a MEMBERLESS unknown verdict is treated as a stale gateway view and
|
|
287
|
+
// falls back to local: the caller was just activated, and invite redemption
|
|
288
|
+
// writes the channel assistant-side, so the gateway may not see the member
|
|
289
|
+
// yet — local resolution has it. A MEMBERFUL unknown verdict (blocked/revoked
|
|
290
|
+
// member, carrying contactId/channelId) is honored so its deny ACL is
|
|
291
|
+
// enforced; falling back could lose the gateway's member status if local
|
|
292
|
+
// state is stale.
|
|
293
|
+
const memberlessUnknown =
|
|
294
|
+
verdict?.trustClass === "unknown" && !verdictHasMemberIdentity(verdict);
|
|
295
|
+
const usable =
|
|
296
|
+
verdict &&
|
|
297
|
+
!verdict.resolutionFailed &&
|
|
298
|
+
!verdictMemberUnresolvable(verdict) &&
|
|
299
|
+
!memberlessUnknown;
|
|
300
|
+
|
|
301
|
+
if (usable) {
|
|
302
|
+
return trustContextFromVerdict(verdict, {
|
|
303
|
+
sourceChannel: "phone",
|
|
304
|
+
conversationExternalId: fromNumber,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Warm the phone-channel guardian-delivery cache before the SYNC
|
|
309
|
+
// resolveActorTrust fallback, which reads the IO-free per-channel snapshot
|
|
310
|
+
// that daemon startup leaves cold for `phone`. Read fresh: gateway-side
|
|
311
|
+
// binding writes don't invalidate the daemon cache, so a stale empty
|
|
312
|
+
// snapshot would otherwise survive the TTL and misclassify the guardian.
|
|
313
|
+
await getGuardianDeliveryFresh({ channelTypes: ["phone"] });
|
|
314
|
+
|
|
315
|
+
return toTrustContext(
|
|
316
|
+
resolveActorTrust({
|
|
317
|
+
assistantId,
|
|
318
|
+
sourceChannel: "phone",
|
|
319
|
+
conversationExternalId: fromNumber,
|
|
320
|
+
actorExternalId: fromNumber,
|
|
321
|
+
}),
|
|
322
|
+
fromNumber,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Return the first whitespace-delimited token of a name, or `null` when the
|
|
328
|
+
* input is null/blank. Used for greetings so "Alice Example" -> "Alice".
|
|
329
|
+
*/
|
|
330
|
+
function firstToken(name: string | null | undefined): string | null {
|
|
331
|
+
if (!name) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
const trimmed = name.trim();
|
|
335
|
+
if (!trimmed) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
return trimmed.split(/\s+/)[0] ?? null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ── Flow ─────────────────────────────────────────────────────────────
|
|
342
|
+
|
|
343
|
+
type VerificationMode =
|
|
344
|
+
| "inbound_verification"
|
|
345
|
+
| "outbound_verification"
|
|
346
|
+
| "callee_verification";
|
|
347
|
+
|
|
348
|
+
/** Sub-flows that collect a numeric code from the caller. */
|
|
349
|
+
type CodeCollectionMode = VerificationMode | "invite_redemption";
|
|
350
|
+
|
|
351
|
+
interface InviteRedemptionState {
|
|
352
|
+
assistantId: string;
|
|
353
|
+
fromNumber: string;
|
|
354
|
+
inviteeName: string | null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
interface AccessRequestState {
|
|
358
|
+
assistantId: string;
|
|
359
|
+
fromNumber: string;
|
|
360
|
+
callerName: string | null;
|
|
361
|
+
requestId: string | null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export class CallSetupFlow {
|
|
365
|
+
private state: SetupFlowState = "idle";
|
|
366
|
+
/** Set before dispatch so `start()` runs at most once per instance. */
|
|
367
|
+
private started = false;
|
|
368
|
+
|
|
369
|
+
// ── Code-collection state (shared by verification + invite) ────────
|
|
370
|
+
private codeMode: CodeCollectionMode | null = null;
|
|
371
|
+
/** Shared digit buffer for code collection (DTMF + spoken digits). */
|
|
372
|
+
private digitBuffer = "";
|
|
373
|
+
private codeLength = 6;
|
|
374
|
+
/**
|
|
375
|
+
* In-flight guard shared by every code-collection sub-flow: async
|
|
376
|
+
* validation reads and advances the attempt counter, so digits and
|
|
377
|
+
* spoken codes arriving while one submission is pending are dropped
|
|
378
|
+
* rather than launching a parallel attempt against the same counter.
|
|
379
|
+
* Set synchronously before dispatch and cleared when the attempt
|
|
380
|
+
* settles.
|
|
381
|
+
*/
|
|
382
|
+
private codeSubmissionInFlight = false;
|
|
383
|
+
/** Setup-time trust, kept as the fallback when re-resolution fails. */
|
|
384
|
+
private initialTrustContext: TrustContext | null = null;
|
|
385
|
+
private trustReResolving = false;
|
|
386
|
+
private deferredTranscripts: string[] = [];
|
|
387
|
+
|
|
388
|
+
// ── Verification sub-flow state ─────────────────────────────────────
|
|
389
|
+
private vdeps: VerificationDeps | null = null;
|
|
390
|
+
private verificationAttempts = 0;
|
|
391
|
+
private verificationMaxAttempts = 3;
|
|
392
|
+
private verificationAssistantId = "";
|
|
393
|
+
private verificationFromNumber = "";
|
|
394
|
+
private outboundVerificationSessionId: string | null = null;
|
|
395
|
+
private calleeVerificationCode: string | null = null;
|
|
396
|
+
|
|
397
|
+
// ── Invite-redemption sub-flow state ────────────────────────────────
|
|
398
|
+
private ideps: InviteDeps | null = null;
|
|
399
|
+
private invite: InviteRedemptionState | null = null;
|
|
400
|
+
|
|
401
|
+
// ── Name-capture / guardian-wait sub-flow state ─────────────────────
|
|
402
|
+
private ndeps: NameCaptureDeps | null = null;
|
|
403
|
+
private accessRequest: AccessRequestState | null = null;
|
|
404
|
+
private nameCaptureTimer: ReturnType<typeof setTimeout> | null = null;
|
|
405
|
+
/**
|
|
406
|
+
* Busy guard: access-request creation and the capture-timeout teardown
|
|
407
|
+
* are async, and a final transcript arriving while either is pending
|
|
408
|
+
* must not be treated as a (second) name and fire a duplicate guardian
|
|
409
|
+
* notification.
|
|
410
|
+
*/
|
|
411
|
+
private nameCaptureBusy = false;
|
|
412
|
+
private guardianWait: GuardianWaitHandle | null = null;
|
|
413
|
+
|
|
414
|
+
// ── Terminal bookkeeping ────────────────────────────────────────────
|
|
415
|
+
private endSessionTimer: ReturnType<typeof setTimeout> | null = null;
|
|
416
|
+
private finalized = false;
|
|
417
|
+
private disposed = false;
|
|
418
|
+
|
|
419
|
+
constructor(
|
|
420
|
+
private readonly callSessionId: string,
|
|
421
|
+
private readonly transport: SetupFlowTransport,
|
|
422
|
+
private readonly deps: CallSetupFlowDeps,
|
|
423
|
+
) {}
|
|
424
|
+
|
|
425
|
+
/** Explicit flow state — never inferred from the transport. */
|
|
426
|
+
getState(): SetupFlowState {
|
|
427
|
+
return this.state;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Whether a flow-terminal path already ran `finalizeCall`. The owning
|
|
432
|
+
* server's transport-close handler checks this to keep finalization
|
|
433
|
+
* exactly-once across flow-terminal and transport-close paths.
|
|
434
|
+
*/
|
|
435
|
+
hasFinalized(): boolean {
|
|
436
|
+
return this.finalized;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Tear down a flow whose transport disconnected mid-setup: clears the
|
|
441
|
+
* name-capture and end-session timers and disposes the guardian wait
|
|
442
|
+
* controller (which emits the callback handoff when the caller opted in
|
|
443
|
+
* and the reason is `transport_closed`). Idempotent; never fires
|
|
444
|
+
* `onComplete`.
|
|
445
|
+
*/
|
|
446
|
+
dispose(reason: GuardianWaitDisposeReason = "teardown"): void {
|
|
447
|
+
if (this.disposed) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
this.disposed = true;
|
|
451
|
+
this.clearNameCaptureTimer();
|
|
452
|
+
if (this.endSessionTimer) {
|
|
453
|
+
clearTimeout(this.endSessionTimer);
|
|
454
|
+
this.endSessionTimer = null;
|
|
455
|
+
}
|
|
456
|
+
this.guardianWait?.dispose(reason);
|
|
457
|
+
// Stop accepting input without emitting a terminal result — the
|
|
458
|
+
// transport is gone, so there is no continuation to run.
|
|
459
|
+
this.state = "completed";
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Act on a routing outcome. Runs at most once per flow instance;
|
|
464
|
+
* terminal continuation is delivered via `deps.onComplete`.
|
|
465
|
+
*/
|
|
466
|
+
async start(outcome: SetupOutcome, resolved: SetupResolved): Promise<void> {
|
|
467
|
+
// Mark started before dispatching: async paths (e.g. deny awaits TTS)
|
|
468
|
+
// stay in `idle` state until their terminal continuation, so the state
|
|
469
|
+
// check alone would admit a concurrent second start().
|
|
470
|
+
if (this.started || this.state !== "idle") {
|
|
471
|
+
throw new Error("CallSetupFlow.start() may only be called once");
|
|
472
|
+
}
|
|
473
|
+
this.started = true;
|
|
474
|
+
|
|
475
|
+
switch (outcome.action) {
|
|
476
|
+
case "normal_call":
|
|
477
|
+
this.complete({
|
|
478
|
+
kind: "proceed-initial-greeting",
|
|
479
|
+
assistantId: resolved.assistantId,
|
|
480
|
+
trustContext: toTrustContext(
|
|
481
|
+
resolved.actorTrust,
|
|
482
|
+
resolved.otherPartyNumber,
|
|
483
|
+
),
|
|
484
|
+
});
|
|
485
|
+
return;
|
|
486
|
+
|
|
487
|
+
case "deny":
|
|
488
|
+
await this.runDeny(outcome, resolved);
|
|
489
|
+
return;
|
|
490
|
+
|
|
491
|
+
case "verification":
|
|
492
|
+
this.startInboundVerification(
|
|
493
|
+
outcome.assistantId,
|
|
494
|
+
outcome.fromNumber,
|
|
495
|
+
resolved,
|
|
496
|
+
);
|
|
497
|
+
return;
|
|
498
|
+
|
|
499
|
+
case "outbound_verification":
|
|
500
|
+
this.startOutboundVerification(
|
|
501
|
+
outcome.assistantId,
|
|
502
|
+
outcome.sessionId,
|
|
503
|
+
outcome.toNumber,
|
|
504
|
+
resolved,
|
|
505
|
+
);
|
|
506
|
+
return;
|
|
507
|
+
|
|
508
|
+
case "callee_verification":
|
|
509
|
+
await this.startCalleeVerification(
|
|
510
|
+
outcome.verificationConfig,
|
|
511
|
+
resolved,
|
|
512
|
+
);
|
|
513
|
+
return;
|
|
514
|
+
|
|
515
|
+
case "invite_redemption":
|
|
516
|
+
this.startInviteRedemption(outcome, resolved);
|
|
517
|
+
return;
|
|
518
|
+
|
|
519
|
+
case "name_capture":
|
|
520
|
+
this.startNameCapture(outcome, resolved);
|
|
521
|
+
return;
|
|
522
|
+
|
|
523
|
+
case "unverified_caller":
|
|
524
|
+
await this.runUnverifiedCaller(outcome);
|
|
525
|
+
return;
|
|
526
|
+
|
|
527
|
+
default:
|
|
528
|
+
throw new UnsupportedSetupFlowError((outcome as SetupOutcome).action);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// ── Caller input ────────────────────────────────────────────────────
|
|
533
|
+
|
|
534
|
+
/** Feed a DTMF digit to the active sub-flow. No-op while idle/completed. */
|
|
535
|
+
pushDtmfDigit(digit: string): void {
|
|
536
|
+
if (!this.acceptsInput()) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
// codeMode is null during name capture and the guardian wait, so DTMF
|
|
540
|
+
// is ignored in those states.
|
|
541
|
+
if (this.codeMode == null) {
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
// Digits arriving while a submitted code is still validating are
|
|
545
|
+
// dropped — buffering them would launch a parallel attempt.
|
|
546
|
+
if (this.codeSubmissionInFlight) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
this.digitBuffer += digit;
|
|
550
|
+
if (this.digitBuffer.length < this.codeLength) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const enteredCode = this.digitBuffer.slice(0, this.codeLength);
|
|
554
|
+
this.digitBuffer = "";
|
|
555
|
+
this.submitCode(enteredCode);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** Feed a final caller transcript to the active sub-flow. No-op while idle/completed. */
|
|
559
|
+
pushTranscriptFinal(text: string): void {
|
|
560
|
+
if (!this.acceptsInput()) {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
// Defer (don't drop) transcripts while trust is being re-resolved so a
|
|
564
|
+
// verified caller's first utterance runs under the upgraded context, not
|
|
565
|
+
// the stale pre-verification one. The drained buffer rides the terminal
|
|
566
|
+
// result for the owning server to replay in order.
|
|
567
|
+
if (this.trustReResolving) {
|
|
568
|
+
this.deferredTranscripts.push(text);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
// During name capture, the caller's response is their name. A blank
|
|
572
|
+
// transcript (silence/noise) keeps waiting; the capture timeout still
|
|
573
|
+
// fires if the caller never provides one.
|
|
574
|
+
if (this.state === "capturing_name") {
|
|
575
|
+
const callerName = text.trim();
|
|
576
|
+
if (!callerName || this.nameCaptureBusy) {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
log.info(
|
|
580
|
+
{ callSessionId: this.callSessionId, callerName },
|
|
581
|
+
"Name captured from unknown inbound caller",
|
|
582
|
+
);
|
|
583
|
+
this.nameCaptureBusy = true;
|
|
584
|
+
void this.handleNameCaptureResponse(callerName)
|
|
585
|
+
.catch((err) =>
|
|
586
|
+
log.error(
|
|
587
|
+
{ err, callSessionId: this.callSessionId },
|
|
588
|
+
"Name capture handling failed",
|
|
589
|
+
),
|
|
590
|
+
)
|
|
591
|
+
.finally(() => {
|
|
592
|
+
this.nameCaptureBusy = false;
|
|
593
|
+
});
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
// During the guardian decision wait, caller speech is classified by the
|
|
597
|
+
// wait controller (reassurance, impatience, callback offer/opt-in).
|
|
598
|
+
if (this.state === "awaiting_guardian_decision") {
|
|
599
|
+
this.guardianWait?.handleTranscript(text);
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (this.codeMode == null) {
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
// Spoken codes arriving while a submitted code is still validating
|
|
606
|
+
// are dropped — they would launch a parallel attempt.
|
|
607
|
+
if (this.codeSubmissionInFlight) {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
// Callee verification is DTMF-only — the callee should be entering
|
|
611
|
+
// digits on their keypad, not speaking.
|
|
612
|
+
if (this.codeMode === "callee_verification") {
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const spokenDigits = parseDigitsFromSpeech(text);
|
|
616
|
+
if (spokenDigits.length >= this.codeLength) {
|
|
617
|
+
this.submitCode(spokenDigits.slice(0, this.codeLength));
|
|
618
|
+
} else if (spokenDigits.length > 0) {
|
|
619
|
+
void this.deps.speakSystemPrompt(
|
|
620
|
+
`I heard ${spokenDigits.length} digits. Please enter all ${this.codeLength} digits of your code.`,
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// ── Sub-flows ───────────────────────────────────────────────────────
|
|
626
|
+
|
|
627
|
+
/** Deny the call: record the ACL event, speak the denial, then hang up. */
|
|
628
|
+
private async runDeny(
|
|
629
|
+
outcome: Extract<SetupOutcome, { action: "deny" }>,
|
|
630
|
+
resolved: SetupResolved,
|
|
631
|
+
): Promise<void> {
|
|
632
|
+
this.deps.recordCallEvent(this.callSessionId, "inbound_acl_denied", {
|
|
633
|
+
from: resolved.otherPartyNumber,
|
|
634
|
+
trustClass: resolved.actorTrust.trustClass,
|
|
635
|
+
channelId: resolved.actorTrust.memberRecord?.channel.id,
|
|
636
|
+
memberPolicy: resolved.actorTrust.memberRecord?.policy,
|
|
637
|
+
});
|
|
638
|
+
this.deps.updateCallSession(this.callSessionId, {
|
|
639
|
+
status: "failed",
|
|
640
|
+
endedAt: Date.now(),
|
|
641
|
+
lastError: outcome.logReason,
|
|
642
|
+
});
|
|
643
|
+
await this.deps.speakSystemPrompt(outcome.message);
|
|
644
|
+
this.endSessionAfterPlayback(outcome.logReason);
|
|
645
|
+
this.complete({ kind: "ended", reason: outcome.logReason });
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// ── Verification sub-flows ──────────────────────────────────────────
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Inbound guardian / trusted-contact verification: the caller has a
|
|
652
|
+
* pending voice challenge and must enter (or speak) their six-digit code.
|
|
653
|
+
*/
|
|
654
|
+
private startInboundVerification(
|
|
655
|
+
assistantId: string,
|
|
656
|
+
fromNumber: string,
|
|
657
|
+
resolved: SetupResolved,
|
|
658
|
+
): void {
|
|
659
|
+
this.enterCodeCollection("inbound_verification", resolved, {
|
|
660
|
+
maxAttempts: 3,
|
|
661
|
+
codeLength: 6,
|
|
662
|
+
});
|
|
663
|
+
this.verificationAssistantId = assistantId;
|
|
664
|
+
this.verificationFromNumber = fromNumber;
|
|
665
|
+
|
|
666
|
+
this.deps.recordCallEvent(
|
|
667
|
+
this.callSessionId,
|
|
668
|
+
"voice_verification_started",
|
|
669
|
+
{
|
|
670
|
+
assistantId,
|
|
671
|
+
maxAttempts: this.verificationMaxAttempts,
|
|
672
|
+
},
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
void this.deps.speakSystemPrompt(
|
|
676
|
+
"Welcome. Please enter your six-digit verification code using your keypad, or speak the digits now.",
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Outbound guardian verification: the system called the guardian's phone;
|
|
682
|
+
* prompt them to enter the verification code via DTMF or speech.
|
|
683
|
+
*/
|
|
684
|
+
private startOutboundVerification(
|
|
685
|
+
assistantId: string,
|
|
686
|
+
verificationSessionId: string,
|
|
687
|
+
toNumber: string,
|
|
688
|
+
resolved: SetupResolved,
|
|
689
|
+
): void {
|
|
690
|
+
this.enterCodeCollection("outbound_verification", resolved, {
|
|
691
|
+
maxAttempts: 3,
|
|
692
|
+
codeLength: 6,
|
|
693
|
+
});
|
|
694
|
+
this.verificationAssistantId = assistantId;
|
|
695
|
+
// For outbound guardian calls, the "to" number is the guardian's phone.
|
|
696
|
+
this.verificationFromNumber = toNumber;
|
|
697
|
+
this.outboundVerificationSessionId = verificationSessionId;
|
|
698
|
+
|
|
699
|
+
this.deps.recordCallEvent(
|
|
700
|
+
this.callSessionId,
|
|
701
|
+
"outbound_voice_verification_started",
|
|
702
|
+
{
|
|
703
|
+
assistantId,
|
|
704
|
+
verificationSessionId,
|
|
705
|
+
maxAttempts: this.verificationMaxAttempts,
|
|
706
|
+
},
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
void this.deps.speakSystemPrompt(
|
|
710
|
+
composeVerificationVoice(GUARDIAN_VERIFY_TEMPLATE_KEYS.VOICE_CALL_INTRO, {
|
|
711
|
+
codeDigits: this.codeLength,
|
|
712
|
+
}),
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Outbound callee verification: generate a random code, post it to the
|
|
718
|
+
* initiating conversation so the guardian can share it with the callee,
|
|
719
|
+
* and prompt the callee to enter it. DTMF-only — speech is ignored.
|
|
720
|
+
*/
|
|
721
|
+
private async startCalleeVerification(
|
|
722
|
+
verificationConfig: { maxAttempts: number; codeLength: number },
|
|
723
|
+
resolved: SetupResolved,
|
|
724
|
+
): Promise<void> {
|
|
725
|
+
const vdeps = this.enterCodeCollection(
|
|
726
|
+
"callee_verification",
|
|
727
|
+
resolved,
|
|
728
|
+
verificationConfig,
|
|
729
|
+
);
|
|
730
|
+
this.verificationAssistantId = resolved.assistantId;
|
|
731
|
+
|
|
732
|
+
const maxValue = Math.pow(10, this.codeLength);
|
|
733
|
+
const code = randomInt(0, maxValue)
|
|
734
|
+
.toString()
|
|
735
|
+
.padStart(this.codeLength, "0");
|
|
736
|
+
this.calleeVerificationCode = code;
|
|
737
|
+
|
|
738
|
+
this.deps.recordCallEvent(
|
|
739
|
+
this.callSessionId,
|
|
740
|
+
"callee_verification_started",
|
|
741
|
+
{
|
|
742
|
+
codeLength: this.codeLength,
|
|
743
|
+
maxAttempts: this.verificationMaxAttempts,
|
|
744
|
+
},
|
|
745
|
+
);
|
|
746
|
+
|
|
747
|
+
const spokenCode = code.split("").join(". ");
|
|
748
|
+
void this.deps.speakSystemPrompt(
|
|
749
|
+
`Please enter the verification code: ${spokenCode}.`,
|
|
750
|
+
);
|
|
751
|
+
|
|
752
|
+
// Post the verification code to the initiating conversation so the
|
|
753
|
+
// guardian (user) can share it with the callee.
|
|
754
|
+
const session = vdeps.getCallSession(this.callSessionId);
|
|
755
|
+
if (session?.initiatedFromConversationId) {
|
|
756
|
+
const codeMsg = `\u{1F510} Verification code for call to ${session.toNumber}: ${code}`;
|
|
757
|
+
await vdeps.addMessage(
|
|
758
|
+
session.initiatedFromConversationId,
|
|
759
|
+
"assistant",
|
|
760
|
+
JSON.stringify([{ type: "text", text: codeMsg }]),
|
|
761
|
+
{
|
|
762
|
+
metadata: {
|
|
763
|
+
userMessageChannel: "phone",
|
|
764
|
+
assistantMessageChannel: "phone",
|
|
765
|
+
userMessageInterface: "phone",
|
|
766
|
+
assistantMessageInterface: "phone",
|
|
767
|
+
},
|
|
768
|
+
},
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/** Shared entry bookkeeping for the verification code-collection sub-flows. */
|
|
774
|
+
private enterCodeCollection(
|
|
775
|
+
mode: VerificationMode,
|
|
776
|
+
resolved: SetupResolved,
|
|
777
|
+
config: { maxAttempts: number; codeLength: number },
|
|
778
|
+
): VerificationDeps {
|
|
779
|
+
const vdeps = this.requireVerificationDeps();
|
|
780
|
+
this.vdeps = vdeps;
|
|
781
|
+
this.codeMode = mode;
|
|
782
|
+
this.verificationAttempts = 0;
|
|
783
|
+
this.verificationMaxAttempts = config.maxAttempts;
|
|
784
|
+
this.codeLength = config.codeLength;
|
|
785
|
+
this.digitBuffer = "";
|
|
786
|
+
this.initialTrustContext = toTrustContext(
|
|
787
|
+
resolved.actorTrust,
|
|
788
|
+
resolved.otherPartyNumber,
|
|
789
|
+
);
|
|
790
|
+
this.state = "collecting_code";
|
|
791
|
+
return vdeps;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/** Route a fully collected code to the active code-collection sub-flow. */
|
|
795
|
+
private submitCode(enteredCode: string): void {
|
|
796
|
+
this.codeSubmissionInFlight = true;
|
|
797
|
+
const handler =
|
|
798
|
+
this.codeMode === "invite_redemption"
|
|
799
|
+
? this.handleInviteCodeEntry(enteredCode)
|
|
800
|
+
: this.codeMode === "callee_verification"
|
|
801
|
+
? this.handleCalleeCode(enteredCode)
|
|
802
|
+
: this.handleVerificationCode(enteredCode);
|
|
803
|
+
handler
|
|
804
|
+
.catch((err) =>
|
|
805
|
+
log.error(
|
|
806
|
+
{ err, callSessionId: this.callSessionId },
|
|
807
|
+
"Setup code handling failed",
|
|
808
|
+
),
|
|
809
|
+
)
|
|
810
|
+
.finally(() => {
|
|
811
|
+
this.codeSubmissionInFlight = false;
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Validate an entered code against the pending voice guardian challenge
|
|
817
|
+
* (inbound and outbound guardian verification).
|
|
818
|
+
*/
|
|
819
|
+
private async handleVerificationCode(enteredCode: string): Promise<void> {
|
|
820
|
+
const vdeps = this.vdeps;
|
|
821
|
+
if (!vdeps) {
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
const isOutbound = this.outboundVerificationSessionId != null;
|
|
825
|
+
const assistantId = this.verificationAssistantId;
|
|
826
|
+
const fromNumber = this.verificationFromNumber;
|
|
827
|
+
|
|
828
|
+
const result = await vdeps.attemptVerificationCode({
|
|
829
|
+
verificationAssistantId: assistantId,
|
|
830
|
+
verificationFromNumber: fromNumber,
|
|
831
|
+
enteredCode,
|
|
832
|
+
isOutbound,
|
|
833
|
+
codeDigits: this.codeLength,
|
|
834
|
+
verificationAttempts: this.verificationAttempts,
|
|
835
|
+
verificationMaxAttempts: this.verificationMaxAttempts,
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
// A concurrent attempt may have already reached a terminal result.
|
|
839
|
+
if (this.state === "completed") {
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
if (result.outcome === "success") {
|
|
844
|
+
this.codeMode = null;
|
|
845
|
+
this.digitBuffer = "";
|
|
846
|
+
this.verificationAttempts = 0;
|
|
847
|
+
|
|
848
|
+
this.deps.recordCallEvent(this.callSessionId, result.eventName, {
|
|
849
|
+
verificationType: result.verificationType,
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
if (isOutbound) {
|
|
853
|
+
await this.completeOutboundVerificationSuccess(
|
|
854
|
+
vdeps,
|
|
855
|
+
assistantId,
|
|
856
|
+
fromNumber,
|
|
857
|
+
);
|
|
858
|
+
} else if (result.verificationType === "trusted_contact") {
|
|
859
|
+
await this.continueAfterTrustedContactActivation(vdeps, {
|
|
860
|
+
assistantId,
|
|
861
|
+
fromNumber,
|
|
862
|
+
activationReason: "trusted_contact_verified",
|
|
863
|
+
});
|
|
864
|
+
} else {
|
|
865
|
+
// Inbound guardian verification — proceed to the normal call flow
|
|
866
|
+
// under the upgraded trust context.
|
|
867
|
+
const trustContext = await this.resolveTrustWithDeferral(
|
|
868
|
+
vdeps,
|
|
869
|
+
assistantId,
|
|
870
|
+
fromNumber,
|
|
871
|
+
);
|
|
872
|
+
this.complete({
|
|
873
|
+
kind: "proceed-initial-greeting",
|
|
874
|
+
assistantId,
|
|
875
|
+
trustContext,
|
|
876
|
+
deferredTranscripts: this.drainDeferredTranscripts(),
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
if (result.outcome === "failure") {
|
|
883
|
+
this.codeMode = null;
|
|
884
|
+
this.verificationAttempts = result.attempts;
|
|
885
|
+
|
|
886
|
+
this.deps.recordCallEvent(this.callSessionId, result.eventName, {
|
|
887
|
+
attempts: result.attempts,
|
|
888
|
+
});
|
|
889
|
+
this.deps.updateCallSession(this.callSessionId, {
|
|
890
|
+
status: "failed",
|
|
891
|
+
endedAt: Date.now(),
|
|
892
|
+
lastError: "Guardian voice verification failed — max attempts exceeded",
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
const session = vdeps.getCallSession(this.callSessionId);
|
|
896
|
+
if (session) {
|
|
897
|
+
this.finalizeOnce(vdeps.finalizeCall, session.conversationId);
|
|
898
|
+
|
|
899
|
+
if (isOutbound && session.initiatedFromConversationId) {
|
|
900
|
+
vdeps.postPointerMessage(
|
|
901
|
+
session.initiatedFromConversationId,
|
|
902
|
+
"verification_failed",
|
|
903
|
+
session.toNumber ?? "",
|
|
904
|
+
{ channel: "phone", reason: "Max verification attempts exceeded" },
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
await this.deps.speakSystemPrompt(result.ttsMessage);
|
|
910
|
+
this.endSessionAfterPlayback("Verification failed — challenge rejected");
|
|
911
|
+
this.complete({
|
|
912
|
+
kind: "ended",
|
|
913
|
+
reason: "Verification failed — challenge rejected",
|
|
914
|
+
});
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Retry — re-prompt and keep collecting.
|
|
919
|
+
this.verificationAttempts = result.attempt;
|
|
920
|
+
void this.deps.speakSystemPrompt(result.ttsMessage);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/** Compare an entered code against the generated callee verification code. */
|
|
924
|
+
private async handleCalleeCode(enteredCode: string): Promise<void> {
|
|
925
|
+
const vdeps = this.vdeps;
|
|
926
|
+
if (!vdeps || this.calleeVerificationCode == null) {
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (enteredCode === this.calleeVerificationCode) {
|
|
931
|
+
this.codeMode = null;
|
|
932
|
+
this.calleeVerificationCode = null;
|
|
933
|
+
this.verificationAttempts = 0;
|
|
934
|
+
|
|
935
|
+
this.deps.recordCallEvent(
|
|
936
|
+
this.callSessionId,
|
|
937
|
+
"callee_verification_succeeded",
|
|
938
|
+
{},
|
|
939
|
+
);
|
|
940
|
+
this.complete({
|
|
941
|
+
kind: "proceed-initial-greeting",
|
|
942
|
+
assistantId: this.verificationAssistantId,
|
|
943
|
+
trustContext: this.initialTrustContext!,
|
|
944
|
+
});
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
this.verificationAttempts++;
|
|
949
|
+
|
|
950
|
+
if (this.verificationAttempts >= this.verificationMaxAttempts) {
|
|
951
|
+
this.codeMode = null;
|
|
952
|
+
|
|
953
|
+
this.deps.recordCallEvent(
|
|
954
|
+
this.callSessionId,
|
|
955
|
+
"callee_verification_failed",
|
|
956
|
+
{
|
|
957
|
+
attempts: this.verificationAttempts,
|
|
958
|
+
},
|
|
959
|
+
);
|
|
960
|
+
// Mark failed immediately so a transport close during the goodbye TTS
|
|
961
|
+
// window cannot race this into a terminal "completed" status.
|
|
962
|
+
this.deps.updateCallSession(this.callSessionId, {
|
|
963
|
+
status: "failed",
|
|
964
|
+
endedAt: Date.now(),
|
|
965
|
+
lastError: "Callee verification failed — max attempts exceeded",
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
const session = vdeps.getCallSession(this.callSessionId);
|
|
969
|
+
if (session) {
|
|
970
|
+
this.finalizeOnce(vdeps.finalizeCall, session.conversationId);
|
|
971
|
+
if (session.initiatedFromConversationId) {
|
|
972
|
+
vdeps.postPointerMessage(
|
|
973
|
+
session.initiatedFromConversationId,
|
|
974
|
+
"failed",
|
|
975
|
+
session.toNumber ?? "",
|
|
976
|
+
{ reason: "Callee verification failed" },
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Wait for synthesis to complete before starting the teardown timer
|
|
982
|
+
// so the caller hears the goodbye message.
|
|
983
|
+
try {
|
|
984
|
+
await this.deps.speakSystemPrompt("Verification failed. Goodbye.");
|
|
985
|
+
} catch (err) {
|
|
986
|
+
log.error(
|
|
987
|
+
{ err, callSessionId: this.callSessionId },
|
|
988
|
+
"System prompt TTS failed during verification teardown",
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
this.endSessionAfterPlayback("Verification failed");
|
|
992
|
+
this.complete({ kind: "ended", reason: "Verification failed" });
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
void this.deps.speakSystemPrompt(
|
|
997
|
+
"That code was incorrect. Please try again.",
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Outbound guardian verification success: pointer back to the originating
|
|
1003
|
+
* conversation, trust upgrade, session to in_progress, then hand off to
|
|
1004
|
+
* the post-verification greeting.
|
|
1005
|
+
*/
|
|
1006
|
+
private async completeOutboundVerificationSuccess(
|
|
1007
|
+
vdeps: VerificationDeps,
|
|
1008
|
+
assistantId: string,
|
|
1009
|
+
fromNumber: string,
|
|
1010
|
+
): Promise<void> {
|
|
1011
|
+
const session = vdeps.getCallSession(this.callSessionId);
|
|
1012
|
+
if (session?.initiatedFromConversationId) {
|
|
1013
|
+
vdeps.postPointerMessage(
|
|
1014
|
+
session.initiatedFromConversationId,
|
|
1015
|
+
"verification_succeeded",
|
|
1016
|
+
session.toNumber ?? "",
|
|
1017
|
+
{ channel: "phone" },
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
const trustContext = await this.resolveTrustWithDeferral(
|
|
1022
|
+
vdeps,
|
|
1023
|
+
assistantId,
|
|
1024
|
+
fromNumber,
|
|
1025
|
+
);
|
|
1026
|
+
// Same teardown race as the activation handoff: don't resurrect a
|
|
1027
|
+
// session whose transport closed during trust re-resolution.
|
|
1028
|
+
if (this.state === "completed") {
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
this.deps.updateCallSession(this.callSessionId, { status: "in_progress" });
|
|
1032
|
+
this.complete({
|
|
1033
|
+
kind: "proceed-post-verification-greeting",
|
|
1034
|
+
assistantId,
|
|
1035
|
+
trustContext,
|
|
1036
|
+
deferredTranscripts: this.drainDeferredTranscripts(),
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Shared post-activation handoff for all trusted-contact success paths
|
|
1042
|
+
* (invite redemption, access-request approval, verification code).
|
|
1043
|
+
* Upgrades trust, delivers deterministic transition copy, and completes
|
|
1044
|
+
* with `proceed-handoff-spoken` so the controller marks the next caller
|
|
1045
|
+
* turn as an opening ack. Returns false when the flow reached a terminal
|
|
1046
|
+
* state during trust re-resolution and no handoff was delivered.
|
|
1047
|
+
*/
|
|
1048
|
+
private async continueAfterTrustedContactActivation(
|
|
1049
|
+
adeps: ActivationDeps,
|
|
1050
|
+
params: {
|
|
1051
|
+
assistantId: string;
|
|
1052
|
+
fromNumber: string;
|
|
1053
|
+
activationReason?:
|
|
1054
|
+
| "invite_redeemed"
|
|
1055
|
+
| "access_approved"
|
|
1056
|
+
| "trusted_contact_verified";
|
|
1057
|
+
/**
|
|
1058
|
+
* Display name resolved from the bound contact (or, for outbound invite
|
|
1059
|
+
* calls, the session's recorded invitee name). Greeting uses only the
|
|
1060
|
+
* first whitespace-delimited token; an empty/blank value triggers the
|
|
1061
|
+
* neutral "Hi there" greeting rather than substituting the channel
|
|
1062
|
+
* address.
|
|
1063
|
+
*/
|
|
1064
|
+
inviteeName?: string | null;
|
|
1065
|
+
},
|
|
1066
|
+
): Promise<boolean> {
|
|
1067
|
+
const { assistantId, fromNumber } = params;
|
|
1068
|
+
|
|
1069
|
+
this.deps.updateCallSession(this.callSessionId, { status: "in_progress" });
|
|
1070
|
+
|
|
1071
|
+
const trustContext = await this.resolveTrustWithDeferral(
|
|
1072
|
+
adeps,
|
|
1073
|
+
assistantId,
|
|
1074
|
+
fromNumber,
|
|
1075
|
+
);
|
|
1076
|
+
|
|
1077
|
+
// A transport close during trust re-resolution tears the flow down;
|
|
1078
|
+
// never speak or record a synthetic handoff on a dead call.
|
|
1079
|
+
if (this.state === "completed") {
|
|
1080
|
+
return false;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
const guardianLabel = adeps.resolveGuardianLabel();
|
|
1084
|
+
let handoffText: string;
|
|
1085
|
+
|
|
1086
|
+
if (params.activationReason === "invite_redeemed") {
|
|
1087
|
+
const firstName = firstToken(params.inviteeName);
|
|
1088
|
+
const assistantName = adeps.resolveAssistantLabel();
|
|
1089
|
+
if (firstName) {
|
|
1090
|
+
handoffText = assistantName
|
|
1091
|
+
? `Great, I've verified that you are ${firstName}. It's nice to meet you! I'm ${assistantName}, ${guardianLabel}'s assistant. How can I help?`
|
|
1092
|
+
: `Great, I've verified that you are ${firstName}. It's nice to meet you! How can I help?`;
|
|
1093
|
+
} else {
|
|
1094
|
+
handoffText = assistantName
|
|
1095
|
+
? `Hi there! I'm ${assistantName}, ${guardianLabel}'s assistant. How can I help?`
|
|
1096
|
+
: `Hi there! How can I help?`;
|
|
1097
|
+
}
|
|
1098
|
+
} else {
|
|
1099
|
+
handoffText = `Great! ${guardianLabel} said I can speak with you. How can I help?`;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
void this.deps.speakSystemPrompt(handoffText);
|
|
1103
|
+
|
|
1104
|
+
this.deps.recordCallEvent(this.callSessionId, "assistant_spoke", {
|
|
1105
|
+
text: handoffText,
|
|
1106
|
+
});
|
|
1107
|
+
const session = adeps.getCallSession(this.callSessionId);
|
|
1108
|
+
if (session) {
|
|
1109
|
+
adeps.fireCallTranscriptNotifier(
|
|
1110
|
+
session.conversationId,
|
|
1111
|
+
this.callSessionId,
|
|
1112
|
+
"assistant",
|
|
1113
|
+
handoffText,
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
this.complete({
|
|
1118
|
+
kind: "proceed-handoff-spoken",
|
|
1119
|
+
assistantId,
|
|
1120
|
+
trustContext,
|
|
1121
|
+
deferredTranscripts: this.drainDeferredTranscripts(),
|
|
1122
|
+
});
|
|
1123
|
+
return true;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/**
|
|
1127
|
+
* Re-resolve mid-setup trust, deferring transcripts that arrive during the
|
|
1128
|
+
* async window (see pushTranscriptFinal). Falls back to the setup-time
|
|
1129
|
+
* trust context on failure so a resolution blip can never wedge the call.
|
|
1130
|
+
*/
|
|
1131
|
+
private async resolveTrustWithDeferral(
|
|
1132
|
+
adeps: ActivationDeps,
|
|
1133
|
+
assistantId: string,
|
|
1134
|
+
fromNumber: string,
|
|
1135
|
+
): Promise<TrustContext> {
|
|
1136
|
+
this.trustReResolving = true;
|
|
1137
|
+
try {
|
|
1138
|
+
return await adeps.resolveMidCallTrustContext(assistantId, fromNumber);
|
|
1139
|
+
} catch (err) {
|
|
1140
|
+
log.error(
|
|
1141
|
+
{ err, callSessionId: this.callSessionId },
|
|
1142
|
+
"Mid-setup trust re-resolution failed — keeping setup-time trust",
|
|
1143
|
+
);
|
|
1144
|
+
return this.initialTrustContext!;
|
|
1145
|
+
} finally {
|
|
1146
|
+
this.trustReResolving = false;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/** Hand off transcripts buffered during trust re-resolution, in order. */
|
|
1151
|
+
private drainDeferredTranscripts(): string[] | undefined {
|
|
1152
|
+
if (this.deferredTranscripts.length === 0) {
|
|
1153
|
+
return undefined;
|
|
1154
|
+
}
|
|
1155
|
+
const drained = this.deferredTranscripts;
|
|
1156
|
+
this.deferredTranscripts = [];
|
|
1157
|
+
return drained;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/** Resolve the verification deps, applying defaults for the pure logic. */
|
|
1161
|
+
private requireVerificationDeps(): VerificationDeps {
|
|
1162
|
+
return requireDeps("verification", this.deps, [
|
|
1163
|
+
"getCallSession",
|
|
1164
|
+
"finalizeCall",
|
|
1165
|
+
"addMessage",
|
|
1166
|
+
"postPointerMessage",
|
|
1167
|
+
"fireCallTranscriptNotifier",
|
|
1168
|
+
"resolveGuardianLabel",
|
|
1169
|
+
"resolveAssistantLabel",
|
|
1170
|
+
"attemptVerificationCode",
|
|
1171
|
+
"resolveMidCallTrustContext",
|
|
1172
|
+
]);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
/** Resolve the invite-redemption deps, applying defaults for the pure logic. */
|
|
1176
|
+
private requireInviteDeps(): InviteDeps {
|
|
1177
|
+
return requireDeps("invite", this.deps, [
|
|
1178
|
+
"getCallSession",
|
|
1179
|
+
"finalizeCall",
|
|
1180
|
+
"fireCallTranscriptNotifier",
|
|
1181
|
+
"resolveGuardianLabel",
|
|
1182
|
+
"resolveAssistantLabel",
|
|
1183
|
+
"attemptInviteCodeRedemption",
|
|
1184
|
+
"resolveMidCallTrustContext",
|
|
1185
|
+
]);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/** Tear the session down once the terminal copy has had time to play. */
|
|
1189
|
+
private endSessionAfterPlayback(reason: string): void {
|
|
1190
|
+
if (this.disposed) {
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
this.endSessionTimer = setTimeout(
|
|
1194
|
+
() => this.transport.endSession(reason),
|
|
1195
|
+
this.deps.ttsPlaybackDelayMs ?? getTtsPlaybackDelayMs(),
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/** Run `finalizeCall` at most once per flow, whichever terminal path wins. */
|
|
1200
|
+
private finalizeOnce(
|
|
1201
|
+
finalize: typeof finalizeCallFn,
|
|
1202
|
+
conversationId: string,
|
|
1203
|
+
): void {
|
|
1204
|
+
if (this.finalized) {
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
this.finalized = true;
|
|
1208
|
+
finalize(this.callSessionId, conversationId);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// ── Invite redemption ───────────────────────────────────────────────
|
|
1212
|
+
|
|
1213
|
+
/**
|
|
1214
|
+
* Enter the invite-redemption sub-flow for a caller with an active voice
|
|
1215
|
+
* invite. Prompts the caller to enter their 6-digit invite code via DTMF
|
|
1216
|
+
* or speech; the code is validated in a single attempt.
|
|
1217
|
+
*/
|
|
1218
|
+
private startInviteRedemption(
|
|
1219
|
+
outcome: Extract<SetupOutcome, { action: "invite_redemption" }>,
|
|
1220
|
+
resolved: SetupResolved,
|
|
1221
|
+
): void {
|
|
1222
|
+
const ideps = this.requireInviteDeps();
|
|
1223
|
+
this.ideps = ideps;
|
|
1224
|
+
this.codeMode = "invite_redemption";
|
|
1225
|
+
this.codeLength = INVITE_CODE_LENGTH;
|
|
1226
|
+
this.digitBuffer = "";
|
|
1227
|
+
this.invite = {
|
|
1228
|
+
assistantId: outcome.assistantId,
|
|
1229
|
+
fromNumber: outcome.fromNumber,
|
|
1230
|
+
inviteeName: outcome.inviteeName,
|
|
1231
|
+
};
|
|
1232
|
+
this.initialTrustContext = toTrustContext(
|
|
1233
|
+
resolved.actorTrust,
|
|
1234
|
+
resolved.otherPartyNumber,
|
|
1235
|
+
);
|
|
1236
|
+
this.state = "collecting_code";
|
|
1237
|
+
|
|
1238
|
+
this.deps.recordCallEvent(this.callSessionId, "invite_redemption_started", {
|
|
1239
|
+
assistantId: outcome.assistantId,
|
|
1240
|
+
codeLength: INVITE_CODE_LENGTH,
|
|
1241
|
+
maxAttempts: 1,
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
const displayFriend = firstToken(outcome.inviteeName) ?? "there";
|
|
1245
|
+
const displayGuardian = ideps.resolveGuardianLabel();
|
|
1246
|
+
|
|
1247
|
+
let promptText: string;
|
|
1248
|
+
if (!resolved.isInbound) {
|
|
1249
|
+
const assistantName = ideps.resolveAssistantLabel();
|
|
1250
|
+
promptText = assistantName
|
|
1251
|
+
? `Hi ${displayFriend}, this is ${assistantName}, ${displayGuardian}'s assistant. To get started, please enter the 6-digit code that ${displayGuardian} shared with you.`
|
|
1252
|
+
: `Hi ${displayFriend}, this is ${displayGuardian}'s assistant. To get started, please enter the 6-digit code that ${displayGuardian} shared with you.`;
|
|
1253
|
+
} else {
|
|
1254
|
+
promptText = `Welcome ${displayFriend}. Please enter the 6-digit code that ${displayGuardian} provided you to verify your identity.`;
|
|
1255
|
+
}
|
|
1256
|
+
void this.deps.speakSystemPrompt(promptText);
|
|
1257
|
+
|
|
1258
|
+
log.info(
|
|
1259
|
+
{ callSessionId: this.callSessionId, assistantId: outcome.assistantId },
|
|
1260
|
+
`${resolved.isInbound ? "Inbound" : "Outbound"} voice invite redemption started`,
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
/**
|
|
1265
|
+
* Validate a fully-entered invite code against the gateway claim.
|
|
1266
|
+
* Concurrent attempts are deduped upstream by the shared
|
|
1267
|
+
* code-submission in-flight guard.
|
|
1268
|
+
*/
|
|
1269
|
+
private async handleInviteCodeEntry(enteredCode: string): Promise<void> {
|
|
1270
|
+
const invite = this.invite;
|
|
1271
|
+
const ideps = this.ideps;
|
|
1272
|
+
if (!invite || !ideps) {
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
const result = await ideps.attemptInviteCodeRedemption({
|
|
1277
|
+
inviteRedemptionFromNumber: invite.fromNumber,
|
|
1278
|
+
enteredCode,
|
|
1279
|
+
guardianLabel: ideps.resolveGuardianLabel(),
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
// A concurrent path may have already reached a terminal result.
|
|
1283
|
+
if (this.state === "completed") {
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
if (result.outcome === "success") {
|
|
1288
|
+
this.codeMode = null;
|
|
1289
|
+
this.digitBuffer = "";
|
|
1290
|
+
|
|
1291
|
+
this.deps.recordCallEvent(
|
|
1292
|
+
this.callSessionId,
|
|
1293
|
+
"invite_redemption_succeeded",
|
|
1294
|
+
{ memberId: result.memberId, inviteId: result.inviteId },
|
|
1295
|
+
);
|
|
1296
|
+
log.info(
|
|
1297
|
+
{
|
|
1298
|
+
callSessionId: this.callSessionId,
|
|
1299
|
+
memberId: result.memberId,
|
|
1300
|
+
type: result.type,
|
|
1301
|
+
},
|
|
1302
|
+
"Voice invite redemption succeeded",
|
|
1303
|
+
);
|
|
1304
|
+
|
|
1305
|
+
await this.continueAfterTrustedContactActivation(ideps, {
|
|
1306
|
+
assistantId: invite.assistantId,
|
|
1307
|
+
fromNumber: invite.fromNumber,
|
|
1308
|
+
activationReason: "invite_redeemed",
|
|
1309
|
+
inviteeName: invite.inviteeName,
|
|
1310
|
+
});
|
|
1311
|
+
} else {
|
|
1312
|
+
this.codeMode = null;
|
|
1313
|
+
|
|
1314
|
+
this.deps.recordCallEvent(
|
|
1315
|
+
this.callSessionId,
|
|
1316
|
+
"invite_redemption_failed",
|
|
1317
|
+
{ attempts: 1 },
|
|
1318
|
+
);
|
|
1319
|
+
log.warn(
|
|
1320
|
+
{ callSessionId: this.callSessionId },
|
|
1321
|
+
"Voice invite redemption failed — invalid or expired code",
|
|
1322
|
+
);
|
|
1323
|
+
|
|
1324
|
+
this.deps.updateCallSession(this.callSessionId, {
|
|
1325
|
+
status: "failed",
|
|
1326
|
+
endedAt: Date.now(),
|
|
1327
|
+
lastError: "Voice invite redemption failed — invalid or expired code",
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
const failSession = ideps.getCallSession(this.callSessionId);
|
|
1331
|
+
if (failSession) {
|
|
1332
|
+
this.finalizeOnce(ideps.finalizeCall, failSession.conversationId);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
await this.deps.speakSystemPrompt(result.ttsMessage);
|
|
1336
|
+
this.endSessionAfterPlayback("Invite redemption failed");
|
|
1337
|
+
this.complete({ kind: "ended", reason: "Invite redemption failed" });
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// ── Name capture + guardian wait ────────────────────────────────────
|
|
1342
|
+
|
|
1343
|
+
/**
|
|
1344
|
+
* Enter the name-capture sub-flow for an unknown inbound caller: speak
|
|
1345
|
+
* the intro greeting and arm the capture timeout. The caller's next
|
|
1346
|
+
* final transcript is taken as their name.
|
|
1347
|
+
*/
|
|
1348
|
+
private startNameCapture(
|
|
1349
|
+
outcome: Extract<SetupOutcome, { action: "name_capture" }>,
|
|
1350
|
+
resolved: SetupResolved,
|
|
1351
|
+
): void {
|
|
1352
|
+
const ndeps = this.requireNameCaptureDeps();
|
|
1353
|
+
this.ndeps = ndeps;
|
|
1354
|
+
this.accessRequest = {
|
|
1355
|
+
assistantId: outcome.assistantId,
|
|
1356
|
+
fromNumber: outcome.fromNumber,
|
|
1357
|
+
callerName: null,
|
|
1358
|
+
requestId: null,
|
|
1359
|
+
};
|
|
1360
|
+
this.initialTrustContext = toTrustContext(
|
|
1361
|
+
resolved.actorTrust,
|
|
1362
|
+
resolved.otherPartyNumber,
|
|
1363
|
+
);
|
|
1364
|
+
this.state = "capturing_name";
|
|
1365
|
+
|
|
1366
|
+
this.deps.recordCallEvent(
|
|
1367
|
+
this.callSessionId,
|
|
1368
|
+
"inbound_acl_name_capture_started",
|
|
1369
|
+
{
|
|
1370
|
+
from: outcome.fromNumber,
|
|
1371
|
+
trustClass: resolved.actorTrust.trustClass,
|
|
1372
|
+
},
|
|
1373
|
+
);
|
|
1374
|
+
|
|
1375
|
+
const guardianLabel = ndeps.resolveGuardianLabel();
|
|
1376
|
+
const assistantName = ndeps.resolveAssistantLabel();
|
|
1377
|
+
const greeting = assistantName
|
|
1378
|
+
? `Hi, this is ${assistantName}, ${guardianLabel}'s assistant. Sorry, I don't recognize this number. I'll let ${guardianLabel} know you called and see if I have permission to speak with you. Can I get your name?`
|
|
1379
|
+
: `Hi, this is ${guardianLabel}'s assistant. Sorry, I don't recognize this number. I'll let ${guardianLabel} know you called and see if I have permission to speak with you. Can I get your name?`;
|
|
1380
|
+
void this.deps.speakSystemPrompt(greeting);
|
|
1381
|
+
|
|
1382
|
+
const timeoutMs = this.deps.nameCaptureTimeoutMs ?? NAME_CAPTURE_TIMEOUT_MS;
|
|
1383
|
+
this.nameCaptureTimer = setTimeout(() => {
|
|
1384
|
+
if (this.state !== "capturing_name") {
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
void this.handleNameCaptureTimeout();
|
|
1388
|
+
}, timeoutMs);
|
|
1389
|
+
|
|
1390
|
+
log.info(
|
|
1391
|
+
{
|
|
1392
|
+
callSessionId: this.callSessionId,
|
|
1393
|
+
assistantId: outcome.assistantId,
|
|
1394
|
+
timeoutMs,
|
|
1395
|
+
},
|
|
1396
|
+
"Name capture started for unknown inbound caller",
|
|
1397
|
+
);
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
/**
|
|
1401
|
+
* Handle the caller's name: create the canonical access request, notify
|
|
1402
|
+
* the guardian, and hand the wait off to a {@link GuardianWaitController}.
|
|
1403
|
+
* Fails closed to the timeout copy when no request id comes back.
|
|
1404
|
+
*/
|
|
1405
|
+
private async handleNameCaptureResponse(callerName: string): Promise<void> {
|
|
1406
|
+
const ndeps = this.ndeps;
|
|
1407
|
+
const accessRequest = this.accessRequest;
|
|
1408
|
+
if (!ndeps || !accessRequest) {
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
this.clearNameCaptureTimer();
|
|
1413
|
+
accessRequest.callerName = callerName;
|
|
1414
|
+
|
|
1415
|
+
this.deps.recordCallEvent(this.callSessionId, "inbound_acl_name_captured", {
|
|
1416
|
+
from: accessRequest.fromNumber,
|
|
1417
|
+
callerName,
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
try {
|
|
1421
|
+
const accessResult = await ndeps.notifyGuardianOfAccessRequest({
|
|
1422
|
+
canonicalAssistantId: accessRequest.assistantId,
|
|
1423
|
+
sourceChannel: "phone",
|
|
1424
|
+
conversationExternalId: accessRequest.fromNumber,
|
|
1425
|
+
actorExternalId: accessRequest.fromNumber,
|
|
1426
|
+
actorDisplayName: callerName,
|
|
1427
|
+
});
|
|
1428
|
+
|
|
1429
|
+
if (accessResult.notified) {
|
|
1430
|
+
accessRequest.requestId = accessResult.requestId;
|
|
1431
|
+
log.info(
|
|
1432
|
+
{
|
|
1433
|
+
callSessionId: this.callSessionId,
|
|
1434
|
+
requestId: accessResult.requestId,
|
|
1435
|
+
callerName,
|
|
1436
|
+
},
|
|
1437
|
+
"Guardian notified of voice access request with caller name",
|
|
1438
|
+
);
|
|
1439
|
+
} else if (accessResult.reason === "already_denied") {
|
|
1440
|
+
// The guardian already denied this caller; they are intentionally not
|
|
1441
|
+
// re-notified. Deliver the denial copy rather than the "I'll let them
|
|
1442
|
+
// know" timeout copy, which would falsely promise a notification.
|
|
1443
|
+
log.info(
|
|
1444
|
+
{ callSessionId: this.callSessionId },
|
|
1445
|
+
"Voice caller previously denied — suppressing re-notification, delivering denial",
|
|
1446
|
+
);
|
|
1447
|
+
await this.handleAccessRequestDenied(ndeps, null);
|
|
1448
|
+
return;
|
|
1449
|
+
} else {
|
|
1450
|
+
log.warn(
|
|
1451
|
+
{ callSessionId: this.callSessionId },
|
|
1452
|
+
"Failed to notify guardian of voice access request — no sender ID",
|
|
1453
|
+
);
|
|
1454
|
+
}
|
|
1455
|
+
} catch (err) {
|
|
1456
|
+
log.error(
|
|
1457
|
+
{ err, callSessionId: this.callSessionId },
|
|
1458
|
+
"Failed to create access request for voice caller",
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// A transport close during the async notification tears the flow down;
|
|
1463
|
+
// don't start a wait (or speak timeout copy) on a dead transport.
|
|
1464
|
+
if (this.state === "completed") {
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// No request id (notify threw or returned notified: false) — fail closed
|
|
1469
|
+
// rather than leaving the caller stuck on hold with no poll target.
|
|
1470
|
+
const requestId = accessRequest.requestId;
|
|
1471
|
+
if (!requestId) {
|
|
1472
|
+
log.warn(
|
|
1473
|
+
{ callSessionId: this.callSessionId },
|
|
1474
|
+
"Access request ID is null after notification attempt — failing closed",
|
|
1475
|
+
);
|
|
1476
|
+
await this.handleAccessRequestTimeout(ndeps, {
|
|
1477
|
+
requestId: null,
|
|
1478
|
+
callbackOptIn: false,
|
|
1479
|
+
});
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
this.startAccessRequestWait(ndeps, requestId, accessRequest);
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
/**
|
|
1487
|
+
* Hand the guardian-decision wait to a {@link GuardianWaitController}:
|
|
1488
|
+
* it owns the hold message, heartbeats, polling, and timeout; the flow
|
|
1489
|
+
* owns the resolution continuations.
|
|
1490
|
+
*/
|
|
1491
|
+
private startAccessRequestWait(
|
|
1492
|
+
ndeps: NameCaptureDeps,
|
|
1493
|
+
requestId: string,
|
|
1494
|
+
accessRequest: AccessRequestState,
|
|
1495
|
+
): void {
|
|
1496
|
+
this.state = "awaiting_guardian_decision";
|
|
1497
|
+
|
|
1498
|
+
this.guardianWait = ndeps.createGuardianWaitController(this.callSessionId, {
|
|
1499
|
+
speakSystemPrompt: this.deps.speakSystemPrompt,
|
|
1500
|
+
updateCallSession: this.deps.updateCallSession,
|
|
1501
|
+
recordCallEvent: this.deps.recordCallEvent,
|
|
1502
|
+
resolveGuardianLabel: ndeps.resolveGuardianLabel,
|
|
1503
|
+
firstHeartbeatDelayMs: this.deps.ttsPlaybackDelayMs,
|
|
1504
|
+
onApproved: (ctx) => this.handleAccessRequestApproved(ndeps, ctx),
|
|
1505
|
+
onDenied: (ctx) => this.handleAccessRequestDenied(ndeps, ctx.requestId),
|
|
1506
|
+
onTimeout: (ctx) => this.handleAccessRequestTimeout(ndeps, ctx),
|
|
1507
|
+
});
|
|
1508
|
+
this.guardianWait.start({
|
|
1509
|
+
requestId,
|
|
1510
|
+
assistantId: accessRequest.assistantId,
|
|
1511
|
+
fromNumber: accessRequest.fromNumber,
|
|
1512
|
+
callerName: accessRequest.callerName,
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
/**
|
|
1517
|
+
* Approved access request: the caller is now an activated trusted
|
|
1518
|
+
* contact — run the shared activation continuation and hand off.
|
|
1519
|
+
*/
|
|
1520
|
+
private async handleAccessRequestApproved(
|
|
1521
|
+
ndeps: NameCaptureDeps,
|
|
1522
|
+
ctx: GuardianWaitResolutionContext,
|
|
1523
|
+
): Promise<void> {
|
|
1524
|
+
this.deps.recordCallEvent(
|
|
1525
|
+
this.callSessionId,
|
|
1526
|
+
"inbound_acl_access_approved",
|
|
1527
|
+
{
|
|
1528
|
+
from: ctx.fromNumber,
|
|
1529
|
+
callerName: ctx.callerName,
|
|
1530
|
+
requestId: ctx.requestId,
|
|
1531
|
+
},
|
|
1532
|
+
);
|
|
1533
|
+
|
|
1534
|
+
log.info(
|
|
1535
|
+
{ callSessionId: this.callSessionId, from: ctx.fromNumber },
|
|
1536
|
+
"Access request approved — caller activated and continuing call",
|
|
1537
|
+
);
|
|
1538
|
+
|
|
1539
|
+
const handoffSpoken = await this.continueAfterTrustedContactActivation(
|
|
1540
|
+
ndeps,
|
|
1541
|
+
{
|
|
1542
|
+
assistantId: ctx.assistantId,
|
|
1543
|
+
fromNumber: ctx.fromNumber,
|
|
1544
|
+
activationReason: "access_approved",
|
|
1545
|
+
},
|
|
1546
|
+
);
|
|
1547
|
+
if (!handoffSpoken) {
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
this.deps.recordCallEvent(
|
|
1552
|
+
this.callSessionId,
|
|
1553
|
+
"inbound_acl_post_approval_handoff_spoken",
|
|
1554
|
+
{ from: ctx.fromNumber },
|
|
1555
|
+
);
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
/** Denied access request: deliver deterministic copy and hang up. */
|
|
1559
|
+
private async handleAccessRequestDenied(
|
|
1560
|
+
ndeps: NameCaptureDeps,
|
|
1561
|
+
requestId: string | null,
|
|
1562
|
+
): Promise<void> {
|
|
1563
|
+
const guardianLabel = ndeps.resolveGuardianLabel();
|
|
1564
|
+
|
|
1565
|
+
this.deps.recordCallEvent(this.callSessionId, "inbound_acl_access_denied", {
|
|
1566
|
+
from: this.accessRequest?.fromNumber,
|
|
1567
|
+
requestId,
|
|
1568
|
+
});
|
|
1569
|
+
|
|
1570
|
+
this.deps.updateCallSession(this.callSessionId, {
|
|
1571
|
+
status: "failed",
|
|
1572
|
+
endedAt: Date.now(),
|
|
1573
|
+
lastError: "Inbound voice ACL: guardian denied access request",
|
|
1574
|
+
});
|
|
1575
|
+
|
|
1576
|
+
log.info(
|
|
1577
|
+
{ callSessionId: this.callSessionId },
|
|
1578
|
+
"Access request denied — ending call",
|
|
1579
|
+
);
|
|
1580
|
+
|
|
1581
|
+
await this.deps.speakSystemPrompt(
|
|
1582
|
+
`Sorry, ${guardianLabel} says I'm not allowed to speak with you. Goodbye.`,
|
|
1583
|
+
);
|
|
1584
|
+
this.endSessionAfterPlayback("Access request denied");
|
|
1585
|
+
this.complete({ kind: "ended", reason: "Access request denied" });
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
/**
|
|
1589
|
+
* Access-request wait timeout (or fail-closed creation failure): deliver
|
|
1590
|
+
* deterministic copy — including the callback note when the caller opted
|
|
1591
|
+
* in — and hang up. The wait controller emits the callback handoff
|
|
1592
|
+
* notification before invoking this.
|
|
1593
|
+
*/
|
|
1594
|
+
private async handleAccessRequestTimeout(
|
|
1595
|
+
ndeps: NameCaptureDeps,
|
|
1596
|
+
params: {
|
|
1597
|
+
requestId: string | null;
|
|
1598
|
+
callbackOptIn: boolean;
|
|
1599
|
+
},
|
|
1600
|
+
): Promise<void> {
|
|
1601
|
+
const guardianLabel = ndeps.resolveGuardianLabel();
|
|
1602
|
+
|
|
1603
|
+
this.deps.recordCallEvent(
|
|
1604
|
+
this.callSessionId,
|
|
1605
|
+
"inbound_acl_access_timeout",
|
|
1606
|
+
{
|
|
1607
|
+
from: this.accessRequest?.fromNumber,
|
|
1608
|
+
requestId: params.requestId,
|
|
1609
|
+
callbackOptIn: params.callbackOptIn,
|
|
1610
|
+
},
|
|
1611
|
+
);
|
|
1612
|
+
|
|
1613
|
+
const callbackNote = params.callbackOptIn
|
|
1614
|
+
? ` I've noted that you'd like a callback — I'll pass that along to ${guardianLabel}.`
|
|
1615
|
+
: "";
|
|
1616
|
+
|
|
1617
|
+
this.deps.updateCallSession(this.callSessionId, {
|
|
1618
|
+
status: "failed",
|
|
1619
|
+
endedAt: Date.now(),
|
|
1620
|
+
lastError: "Inbound voice ACL: guardian approval wait timed out",
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
log.info(
|
|
1624
|
+
{ callSessionId: this.callSessionId },
|
|
1625
|
+
"Access request timed out — ending call",
|
|
1626
|
+
);
|
|
1627
|
+
|
|
1628
|
+
await this.deps.speakSystemPrompt(
|
|
1629
|
+
`Sorry, I can't get ahold of ${guardianLabel} right now. I'll let them know you called.${callbackNote}`,
|
|
1630
|
+
);
|
|
1631
|
+
this.endSessionAfterPlayback("Access request timed out");
|
|
1632
|
+
this.complete({ kind: "ended", reason: "Access request timed out" });
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
/**
|
|
1636
|
+
* Name-capture timeout: the caller never provided their name within the
|
|
1637
|
+
* allotted window. Deliver deterministic copy and hang up.
|
|
1638
|
+
*/
|
|
1639
|
+
private async handleNameCaptureTimeout(): Promise<void> {
|
|
1640
|
+
// A transcript racing the timeout must not start a second terminal path.
|
|
1641
|
+
this.nameCaptureBusy = true;
|
|
1642
|
+
this.clearNameCaptureTimer();
|
|
1643
|
+
|
|
1644
|
+
this.deps.recordCallEvent(
|
|
1645
|
+
this.callSessionId,
|
|
1646
|
+
"inbound_acl_name_capture_timeout",
|
|
1647
|
+
{ from: this.accessRequest?.fromNumber },
|
|
1648
|
+
);
|
|
1649
|
+
|
|
1650
|
+
this.deps.updateCallSession(this.callSessionId, {
|
|
1651
|
+
status: "failed",
|
|
1652
|
+
endedAt: Date.now(),
|
|
1653
|
+
lastError: "Inbound voice ACL: name capture timed out",
|
|
1654
|
+
});
|
|
1655
|
+
|
|
1656
|
+
log.info(
|
|
1657
|
+
{ callSessionId: this.callSessionId },
|
|
1658
|
+
"Name capture timed out — ending call",
|
|
1659
|
+
);
|
|
1660
|
+
|
|
1661
|
+
await this.deps.speakSystemPrompt(
|
|
1662
|
+
"Sorry, I didn't catch your name. Please try calling back. Goodbye.",
|
|
1663
|
+
);
|
|
1664
|
+
this.endSessionAfterPlayback("Name capture timed out");
|
|
1665
|
+
this.complete({ kind: "ended", reason: "Name capture timed out" });
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
private clearNameCaptureTimer(): void {
|
|
1669
|
+
if (this.nameCaptureTimer) {
|
|
1670
|
+
clearTimeout(this.nameCaptureTimer);
|
|
1671
|
+
this.nameCaptureTimer = null;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
/** Resolve the name-capture deps, applying defaults for the pure logic. */
|
|
1676
|
+
private requireNameCaptureDeps(): NameCaptureDeps {
|
|
1677
|
+
return requireDeps("name-capture", this.deps, [
|
|
1678
|
+
"getCallSession",
|
|
1679
|
+
"fireCallTranscriptNotifier",
|
|
1680
|
+
"resolveGuardianLabel",
|
|
1681
|
+
"resolveAssistantLabel",
|
|
1682
|
+
"notifyGuardianOfAccessRequest",
|
|
1683
|
+
"createGuardianWaitController",
|
|
1684
|
+
"resolveMidCallTrustContext",
|
|
1685
|
+
]);
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// ── Unverified caller ───────────────────────────────────────────────
|
|
1689
|
+
|
|
1690
|
+
/** Speak verification guidance to a known-but-unverified caller, then disconnect. */
|
|
1691
|
+
private async runUnverifiedCaller(
|
|
1692
|
+
outcome: Extract<SetupOutcome, { action: "unverified_caller" }>,
|
|
1693
|
+
): Promise<void> {
|
|
1694
|
+
this.deps.recordCallEvent(
|
|
1695
|
+
this.callSessionId,
|
|
1696
|
+
"inbound_acl_unverified_caller",
|
|
1697
|
+
{
|
|
1698
|
+
callSessionId: this.callSessionId,
|
|
1699
|
+
isGuardian: outcome.isGuardian,
|
|
1700
|
+
},
|
|
1701
|
+
);
|
|
1702
|
+
this.deps.updateCallSession(this.callSessionId, {
|
|
1703
|
+
status: "failed",
|
|
1704
|
+
endedAt: Date.now(),
|
|
1705
|
+
lastError: "Inbound voice ACL: caller channel unverified",
|
|
1706
|
+
});
|
|
1707
|
+
const action = outcome.isGuardian
|
|
1708
|
+
? `To verify, open your assistant's contacts page, click Verify next to the phone channel, ` +
|
|
1709
|
+
`and follow the prompts. Then call back once the verification session is active.`
|
|
1710
|
+
: `Please reach out to the account guardian to start a new verification session, ` +
|
|
1711
|
+
`then call back once the verification session is active.`;
|
|
1712
|
+
const message =
|
|
1713
|
+
`This number is registered as ${outcome.displayName}'s phone but has not been verified yet. ` +
|
|
1714
|
+
action;
|
|
1715
|
+
await this.deps.speakSystemPrompt(message);
|
|
1716
|
+
this.endSessionAfterPlayback(
|
|
1717
|
+
"Inbound voice ACL: caller channel unverified",
|
|
1718
|
+
);
|
|
1719
|
+
this.complete({
|
|
1720
|
+
kind: "ended",
|
|
1721
|
+
reason: "Inbound voice ACL: caller channel unverified",
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
// ── Internals ───────────────────────────────────────────────────────
|
|
1726
|
+
|
|
1727
|
+
private acceptsInput(): boolean {
|
|
1728
|
+
return this.state !== "idle" && this.state !== "completed";
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
/**
|
|
1732
|
+
* Deliver the terminal result exactly once. A flow already completed
|
|
1733
|
+
* (or disposed on transport close) swallows late completions from
|
|
1734
|
+
* racing terminal paths.
|
|
1735
|
+
*/
|
|
1736
|
+
private complete(result: SetupFlowResult): void {
|
|
1737
|
+
if (this.state === "completed") {
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
this.state = "completed";
|
|
1741
|
+
this.deps.onComplete(result);
|
|
1742
|
+
}
|
|
1743
|
+
}
|