@vellumai/assistant 0.4.26 → 0.4.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +2 -2
- package/AGENTS.md +5 -0
- package/ARCHITECTURE.md +169 -69
- package/Dockerfile +1 -1
- package/README.md +111 -112
- package/bun.lock +0 -3
- package/docs/architecture/integrations.md +0 -1
- package/docs/architecture/memory.md +100 -63
- package/docs/error-handling.md +71 -0
- package/docs/runbook-trusted-contacts.md +10 -9
- package/docs/trusted-contact-access.md +48 -46
- package/package.json +3 -3
- package/scripts/compare-benchmarks.sh +12 -5
- package/scripts/ipc/check-swift-decoder-drift.ts +3 -0
- package/scripts/test.sh +89 -5
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +46 -0
- package/src/__tests__/access-request-decision.test.ts +0 -1
- package/src/__tests__/account-registry.test.ts +1 -1
- package/src/__tests__/actor-token-service.test.ts +36 -23
- package/src/__tests__/agent-loop-thinking.test.ts +29 -13
- package/src/__tests__/agent-loop.test.ts +2 -1
- package/src/__tests__/app-builder-tool-scripts.test.ts +1 -1
- package/src/__tests__/approval-routes-http.test.ts +2 -2
- package/src/__tests__/asset-materialize-tool.test.ts +7 -7
- package/src/__tests__/asset-search-tool.test.ts +7 -7
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +217 -0
- package/src/__tests__/call-controller.test.ts +99 -69
- package/src/__tests__/call-start-guardian-guard.test.ts +1 -1
- package/src/__tests__/channel-approval-routes.test.ts +113 -70
- package/src/__tests__/channel-guardian.test.ts +173 -282
- package/src/__tests__/channel-readiness-service.test.ts +6 -2
- package/src/__tests__/channel-reply-delivery.test.ts +2 -2
- package/src/__tests__/channel-retry-sweep.test.ts +14 -14
- package/src/__tests__/checker.test.ts +12 -31
- package/src/__tests__/claude-code-tool-profiles.test.ts +1 -1
- package/src/__tests__/commit-message-enrichment-service.test.ts +67 -59
- package/src/__tests__/compaction.benchmark.test.ts +6 -2
- package/src/__tests__/computer-use-tools.test.ts +1 -1
- package/src/__tests__/config-schema.test.ts +66 -7
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +29 -29
- package/src/__tests__/contacts-tools.test.ts +63 -2
- package/src/__tests__/context-overflow-approval.test.ts +141 -0
- package/src/__tests__/context-overflow-policy.test.ts +171 -0
- package/src/__tests__/context-overflow-reducer.test.ts +533 -0
- package/src/__tests__/context-window-manager.test.ts +97 -0
- package/src/__tests__/conversation-attention-telegram.test.ts +38 -46
- package/src/__tests__/conversation-pairing.test.ts +2 -2
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +214 -10
- package/src/__tests__/conversation-routes.test.ts +4 -7
- package/src/__tests__/credential-broker-browser-fill.test.ts +13 -2
- package/src/__tests__/credential-security-e2e.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/credential-vault-unit.test.ts +1 -1
- package/src/__tests__/credential-vault.test.ts +11 -8
- package/src/__tests__/daemon-lifecycle.test.ts +2 -2
- package/src/__tests__/daemon-server-session-init.test.ts +6 -6
- package/src/__tests__/delete-managed-skill-tool.test.ts +1 -1
- package/src/__tests__/deterministic-verification-control-plane.test.ts +2 -2
- package/src/__tests__/emit-signal-routing-intent.test.ts +4 -0
- package/src/__tests__/encrypted-store.test.ts +10 -7
- package/src/__tests__/ephemeral-permissions.test.ts +3 -3
- package/src/__tests__/file-edit-tool.test.ts +1 -1
- package/src/__tests__/file-read-tool.test.ts +1 -1
- package/src/__tests__/file-write-tool.test.ts +1 -1
- package/src/__tests__/fixtures/credential-security-fixtures.ts +87 -64
- package/src/__tests__/fixtures/media-reuse-fixtures.ts +37 -31
- package/src/__tests__/fixtures/mock-signup-server.ts +171 -115
- package/src/__tests__/fixtures/proxy-fixtures.ts +39 -39
- package/src/__tests__/followup-tools.test.ts +1 -1
- package/src/__tests__/gateway-only-guard.test.ts +3 -0
- package/src/__tests__/guardian-actions-endpoint.test.ts +543 -1
- package/src/__tests__/guardian-control-plane-policy.test.ts +15 -15
- package/src/__tests__/guardian-dispatch.test.ts +79 -1
- package/src/__tests__/guardian-grant-minting.test.ts +14 -14
- package/src/__tests__/guardian-outbound-http.test.ts +1 -2
- package/src/__tests__/guardian-principal-id-roundtrip.test.ts +0 -41
- package/src/__tests__/guardian-routing-invariants.test.ts +2 -5
- package/src/__tests__/guardian-routing-state.test.ts +36 -52
- package/src/__tests__/guardian-verification-intent-routing.test.ts +4 -6
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +2 -2
- package/src/__tests__/handle-user-message-secret-resume.test.ts +39 -1
- package/src/__tests__/handlers-cu-observation-blob.test.ts +21 -10
- package/src/__tests__/handlers-telegram-config.test.ts +14 -14
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +23 -2
- package/src/__tests__/headless-browser-interactions.test.ts +1 -1
- package/src/__tests__/headless-browser-navigate.test.ts +1 -1
- package/src/__tests__/headless-browser-read-tools.test.ts +1 -1
- package/src/__tests__/headless-browser-snapshot.test.ts +1 -1
- package/src/__tests__/heartbeat-service.test.ts +45 -2
- package/src/__tests__/host-file-edit-tool.test.ts +1 -1
- package/src/__tests__/host-file-read-tool.test.ts +1 -1
- package/src/__tests__/host-file-write-tool.test.ts +1 -1
- package/src/__tests__/host-shell-tool.test.ts +1 -1
- package/src/__tests__/inbound-invite-redemption.test.ts +16 -18
- package/src/__tests__/ingress-reconcile.test.ts +2 -2
- package/src/__tests__/ingress-routes-http.test.ts +2 -1
- package/src/__tests__/integrations-cli.test.ts +256 -0
- package/src/__tests__/intent-routing.test.ts +4 -5
- package/src/__tests__/invite-redemption-service.test.ts +4 -3
- package/src/__tests__/ipc-snapshot.test.ts +28 -0
- package/src/__tests__/managed-skill-lifecycle.test.ts +1 -1
- package/src/__tests__/mcp-cli.test.ts +136 -57
- package/src/__tests__/mcp-client-auth.test.ts +95 -0
- package/src/__tests__/media-generate-image.test.ts +2 -2
- package/src/__tests__/media-reuse-story.e2e.test.ts +8 -8
- package/src/__tests__/memory-regressions.test.ts +6 -6
- package/src/__tests__/messaging-send-tool.test.ts +1 -1
- package/src/__tests__/migration-cross-version-compatibility.test.ts +1855 -0
- package/src/__tests__/migration-export-http.test.ts +540 -0
- package/src/__tests__/migration-import-commit-http.test.ts +823 -0
- package/src/__tests__/migration-import-preflight-http.test.ts +755 -0
- package/src/__tests__/migration-parity-persistence.test.ts +1854 -0
- package/src/__tests__/migration-transport.test.ts +904 -0
- package/src/__tests__/migration-validate-http.test.ts +698 -0
- package/src/__tests__/migration-wizard.test.ts +1289 -0
- package/src/__tests__/non-member-access-request.test.ts +17 -17
- package/src/__tests__/notification-decision-strategy.test.ts +110 -2
- package/src/__tests__/notification-deep-link.test.ts +18 -0
- package/src/__tests__/notification-guardian-path.test.ts +0 -1
- package/src/__tests__/oauth2-gateway-transport.test.ts +1 -1
- package/src/__tests__/playbook-execution.test.ts +1 -1
- package/src/__tests__/playbook-tools.test.ts +1 -1
- package/src/__tests__/provider-streaming.benchmark.test.ts +3 -1
- package/src/__tests__/proxy-approval-callback.test.ts +1 -1
- package/src/__tests__/qdrant-manager.test.ts +40 -11
- package/src/__tests__/rebind-secrets-screen.test.ts +839 -0
- package/src/__tests__/recording-handler.test.ts +2 -2
- package/src/__tests__/recording-intent-handler.test.ts +3 -3
- package/src/__tests__/recording-state-machine.test.ts +2 -2
- package/src/__tests__/relay-server.test.ts +506 -227
- package/src/__tests__/reminder-store.test.ts +8 -0
- package/src/__tests__/reminder.test.ts +8 -0
- package/src/__tests__/{resolve-guardian-trust-class.test.ts → resolve-trust-class.test.ts} +11 -17
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +1 -1
- package/src/__tests__/schedule-tools.test.ts +1 -1
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/script-proxy-connect-tunnel.test.ts +2 -3
- package/src/__tests__/script-proxy-decision-trace.test.ts +2 -2
- package/src/__tests__/script-proxy-http-forwarder.test.ts +1 -1
- package/src/__tests__/script-proxy-injection-runtime.test.ts +5 -5
- package/src/__tests__/script-proxy-mitm-handler.test.ts +4 -4
- package/src/__tests__/script-proxy-policy-runtime.test.ts +2 -2
- package/src/__tests__/script-proxy-policy.test.ts +2 -2
- package/src/__tests__/script-proxy-session-manager.test.ts +4 -7
- package/src/__tests__/script-proxy-session-runtime.test.ts +1 -6
- package/src/__tests__/secret-onetime-send.test.ts +4 -4
- package/src/__tests__/secret-scanner-executor.test.ts +2 -2
- package/src/__tests__/send-endpoint-busy.test.ts +11 -9
- package/src/__tests__/send-notification-tool.test.ts +2 -2
- package/src/__tests__/session-abort-tool-results.test.ts +17 -2
- package/src/__tests__/session-agent-loop.test.ts +456 -35
- package/src/__tests__/session-confirmation-signals.test.ts +3 -2
- package/src/__tests__/session-conflict-gate.test.ts +20 -3
- package/src/__tests__/session-init.benchmark.test.ts +2 -2
- package/src/__tests__/session-load-history-repair.test.ts +7 -7
- package/src/__tests__/session-pre-run-repair.test.ts +17 -2
- package/src/__tests__/session-profile-injection.test.ts +20 -3
- package/src/__tests__/session-provider-retry-repair.test.ts +86 -6
- package/src/__tests__/session-queue.test.ts +33 -18
- package/src/__tests__/session-runtime-assembly.test.ts +147 -1
- package/src/__tests__/session-runtime-workspace.test.ts +40 -0
- package/src/__tests__/session-slash-known.test.ts +21 -3
- package/src/__tests__/session-slash-queue.test.ts +17 -2
- package/src/__tests__/session-slash-unknown.test.ts +17 -2
- package/src/__tests__/session-surfaces-deselection.test.ts +208 -0
- package/src/__tests__/session-workspace-cache-state.test.ts +2 -2
- package/src/__tests__/session-workspace-injection.test.ts +17 -2
- package/src/__tests__/session-workspace-tool-tracking.test.ts +17 -2
- package/src/__tests__/shell-credential-ref.test.ts +1 -1
- package/src/__tests__/shell-tool-proxy-mode.test.ts +1 -1
- package/src/__tests__/skill-load-feature-flag.test.ts +1 -1
- package/src/__tests__/skill-load-tool.test.ts +1 -1
- package/src/__tests__/skill-script-runner-host.test.ts +1 -1
- package/src/__tests__/skill-script-runner-sandbox.test.ts +1 -1
- package/src/__tests__/skill-script-runner.test.ts +1 -1
- package/src/__tests__/skill-tool-factory.test.ts +1 -1
- package/src/__tests__/slack-skill.test.ts +3 -2
- package/src/__tests__/subagent-tools.test.ts +3 -3
- package/src/__tests__/swarm-recursion.test.ts +1 -1
- package/src/__tests__/swarm-session-integration.test.ts +1 -1
- package/src/__tests__/swarm-tool.test.ts +1 -1
- package/src/__tests__/task-management-tools.test.ts +1 -1
- package/src/__tests__/task-tools.test.ts +1 -1
- package/src/__tests__/terminal-tools.test.ts +1 -1
- package/src/__tests__/test-support/browser-skill-harness.ts +39 -27
- package/src/__tests__/test-support/computer-use-skill-harness.ts +14 -14
- package/src/__tests__/tool-approval-handler.test.ts +15 -15
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -1
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -2
- package/src/__tests__/tool-executor-shell-integration.test.ts +1 -1
- package/src/__tests__/tool-executor.test.ts +23 -182
- package/src/__tests__/tool-grant-request-escalation.test.ts +11 -11
- package/src/__tests__/tool-permission-simulate-handler.test.ts +4 -4
- package/src/__tests__/transfer-progress-screen.test.ts +1180 -0
- package/src/__tests__/trust-context-guards.test.ts +25 -29
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +23 -21
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +37 -40
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +29 -25
- package/src/__tests__/trusted-contact-multichannel.test.ts +25 -24
- package/src/__tests__/trusted-contact-verification.test.ts +63 -77
- package/src/__tests__/turn-commit.test.ts +18 -18
- package/src/__tests__/twilio-provider.test.ts +7 -7
- package/src/__tests__/validation-results-screen.test.ts +1107 -0
- package/src/__tests__/view-image-tool.test.ts +1 -1
- package/src/__tests__/voice-invite-redemption.test.ts +3 -2
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +12 -12
- package/src/__tests__/voice-session-bridge.test.ts +24 -24
- package/src/agent/attachments.ts +3 -1
- package/src/agent/loop.ts +13 -13
- package/src/agent/message-types.ts +13 -7
- package/src/amazon/cart.ts +59 -32
- package/src/amazon/checkout.ts +25 -14
- package/src/amazon/client.ts +68 -48
- package/src/amazon/product-details.ts +3 -3
- package/src/amazon/request-extractor.ts +46 -31
- package/src/amazon/search.ts +6 -4
- package/src/amazon/session.ts +33 -24
- package/src/approvals/AGENTS.md +26 -0
- package/src/approvals/approval-primitive.ts +87 -64
- package/src/approvals/guardian-decision-primitive.ts +172 -81
- package/src/approvals/guardian-request-resolvers.ts +262 -155
- package/src/autonomy/autonomy-resolver.ts +7 -5
- package/src/autonomy/autonomy-store.ts +34 -19
- package/src/autonomy/disposition-mapper.ts +5 -5
- package/src/autonomy/index.ts +6 -6
- package/src/autonomy/types.ts +7 -3
- package/src/browser-extension-relay/client.ts +50 -19
- package/src/browser-extension-relay/protocol.ts +11 -11
- package/src/browser-extension-relay/server.ts +45 -20
- package/src/bundler/app-bundler.ts +75 -50
- package/src/bundler/bundle-scanner.ts +145 -41
- package/src/bundler/bundle-signer.ts +16 -14
- package/src/bundler/signature-verifier.ts +36 -33
- package/src/calls/call-constants.ts +10 -3
- package/src/calls/call-controller.ts +473 -214
- package/src/calls/call-conversation-messages.ts +25 -15
- package/src/calls/call-domain.ts +401 -148
- package/src/calls/call-pointer-message-composer.ts +26 -21
- package/src/calls/call-pointer-messages.ts +52 -28
- package/src/calls/call-recovery.ts +53 -37
- package/src/calls/call-state-machine.ts +37 -7
- package/src/calls/call-state.ts +35 -13
- package/src/calls/call-store.ts +165 -77
- package/src/calls/elevenlabs-client.ts +39 -20
- package/src/calls/guardian-action-sweep.ts +42 -24
- package/src/calls/guardian-dispatch.ts +79 -56
- package/src/calls/guardian-question-copy.ts +28 -23
- package/src/calls/relay-server.ts +1121 -532
- package/src/calls/speaker-identification.ts +21 -15
- package/src/calls/twilio-config.ts +34 -17
- package/src/calls/twilio-provider.ts +108 -55
- package/src/calls/twilio-rest.ts +212 -100
- package/src/calls/twilio-routes.ts +165 -92
- package/src/calls/types.ts +55 -7
- package/src/calls/voice-quality.ts +6 -4
- package/src/calls/voice-session-bridge.ts +181 -133
- package/src/channels/config.ts +17 -13
- package/src/channels/types.ts +38 -10
- package/src/cli/amazon.ts +333 -227
- package/src/cli/config-commands.ts +236 -146
- package/src/cli/core-commands.ts +403 -329
- package/src/cli/email-guardrails.ts +38 -19
- package/src/cli/email.ts +207 -153
- package/src/cli/influencer.ts +58 -56
- package/src/cli/integrations.ts +362 -0
- package/src/cli/ipc-client.ts +24 -19
- package/src/cli/map.ts +176 -129
- package/src/cli/mcp.ts +260 -152
- package/src/cli/sequence.ts +165 -107
- package/src/cli/twitter.ts +302 -218
- package/src/cli.ts +418 -279
- package/src/commands/cc-command-registry.ts +52 -27
- package/src/config/agent-schema.ts +217 -134
- package/src/config/assistant-feature-flags.ts +23 -18
- package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +19 -0
- package/src/config/bundled-skills/app-builder/tools/app-create.ts +7 -4
- package/src/config/bundled-skills/app-builder/tools/app-delete.ts +6 -3
- package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +7 -4
- package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +6 -3
- package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +6 -3
- package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +7 -4
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +6 -3
- package/src/config/bundled-skills/app-builder/tools/app-query.ts +6 -3
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +6 -3
- package/src/config/bundled-skills/browser/tools/browser-click.ts +5 -2
- package/src/config/bundled-skills/browser/tools/browser-close.ts +5 -2
- package/src/config/bundled-skills/browser/tools/browser-extract.ts +5 -2
- package/src/config/bundled-skills/browser/tools/browser-fill-credential.ts +5 -2
- package/src/config/bundled-skills/browser/tools/browser-hover.ts +5 -2
- package/src/config/bundled-skills/browser/tools/browser-navigate.ts +5 -2
- package/src/config/bundled-skills/browser/tools/browser-press-key.ts +5 -2
- package/src/config/bundled-skills/browser/tools/browser-screenshot.ts +5 -2
- package/src/config/bundled-skills/browser/tools/browser-scroll.ts +5 -2
- package/src/config/bundled-skills/browser/tools/browser-select-option.ts +5 -2
- package/src/config/bundled-skills/browser/tools/browser-snapshot.ts +5 -2
- package/src/config/bundled-skills/browser/tools/browser-type.ts +5 -2
- package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +13 -6
- package/src/config/bundled-skills/browser/tools/browser-wait-for.ts +5 -2
- package/src/config/bundled-skills/claude-code/TOOLS.json +4 -0
- package/src/config/bundled-skills/claude-code/tools/claude-code.ts +5 -2
- package/src/config/bundled-skills/computer-use/SKILL.md +2 -2
- package/src/config/bundled-skills/computer-use/tools/computer-use-click.ts +6 -3
- package/src/config/bundled-skills/computer-use/tools/computer-use-done.ts +6 -3
- package/src/config/bundled-skills/computer-use/tools/computer-use-double-click.ts +10 -3
- package/src/config/bundled-skills/computer-use/tools/computer-use-drag.ts +6 -3
- package/src/config/bundled-skills/computer-use/tools/computer-use-key.ts +6 -3
- package/src/config/bundled-skills/computer-use/tools/computer-use-open-app.ts +6 -3
- package/src/config/bundled-skills/computer-use/tools/computer-use-request-control.ts +10 -3
- package/src/config/bundled-skills/computer-use/tools/computer-use-respond.ts +6 -3
- package/src/config/bundled-skills/computer-use/tools/computer-use-right-click.ts +10 -3
- package/src/config/bundled-skills/computer-use/tools/computer-use-run-applescript.ts +10 -3
- package/src/config/bundled-skills/computer-use/tools/computer-use-scroll.ts +6 -3
- package/src/config/bundled-skills/computer-use/tools/computer-use-type-text.ts +6 -3
- package/src/config/bundled-skills/computer-use/tools/computer-use-wait.ts +6 -3
- package/src/config/bundled-skills/configure-settings/SKILL.md +28 -14
- package/src/config/bundled-skills/contacts/SKILL.md +446 -15
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +99 -20
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +74 -17
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +89 -26
- package/src/config/bundled-skills/document/tools/document-create.ts +5 -2
- package/src/config/bundled-skills/document/tools/document-update.ts +5 -2
- package/src/config/bundled-skills/doordash/doordash-cli.ts +17 -7
- package/src/config/bundled-skills/email-setup/SKILL.md +9 -9
- package/src/config/bundled-skills/followups/tools/followup-create.ts +5 -2
- package/src/config/bundled-skills/followups/tools/followup-list.ts +5 -2
- package/src/config/bundled-skills/followups/tools/followup-resolve.ts +5 -2
- package/src/config/bundled-skills/google-calendar/calendar-client.ts +44 -32
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +11 -5
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +13 -7
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +11 -5
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +13 -7
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +28 -12
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +6 -4
- package/src/config/bundled-skills/google-calendar/types.ts +3 -3
- package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +46 -24
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +36 -19
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +60 -35
- package/src/config/bundled-skills/mcp-setup/SKILL.md +75 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +55 -15
- package/src/config/bundled-skills/media-processing/TOOLS.json +20 -2
- package/src/config/bundled-skills/media-processing/__tests__/concurrency-pool.test.ts +12 -10
- package/src/config/bundled-skills/media-processing/__tests__/cost-tracker.test.ts +34 -19
- package/src/config/bundled-skills/media-processing/__tests__/preprocess.test.ts +82 -66
- package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +148 -0
- package/src/config/bundled-skills/media-processing/services/concurrency-pool.ts +1 -1
- package/src/config/bundled-skills/media-processing/services/cost-tracker.ts +8 -3
- package/src/config/bundled-skills/media-processing/services/gemini-map.ts +117 -53
- package/src/config/bundled-skills/media-processing/services/gemini-video.ts +273 -0
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +185 -97
- package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +32 -27
- package/src/config/bundled-skills/media-processing/services/reduce.ts +101 -24
- package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +121 -55
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +58 -24
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +177 -91
- package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +98 -70
- package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +59 -19
- package/src/config/bundled-skills/media-processing/tools/media-status.ts +26 -10
- package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +29 -14
- package/src/config/bundled-skills/messaging/SKILL.md +7 -5
- package/src/config/bundled-skills/messaging/TOOLS.json +7 -7
- package/src/config/bundled-skills/messaging/tools/gmail-archive-by-query.ts +31 -13
- package/src/config/bundled-skills/messaging/tools/gmail-archive.ts +16 -10
- package/src/config/bundled-skills/messaging/tools/gmail-batch-label.ts +18 -9
- package/src/config/bundled-skills/messaging/tools/gmail-download-attachment.ts +23 -16
- package/src/config/bundled-skills/messaging/tools/gmail-draft.ts +28 -12
- package/src/config/bundled-skills/messaging/tools/gmail-filters.ts +41 -21
- package/src/config/bundled-skills/messaging/tools/gmail-follow-up.ts +44 -23
- package/src/config/bundled-skills/messaging/tools/gmail-forward.ts +73 -33
- package/src/config/bundled-skills/messaging/tools/gmail-label.ts +15 -9
- package/src/config/bundled-skills/messaging/tools/gmail-list-attachments.ts +22 -14
- package/src/config/bundled-skills/messaging/tools/gmail-outreach-scan.ts +99 -50
- package/src/config/bundled-skills/messaging/tools/gmail-send-draft.ts +14 -8
- package/src/config/bundled-skills/messaging/tools/gmail-send-with-attachments.ts +63 -44
- package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +90 -46
- package/src/config/bundled-skills/messaging/tools/gmail-summarize-thread.ts +43 -22
- package/src/config/bundled-skills/messaging/tools/gmail-trash.ts +15 -9
- package/src/config/bundled-skills/messaging/tools/gmail-triage.ts +51 -22
- package/src/config/bundled-skills/messaging/tools/gmail-unsubscribe.ts +62 -26
- package/src/config/bundled-skills/messaging/tools/gmail-vacation.ts +34 -19
- package/src/config/bundled-skills/messaging/tools/google-contacts.ts +32 -16
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-activity.ts +10 -4
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +91 -47
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +21 -9
- package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +9 -3
- package/src/config/bundled-skills/messaging/tools/messaging-draft.ts +30 -17
- package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +10 -4
- package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +14 -6
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +16 -5
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +63 -36
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +10 -4
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +30 -12
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +48 -29
- package/src/config/bundled-skills/messaging/tools/scan-result-store.ts +20 -6
- package/src/config/bundled-skills/messaging/tools/send-notification.ts +1 -1
- package/src/config/bundled-skills/messaging/tools/sequence-analytics.ts +59 -22
- package/src/config/bundled-skills/messaging/tools/sequence-cancel.ts +13 -7
- package/src/config/bundled-skills/messaging/tools/sequence-create.ts +27 -12
- package/src/config/bundled-skills/messaging/tools/sequence-delete.ts +14 -6
- package/src/config/bundled-skills/messaging/tools/sequence-enroll.ts +30 -11
- package/src/config/bundled-skills/messaging/tools/sequence-enrollment-list.ts +16 -8
- package/src/config/bundled-skills/messaging/tools/sequence-get.ts +31 -13
- package/src/config/bundled-skills/messaging/tools/sequence-import.ts +38 -22
- package/src/config/bundled-skills/messaging/tools/sequence-list.ts +16 -7
- package/src/config/bundled-skills/messaging/tools/sequence-pause.ts +29 -10
- package/src/config/bundled-skills/messaging/tools/sequence-resume.ts +16 -8
- package/src/config/bundled-skills/messaging/tools/sequence-update.ts +35 -16
- package/src/config/bundled-skills/messaging/tools/shared.ts +26 -12
- package/src/config/bundled-skills/notifications/tools/send-notification.ts +69 -34
- package/src/config/bundled-skills/notifications/tools/shared.ts +1 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +46 -48
- package/src/config/bundled-skills/phone-calls/tools/call-end.ts +1 -1
- package/src/config/bundled-skills/phone-calls/tools/call-start.ts +1 -1
- package/src/config/bundled-skills/phone-calls/tools/call-status.ts +1 -1
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +91 -51
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +30 -16
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +66 -27
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +89 -42
- package/src/config/bundled-skills/public-ingress/SKILL.md +26 -19
- package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +5 -2
- package/src/config/bundled-skills/reminder/tools/reminder-create.ts +5 -2
- package/src/config/bundled-skills/reminder/tools/reminder-list.ts +5 -2
- package/src/config/bundled-skills/schedule/tools/schedule-create.ts +5 -2
- package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +5 -2
- package/src/config/bundled-skills/schedule/tools/schedule-list.ts +5 -2
- package/src/config/bundled-skills/schedule/tools/schedule-update.ts +5 -2
- package/src/config/bundled-skills/screen-recording/SKILL.md +11 -3
- package/src/config/bundled-skills/self-upgrade/SKILL.md +9 -8
- package/src/config/bundled-skills/slack/TOOLS.json +33 -15
- package/src/config/bundled-skills/slack/tools/shared.ts +7 -5
- package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +11 -5
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +11 -5
- package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +46 -16
- package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +11 -5
- package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +28 -0
- package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +12 -6
- package/src/config/bundled-skills/sms-setup/SKILL.md +5 -8
- package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +5 -2
- package/src/config/bundled-skills/subagent/tools/subagent-message.ts +5 -2
- package/src/config/bundled-skills/subagent/tools/subagent-read.ts +5 -2
- package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +5 -2
- package/src/config/bundled-skills/subagent/tools/subagent-status.ts +5 -2
- package/src/config/bundled-skills/tasks/tools/task-delete.ts +5 -2
- package/src/config/bundled-skills/tasks/tools/task-list-add.ts +5 -2
- package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +5 -2
- package/src/config/bundled-skills/tasks/tools/task-list-show.ts +5 -2
- package/src/config/bundled-skills/tasks/tools/task-list-update.ts +5 -2
- package/src/config/bundled-skills/tasks/tools/task-list.ts +5 -2
- package/src/config/bundled-skills/tasks/tools/task-queue-run.ts +5 -2
- package/src/config/bundled-skills/tasks/tools/task-run.ts +5 -2
- package/src/config/bundled-skills/tasks/tools/task-save.ts +5 -2
- package/src/config/bundled-skills/telegram-setup/SKILL.md +7 -8
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +232 -127
- package/src/config/bundled-skills/twilio-setup/SKILL.md +7 -12
- package/src/config/bundled-skills/twitter/SKILL.md +19 -2
- package/src/config/bundled-skills/voice-setup/SKILL.md +5 -5
- package/src/config/bundled-skills/watcher/tools/watcher-create.ts +5 -2
- package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +5 -2
- package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +5 -2
- package/src/config/bundled-skills/watcher/tools/watcher-list.ts +5 -2
- package/src/config/bundled-skills/watcher/tools/watcher-update.ts +5 -2
- package/src/config/bundled-skills/weather/tools/get-weather.ts +5 -2
- package/src/config/calls-schema.ts +108 -63
- package/src/config/computer-use-prompt.ts +7 -7
- package/src/config/core-schema.ts +239 -155
- package/src/config/defaults.ts +2 -2
- package/src/config/elevenlabs-schema.ts +15 -15
- package/src/config/env-registry.ts +33 -33
- package/src/config/feature-flag-registry.json +31 -7
- package/src/config/loader.ts +118 -58
- package/src/config/mcp-schema.ts +29 -15
- package/src/config/memory-schema.ts +434 -229
- package/src/config/notifications-schema.ts +4 -4
- package/src/config/sandbox-schema.ts +2 -2
- package/src/config/schema.ts +12 -2
- package/src/config/skill-state.ts +27 -15
- package/src/config/skills-schema.ts +72 -23
- package/src/config/skills.ts +303 -143
- package/src/config/system-prompt.ts +25 -6
- package/src/config/types.ts +1 -1
- package/src/config/update-bulletin-format.ts +3 -3
- package/src/config/update-bulletin-state.ts +15 -6
- package/src/config/update-bulletin-template-path.ts +8 -4
- package/src/config/update-bulletin.ts +33 -14
- package/src/config/user-reference.ts +8 -8
- package/src/contacts/contact-events.ts +21 -0
- package/src/contacts/contact-store.ts +622 -100
- package/src/contacts/contacts-write.ts +287 -0
- package/src/contacts/index.ts +13 -4
- package/src/contacts/startup-migration.ts +21 -0
- package/src/contacts/types.ts +47 -2
- package/src/context/token-estimator.ts +54 -31
- package/src/context/tool-result-truncation.ts +41 -7
- package/src/context/window-manager.ts +225 -120
- package/src/daemon/approval-generators.ts +83 -55
- package/src/daemon/approved-devices-store.ts +33 -20
- package/src/daemon/assistant-attachments.ts +134 -98
- package/src/daemon/auth-manager.ts +17 -15
- package/src/daemon/classifier.ts +117 -46
- package/src/daemon/computer-use-session.ts +316 -187
- package/src/daemon/config-watcher.ts +91 -44
- package/src/daemon/connection-policy.ts +18 -10
- package/src/daemon/context-overflow-approval.ts +48 -0
- package/src/daemon/context-overflow-policy.ts +50 -0
- package/src/daemon/context-overflow-reducer.ts +300 -0
- package/src/daemon/daemon-control.ts +79 -51
- package/src/daemon/date-context.ts +119 -69
- package/src/daemon/dictation-profile-store.ts +94 -48
- package/src/daemon/dictation-text-processing.ts +33 -12
- package/src/daemon/doordash-steps.ts +92 -49
- package/src/daemon/guardian-action-generators.ts +62 -46
- package/src/daemon/guardian-verification-intent.ts +31 -18
- package/src/daemon/handlers/apps.ts +257 -111
- package/src/daemon/handlers/avatar.ts +20 -15
- package/src/daemon/handlers/computer-use.ts +82 -39
- package/src/daemon/handlers/config-channels.ts +146 -69
- package/src/daemon/handlers/config-heartbeat.ts +114 -59
- package/src/daemon/handlers/config-inbox.ts +277 -106
- package/src/daemon/handlers/config-ingress.ts +127 -55
- package/src/daemon/handlers/config-integrations.ts +145 -88
- package/src/daemon/handlers/config-model.ts +58 -22
- package/src/daemon/handlers/config-platform.ts +40 -16
- package/src/daemon/handlers/config-scheduling.ts +109 -48
- package/src/daemon/handlers/config-slack-channel.ts +67 -35
- package/src/daemon/handlers/config-slack.ts +21 -20
- package/src/daemon/handlers/config-telegram.ts +100 -70
- package/src/daemon/handlers/config-tools.ts +103 -55
- package/src/daemon/handlers/config-trust.ts +50 -20
- package/src/daemon/handlers/config.ts +72 -24
- package/src/daemon/handlers/contacts.ts +163 -0
- package/src/daemon/handlers/diagnostics.ts +90 -48
- package/src/daemon/handlers/documents.ts +74 -46
- package/src/daemon/handlers/guardian-actions.ts +118 -71
- package/src/daemon/handlers/home-base.ts +19 -16
- package/src/daemon/handlers/identity.ts +65 -45
- package/src/daemon/handlers/index.ts +78 -54
- package/src/daemon/handlers/misc.ts +664 -234
- package/src/daemon/handlers/navigate-settings.ts +14 -11
- package/src/daemon/handlers/oauth-connect.ts +48 -35
- package/src/daemon/handlers/open-bundle-handler.ts +31 -24
- package/src/daemon/handlers/pairing.ts +51 -25
- package/src/daemon/handlers/publish.ts +55 -33
- package/src/daemon/handlers/recording.ts +378 -162
- package/src/daemon/handlers/sessions.ts +923 -423
- package/src/daemon/handlers/shared.ts +202 -117
- package/src/daemon/handlers/signing.ts +25 -6
- package/src/daemon/handlers/subagents.ts +117 -56
- package/src/daemon/handlers/twitter-auth.ts +70 -49
- package/src/daemon/handlers/work-items.ts +264 -112
- package/src/daemon/handlers/workspace-files.ts +27 -20
- package/src/daemon/handlers.ts +2 -2
- package/src/daemon/history-repair.ts +16 -15
- package/src/daemon/identity-helpers.ts +4 -4
- package/src/daemon/install-cli-launchers.ts +33 -22
- package/src/daemon/ipc-blob-store.ts +38 -24
- package/src/daemon/ipc-contract/apps.ts +61 -49
- package/src/daemon/ipc-contract/computer-use.ts +47 -37
- package/src/daemon/ipc-contract/contacts.ts +69 -0
- package/src/daemon/ipc-contract/diagnostics.ts +14 -14
- package/src/daemon/ipc-contract/documents.ts +8 -8
- package/src/daemon/ipc-contract/guardian-actions.ts +4 -4
- package/src/daemon/ipc-contract/inbox.ts +16 -16
- package/src/daemon/ipc-contract/integrations.ts +57 -44
- package/src/daemon/ipc-contract/memory.ts +3 -5
- package/src/daemon/ipc-contract/messages.ts +95 -69
- package/src/daemon/ipc-contract/notifications.ts +10 -6
- package/src/daemon/ipc-contract/pairing.ts +8 -8
- package/src/daemon/ipc-contract/schedules.ts +20 -20
- package/src/daemon/ipc-contract/sessions.ts +88 -57
- package/src/daemon/ipc-contract/settings.ts +12 -7
- package/src/daemon/ipc-contract/shared.ts +9 -7
- package/src/daemon/ipc-contract/skills.ts +46 -26
- package/src/daemon/ipc-contract/subagents.ts +9 -9
- package/src/daemon/ipc-contract/trust.ts +11 -11
- package/src/daemon/ipc-contract/work-items.ts +33 -28
- package/src/daemon/ipc-contract/workspace.ts +28 -21
- package/src/daemon/ipc-contract-inventory.json +8 -0
- package/src/daemon/ipc-contract-inventory.ts +29 -26
- package/src/daemon/ipc-contract.ts +111 -44
- package/src/daemon/ipc-handler.ts +27 -19
- package/src/daemon/ipc-protocol.ts +22 -12
- package/src/daemon/ipc-validate.ts +91 -46
- package/src/daemon/lifecycle.ts +25 -1
- package/src/daemon/main.ts +10 -8
- package/src/daemon/media-visibility-policy.ts +3 -1
- package/src/daemon/pairing-store.ts +72 -40
- package/src/daemon/providers-setup.ts +35 -25
- package/src/daemon/recording-executor.ts +37 -30
- package/src/daemon/recording-intent-fallback.ts +58 -28
- package/src/daemon/recording-intent.ts +71 -61
- package/src/daemon/ride-shotgun-handler.ts +201 -121
- package/src/daemon/seed-files.ts +28 -17
- package/src/daemon/server.ts +23 -14
- package/src/daemon/session-agent-loop-handlers.ts +261 -135
- package/src/daemon/session-agent-loop.ts +795 -253
- package/src/daemon/session-attachments.ts +104 -39
- package/src/daemon/session-conflict-gate.ts +72 -28
- package/src/daemon/session-dynamic-profile.ts +36 -22
- package/src/daemon/session-error.ts +50 -45
- package/src/daemon/session-evictor.ts +17 -10
- package/src/daemon/session-history.ts +201 -89
- package/src/daemon/session-lifecycle.ts +79 -42
- package/src/daemon/session-media-retry.ts +89 -41
- package/src/daemon/session-memory.ts +77 -55
- package/src/daemon/session-messaging.ts +261 -111
- package/src/daemon/session-notifiers.ts +57 -45
- package/src/daemon/session-process.ts +370 -154
- package/src/daemon/session-queue-manager.ts +30 -13
- package/src/daemon/session-runtime-assembly.ts +61 -15
- package/src/daemon/session-skill-tools.ts +84 -36
- package/src/daemon/session-slash.ts +178 -113
- package/src/daemon/session-surfaces.ts +498 -211
- package/src/daemon/session-tool-setup.ts +22 -17
- package/src/daemon/session-usage.ts +26 -13
- package/src/daemon/session-workspace.ts +7 -4
- package/src/daemon/session.ts +18 -19
- package/src/daemon/shutdown-handlers.ts +36 -33
- package/src/daemon/tls-certs.ts +90 -57
- package/src/daemon/tool-side-effects.ts +97 -65
- package/src/daemon/trace-emitter.ts +8 -7
- package/src/daemon/video-thumbnail.ts +55 -25
- package/src/daemon/watch-handler.ts +164 -86
- package/src/email/provider.ts +1 -1
- package/src/email/providers/agentmail.ts +87 -45
- package/src/email/providers/index.ts +19 -14
- package/src/email/service.ts +52 -24
- package/src/email/types.ts +2 -2
- package/src/errors.ts +1 -1
- package/src/events/bus.ts +30 -10
- package/src/events/domain-events.ts +19 -13
- package/src/events/index.ts +6 -6
- package/src/events/tool-audit-listener.ts +34 -20
- package/src/events/tool-domain-event-publisher.ts +22 -20
- package/src/events/tool-metrics-listener.ts +26 -21
- package/src/events/tool-notification-listener.ts +5 -5
- package/src/events/tool-profiling-listener.ts +33 -23
- package/src/events/tool-trace-listener.ts +70 -46
- package/src/export/formatter.ts +38 -32
- package/src/followups/followup-store.ts +43 -36
- package/src/followups/index.ts +2 -2
- package/src/followups/types.ts +1 -1
- package/src/gallery/default-gallery.ts +37 -34
- package/src/gallery/gallery-manifest.ts +9 -9
- package/src/heartbeat/heartbeat-service.ts +59 -37
- package/src/home-base/app-link-store.ts +14 -12
- package/src/home-base/bootstrap.ts +14 -8
- package/src/home-base/prebuilt/seed.ts +35 -26
- package/src/home-base/prebuilt-home-base-updater.ts +14 -8
- package/src/hooks/cli.ts +56 -43
- package/src/hooks/config.ts +27 -14
- package/src/hooks/discovery.ts +53 -33
- package/src/hooks/manager.ts +50 -26
- package/src/hooks/runner.ts +35 -29
- package/src/hooks/templates.ts +38 -15
- package/src/hooks/types.ts +13 -13
- package/src/inbound/platform-callback-registration.ts +21 -15
- package/src/inbound/public-ingress-urls.ts +9 -6
- package/src/index.ts +20 -19
- package/src/influencer/client.ts +269 -108
- package/src/instrument.ts +3 -1
- package/src/logfire.ts +64 -39
- package/src/mcp/client.ts +107 -55
- package/src/mcp/manager.ts +45 -18
- package/src/mcp/mcp-oauth-provider.ts +114 -62
- package/src/media/gemini-image-service.ts +28 -21
- package/src/memory/account-store.ts +16 -9
- package/src/memory/admin.ts +87 -57
- package/src/memory/app-git-service.ts +77 -47
- package/src/memory/app-store.ts +151 -77
- package/src/memory/attachments-store.ts +123 -53
- package/src/memory/canonical-guardian-store.ts +190 -48
- package/src/memory/channel-delivery-store.ts +5 -5
- package/src/memory/channel-guardian-store.ts +31 -16
- package/src/memory/checkpoints.ts +14 -7
- package/src/memory/clarification-resolver.ts +219 -104
- package/src/memory/conflict-intent.ts +74 -23
- package/src/memory/conflict-policy.ts +20 -7
- package/src/memory/conflict-store.ts +144 -94
- package/src/memory/contradiction-checker.ts +257 -132
- package/src/memory/conversation-attention-store.ts +72 -32
- package/src/memory/conversation-bootstrap.ts +28 -0
- package/src/memory/conversation-crud.ts +12 -5
- package/src/memory/conversation-display-order-migration.ts +7 -7
- package/src/memory/conversation-key-store.ts +18 -13
- package/src/memory/conversation-queries.ts +130 -52
- package/src/memory/conversation-store.ts +43 -26
- package/src/memory/conversation-title-service.ts +89 -66
- package/src/memory/db-init.ts +90 -2
- package/src/memory/db.ts +10 -3
- package/src/memory/delivery-channels.ts +12 -6
- package/src/memory/delivery-crud.ts +26 -12
- package/src/memory/delivery-status.ts +19 -16
- package/src/memory/embedding-backend.ts +205 -77
- package/src/memory/embedding-gemini.ts +23 -10
- package/src/memory/embedding-local.ts +89 -44
- package/src/memory/embedding-ollama.ts +25 -13
- package/src/memory/embedding-openai.ts +20 -11
- package/src/memory/embedding-runtime-manager.ts +163 -90
- package/src/memory/entity-extractor.ts +185 -123
- package/src/memory/external-conversation-store.ts +30 -12
- package/src/memory/fingerprint.ts +2 -2
- package/src/memory/fts-reconciler.ts +57 -28
- package/src/memory/guardian-action-store.ts +162 -100
- package/src/memory/guardian-approvals.ts +63 -129
- package/src/memory/guardian-rate-limits.ts +20 -9
- package/src/memory/guardian-verification.ts +82 -35
- package/src/memory/indexer.ts +96 -55
- package/src/memory/ingress-invite-store.ts +28 -169
- package/src/memory/items-extractor.ts +313 -157
- package/src/memory/job-handlers/backfill.ts +116 -63
- package/src/memory/job-handlers/cleanup.ts +64 -41
- package/src/memory/job-handlers/conflict.ts +90 -49
- package/src/memory/job-handlers/embedding.ts +32 -17
- package/src/memory/job-handlers/extraction.ts +58 -33
- package/src/memory/job-handlers/index-maintenance.ts +31 -17
- package/src/memory/job-handlers/media-processing.ts +65 -24
- package/src/memory/job-handlers/summarization.ts +186 -128
- package/src/memory/job-utils.ts +100 -57
- package/src/memory/jobs-store.ts +235 -142
- package/src/memory/jobs-worker.ts +167 -83
- package/src/memory/llm-request-log-store.ts +13 -11
- package/src/memory/llm-usage-store.ts +35 -26
- package/src/memory/media-store.ts +151 -44
- package/src/memory/message-content.ts +28 -18
- package/src/memory/migrations/001-job-deferrals.ts +11 -5
- package/src/memory/migrations/002-tool-invocations-fk.ts +14 -6
- package/src/memory/migrations/003-memory-fts-backfill.ts +11 -5
- package/src/memory/migrations/004-entity-relation-dedup.ts +17 -11
- package/src/memory/migrations/005-fingerprint-scope-unique.ts +36 -21
- package/src/memory/migrations/006-scope-salted-fingerprints.ts +35 -20
- package/src/memory/migrations/007-assistant-id-to-self.ts +40 -27
- package/src/memory/migrations/008-remove-assistant-id-columns.ts +58 -36
- package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +36 -22
- package/src/memory/migrations/010-ext-conv-bindings-channel-chat-unique.ts +21 -11
- package/src/memory/migrations/011-call-sessions-provider-sid-dedup.ts +30 -15
- package/src/memory/migrations/012-call-sessions-add-initiated-from.ts +4 -2
- package/src/memory/migrations/013-guardian-action-tables.ts +29 -11
- package/src/memory/migrations/014-backfill-inbox-thread-state.ts +35 -21
- package/src/memory/migrations/015-drop-active-search-index.ts +17 -11
- package/src/memory/migrations/016-memory-segments-indexes.ts +7 -3
- package/src/memory/migrations/017-memory-items-indexes.ts +4 -2
- package/src/memory/migrations/018-remaining-table-indexes.ts +13 -5
- package/src/memory/migrations/019-notification-tables-schema-migration.ts +34 -20
- package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +87 -53
- package/src/memory/migrations/021-conversation-status-indexes.ts +7 -3
- package/src/memory/migrations/022-add-origin-interface.ts +4 -2
- package/src/memory/migrations/023-memory-item-sources-indexes.ts +4 -2
- package/src/memory/migrations/024-embedding-vector-blob.ts +34 -18
- package/src/memory/migrations/025-messages-fts-backfill.ts +11 -5
- package/src/memory/migrations/026-guardian-verification-sessions.ts +80 -14
- package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +42 -26
- package/src/memory/migrations/027-notification-delivery-pairing-columns.ts +22 -8
- package/src/memory/migrations/027a-guardian-bootstrap-token.ts +11 -3
- package/src/memory/migrations/028-call-session-mode.ts +13 -3
- package/src/memory/migrations/028-notification-delivery-client-ack.ts +22 -8
- package/src/memory/migrations/029-channel-inbound-delivered-segments.ts +7 -3
- package/src/memory/migrations/030-guardian-action-followup.ts +46 -8
- package/src/memory/migrations/030-guardian-verification-purpose.ts +4 -2
- package/src/memory/migrations/031-conversations-thread-type-index.ts +4 -2
- package/src/memory/migrations/032-guardian-delivery-conversation-index.ts +4 -2
- package/src/memory/migrations/032-notification-delivery-thread-decision.ts +22 -8
- package/src/memory/migrations/033-scoped-approval-grants.ts +1 -1
- package/src/memory/migrations/034-guardian-action-tool-metadata.ts +15 -3
- package/src/memory/migrations/035-guardian-action-supersession.ts +15 -3
- package/src/memory/migrations/036-normalize-phone-identities.ts +101 -87
- package/src/memory/migrations/037-voice-invite-columns.ts +22 -4
- package/src/memory/migrations/038-actor-token-records.ts +5 -9
- package/src/memory/migrations/039-actor-refresh-token-records.ts +7 -13
- package/src/memory/migrations/100-core-tables.ts +1 -1
- package/src/memory/migrations/101-watchers-and-logs.ts +1 -1
- package/src/memory/migrations/103-complex-migrations.ts +9 -9
- package/src/memory/migrations/104-core-indexes.ts +188 -64
- package/src/memory/migrations/105-contacts-and-triage.ts +28 -10
- package/src/memory/migrations/106-call-sessions.ts +58 -16
- package/src/memory/migrations/107-followups.ts +16 -6
- package/src/memory/migrations/108-tasks-and-work-items.ts +43 -11
- package/src/memory/migrations/109-external-conversation-bindings.ts +11 -5
- package/src/memory/migrations/110-channel-guardian.ts +48 -10
- package/src/memory/migrations/111-media-assets.ts +52 -18
- package/src/memory/migrations/112-assistant-inbox.ts +32 -12
- package/src/memory/migrations/113-late-migrations.ts +12 -12
- package/src/memory/migrations/114-notifications.ts +28 -12
- package/src/memory/migrations/115-sequences.ts +10 -4
- package/src/memory/migrations/116-messages-fts.ts +1 -1
- package/src/memory/migrations/117-conversation-attention.ts +16 -6
- package/src/memory/migrations/118-reminder-routing-intent.ts +7 -3
- package/src/memory/migrations/119-schema-indexes-and-columns.ts +35 -15
- package/src/memory/migrations/120-fk-cascade-rebuilds.ts +36 -17
- package/src/memory/migrations/121-canonical-guardian-requests.ts +25 -9
- package/src/memory/migrations/122-canonical-guardian-requester-chat-id.ts +11 -3
- package/src/memory/migrations/123-canonical-guardian-deliveries-destination-index.ts +4 -2
- package/src/memory/migrations/124-voice-invite-display-metadata.ts +15 -3
- package/src/memory/migrations/125-guardian-principal-id-columns.ts +22 -4
- package/src/memory/migrations/126-backfill-guardian-principal-id.ts +174 -126
- package/src/memory/migrations/127-guardian-principal-id-not-null.ts +58 -42
- package/src/memory/migrations/128-contacts-role-principal.ts +26 -0
- package/src/memory/migrations/129-contact-channels-access-fields.ts +105 -0
- package/src/memory/migrations/130-contact-channels-type-ext-chat-id-index.ts +15 -0
- package/src/memory/migrations/131-drop-legacy-member-guardian-tables.ts +134 -0
- package/src/memory/migrations/132-contacts-assistant-id.ts +21 -0
- package/src/memory/migrations/index.ts +82 -73
- package/src/memory/migrations/registry.ts +53 -37
- package/src/memory/migrations/validate-migration-state.ts +73 -46
- package/src/memory/profile-compiler.ts +58 -24
- package/src/memory/published-pages-store.ts +12 -16
- package/src/memory/qdrant-circuit-breaker.ts +28 -20
- package/src/memory/qdrant-client.ts +99 -63
- package/src/memory/qdrant-manager.ts +89 -57
- package/src/memory/query-builder.ts +9 -7
- package/src/memory/raw-query.ts +63 -14
- package/src/memory/recall-cache.ts +15 -8
- package/src/memory/retrieval-budget.ts +0 -1
- package/src/memory/retriever.ts +385 -192
- package/src/memory/schema-migration.ts +1 -1
- package/src/memory/schema.ts +44 -56
- package/src/memory/scoped-approval-grants.ts +99 -45
- package/src/memory/search/entity.ts +102 -40
- package/src/memory/search/formatting.ts +70 -52
- package/src/memory/search/lexical.ts +82 -43
- package/src/memory/search/ranking.ts +103 -39
- package/src/memory/search/semantic.ts +59 -35
- package/src/memory/search/types.ts +8 -8
- package/src/memory/segmenter.ts +20 -12
- package/src/memory/shared-app-links-store.ts +21 -16
- package/src/memory/task-memory-cleanup.ts +18 -8
- package/src/memory/tool-usage-store.ts +27 -19
- package/src/memory/validation.ts +4 -2
- package/src/messaging/activity-analyzer.ts +7 -7
- package/src/messaging/draft-store.ts +13 -10
- package/src/messaging/email-classifier.ts +73 -37
- package/src/messaging/index.ts +3 -3
- package/src/messaging/outreach-classifier.ts +76 -38
- package/src/messaging/provider-types.ts +2 -4
- package/src/messaging/provider.ts +37 -8
- package/src/messaging/providers/gmail/adapter.ts +183 -66
- package/src/messaging/providers/gmail/client.ts +3 -1
- package/src/messaging/providers/gmail/mime-builder.ts +21 -19
- package/src/messaging/providers/gmail/people-client.ts +22 -9
- package/src/messaging/providers/gmail/types.ts +6 -6
- package/src/messaging/providers/slack/adapter.ts +93 -43
- package/src/messaging/providers/slack/client.ts +100 -41
- package/src/messaging/providers/slack/types.ts +6 -0
- package/src/messaging/providers/sms/adapter.ts +76 -40
- package/src/messaging/providers/sms/client.ts +4 -4
- package/src/messaging/providers/telegram-bot/adapter.ts +52 -30
- package/src/messaging/providers/telegram-bot/client.ts +7 -7
- package/src/messaging/providers/whatsapp/adapter.ts +58 -31
- package/src/messaging/providers/whatsapp/client.ts +4 -4
- package/src/messaging/registry.ts +9 -5
- package/src/messaging/style-analyzer.ts +69 -39
- package/src/messaging/thread-summarizer.ts +101 -53
- package/src/messaging/triage-engine.ts +111 -82
- package/src/messaging/types.ts +10 -10
- package/src/migrations/config-merge.ts +18 -10
- package/src/migrations/data-layout.ts +35 -22
- package/src/migrations/data-merge.ts +17 -7
- package/src/migrations/hooks-merge.ts +43 -16
- package/src/migrations/index.ts +6 -6
- package/src/migrations/log.ts +9 -5
- package/src/migrations/skills-merge.ts +17 -7
- package/src/migrations/workspace-layout.ts +39 -25
- package/src/notifications/AGENTS.md +5 -0
- package/src/notifications/adapters/macos.ts +21 -14
- package/src/notifications/adapters/sms.ts +28 -15
- package/src/notifications/adapters/telegram.ts +24 -15
- package/src/notifications/broadcaster.ts +108 -52
- package/src/notifications/conversation-pairing.ts +64 -29
- package/src/notifications/copy-composer.ts +165 -95
- package/src/notifications/decision-engine.ts +353 -147
- package/src/notifications/decisions-store.ts +26 -10
- package/src/notifications/deliveries-store.ts +23 -13
- package/src/notifications/destination-resolver.ts +42 -24
- package/src/notifications/deterministic-checks.ts +78 -27
- package/src/notifications/emit-signal.ts +83 -45
- package/src/notifications/events-store.ts +13 -7
- package/src/notifications/guardian-question-mode.ts +125 -75
- package/src/notifications/preference-extractor.ts +85 -53
- package/src/notifications/preference-summary.ts +31 -18
- package/src/notifications/preferences-store.ts +29 -18
- package/src/notifications/runtime-dispatch.ts +22 -12
- package/src/notifications/signal.ts +4 -4
- package/src/notifications/thread-candidates.ts +59 -23
- package/src/notifications/thread-seed-composer.ts +45 -27
- package/src/notifications/types.ts +19 -10
- package/src/oauth/connect-orchestrator.ts +105 -54
- package/src/oauth/connect-types.ts +3 -3
- package/src/oauth/provider-profiles.ts +80 -59
- package/src/oauth/scope-policy.ts +5 -2
- package/src/oauth/token-persistence.ts +58 -24
- package/src/outbound-proxy/certs.ts +284 -0
- package/src/outbound-proxy/config.ts +94 -0
- package/src/outbound-proxy/connect-tunnel.ts +84 -0
- package/src/outbound-proxy/health.ts +62 -0
- package/src/outbound-proxy/host-pattern-match.ts +67 -0
- package/src/outbound-proxy/http-forwarder.ts +162 -0
- package/src/outbound-proxy/index.ts +80 -0
- package/src/outbound-proxy/logging.ts +193 -0
- package/src/outbound-proxy/mitm-handler.ts +292 -0
- package/src/outbound-proxy/policy.ts +172 -0
- package/src/outbound-proxy/router.ts +64 -0
- package/src/outbound-proxy/server.ts +145 -0
- package/src/outbound-proxy/types.ts +150 -0
- package/src/permissions/checker.ts +481 -189
- package/src/permissions/defaults.ts +135 -108
- package/src/permissions/prompter.ts +53 -27
- package/src/permissions/secret-prompter.ts +21 -15
- package/src/permissions/shell-identity.ts +47 -16
- package/src/permissions/trust-store.ts +185 -73
- package/src/permissions/types.ts +22 -12
- package/src/permissions/workspace-policy.ts +47 -38
- package/src/playbooks/index.ts +10 -2
- package/src/playbooks/playbook-compiler.ts +30 -24
- package/src/playbooks/types.ts +11 -8
- package/src/providers/anthropic/client.ts +325 -168
- package/src/providers/failover.ts +57 -22
- package/src/providers/fireworks/client.ts +9 -5
- package/src/providers/gemini/client.ts +61 -39
- package/src/providers/model-intents.ts +40 -33
- package/src/providers/ollama/client.ts +7 -7
- package/src/providers/openai/client.ts +106 -68
- package/src/providers/openrouter/client.ts +9 -5
- package/src/providers/provider-send-message.ts +59 -27
- package/src/providers/ratelimit.ts +25 -8
- package/src/providers/registry.ts +86 -38
- package/src/providers/retry.ts +84 -36
- package/src/providers/stream-timeout.ts +5 -3
- package/src/providers/types.ts +7 -6
- package/src/runtime/AGENTS.md +42 -0
- package/src/runtime/access-request-helper.ts +118 -68
- package/src/runtime/actor-refresh-token-store.ts +21 -16
- package/src/runtime/actor-token-store.ts +25 -18
- package/src/runtime/actor-trust-resolver.ts +183 -80
- package/src/runtime/approval-conversation-turn.ts +39 -26
- package/src/runtime/approval-message-composer.ts +116 -84
- package/src/runtime/assistant-event-hub.ts +25 -6
- package/src/runtime/assistant-event.ts +4 -4
- package/src/runtime/assistant-scope.ts +1 -1
- package/src/runtime/auth/__tests__/guard-tests.test.ts +36 -14
- package/src/runtime/auth/context.ts +8 -7
- package/src/runtime/auth/credential-service.ts +60 -38
- package/src/runtime/auth/external-assistant-id.ts +16 -8
- package/src/runtime/auth/index.ts +23 -16
- package/src/runtime/auth/route-policy.ts +170 -104
- package/src/runtime/auth/scopes.ts +22 -29
- package/src/runtime/auth/subject.ts +19 -13
- package/src/runtime/auth/token-service.ts +3 -3
- package/src/runtime/auth/types.ts +23 -23
- package/src/runtime/channel-approval-parser.ts +37 -14
- package/src/runtime/channel-approval-types.ts +12 -4
- package/src/runtime/channel-approvals.ts +41 -23
- package/src/runtime/channel-guardian-service.ts +144 -103
- package/src/runtime/channel-invite-transport.ts +4 -2
- package/src/runtime/channel-invite-transports/telegram.ts +16 -10
- package/src/runtime/channel-invite-transports/voice.ts +7 -7
- package/src/runtime/channel-readiness-service.ts +139 -90
- package/src/runtime/channel-readiness-types.ts +4 -2
- package/src/runtime/channel-reply-delivery.ts +21 -11
- package/src/runtime/channel-retry-sweep.ts +111 -62
- package/src/runtime/confirmation-request-guardian-bridge.ts +73 -54
- package/src/runtime/gateway-client.ts +86 -53
- package/src/runtime/guardian-action-conversation-turn.ts +34 -18
- package/src/runtime/guardian-action-followup-executor.ts +115 -45
- package/src/runtime/guardian-action-grant-minter.ts +40 -24
- package/src/runtime/guardian-action-message-composer.ts +105 -84
- package/src/runtime/guardian-decision-types.ts +28 -13
- package/src/runtime/guardian-outbound-actions.ts +9 -0
- package/src/runtime/guardian-reply-router.ts +274 -145
- package/src/runtime/guardian-vellum-migration.ts +38 -24
- package/src/runtime/guardian-verification-templates.ts +8 -11
- package/src/runtime/http-router.ts +175 -0
- package/src/runtime/http-server.ts +931 -669
- package/src/runtime/http-types.ts +2 -2
- package/src/runtime/ingress-service.ts +182 -89
- package/src/runtime/invite-redemption-service.ts +211 -134
- package/src/runtime/invite-redemption-templates.ts +18 -11
- package/src/runtime/local-actor-identity.ts +73 -55
- package/src/runtime/middleware/auth.ts +25 -14
- package/src/runtime/middleware/error-handler.ts +15 -11
- package/src/runtime/middleware/rate-limiter.ts +23 -17
- package/src/runtime/middleware/request-logger.ts +4 -4
- package/src/runtime/middleware/twilio-validation.ts +29 -20
- package/src/runtime/migrations/migration-transport.ts +575 -0
- package/src/runtime/migrations/migration-wizard.ts +715 -0
- package/src/runtime/migrations/rebind-secrets-screen.ts +351 -0
- package/src/runtime/migrations/transfer-progress-screen.ts +321 -0
- package/src/runtime/migrations/validation-results-screen.ts +467 -0
- package/src/runtime/migrations/vbundle-builder.ts +295 -0
- package/src/runtime/migrations/vbundle-import-analyzer.ts +212 -0
- package/src/runtime/migrations/vbundle-importer.ts +339 -0
- package/src/runtime/migrations/vbundle-validator.ts +356 -0
- package/src/runtime/pending-interactions.ts +16 -7
- package/src/runtime/routes/access-request-decision.ts +73 -52
- package/src/runtime/routes/app-routes.ts +56 -38
- package/src/runtime/routes/approval-routes.ts +165 -74
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +930 -0
- package/src/runtime/routes/approval-strategies/guardian-legacy-fallback-strategy.ts +82 -0
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +151 -0
- package/src/runtime/routes/attachment-routes.ts +59 -48
- package/src/runtime/routes/brain-graph-routes.ts +85 -69
- package/src/runtime/routes/call-routes.ts +79 -38
- package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +10 -10
- package/src/runtime/routes/channel-delivery-routes.ts +19 -14
- package/src/runtime/routes/channel-guardian-routes.ts +3 -3
- package/src/runtime/routes/channel-inbound-routes.ts +2 -2
- package/src/runtime/routes/channel-readiness-routes.ts +12 -6
- package/src/runtime/routes/channel-route-shared.ts +33 -25
- package/src/runtime/routes/channel-routes.ts +4 -6
- package/src/runtime/routes/contact-routes.ts +205 -16
- package/src/runtime/routes/conversation-attention-routes.ts +57 -28
- package/src/runtime/routes/conversation-routes.ts +321 -174
- package/src/runtime/routes/debug-routes.ts +14 -10
- package/src/runtime/routes/events-routes.ts +90 -57
- package/src/runtime/routes/global-search-routes.ts +266 -0
- package/src/runtime/routes/guardian-action-routes.ts +147 -56
- package/src/runtime/routes/guardian-approval-interception.ts +255 -880
- package/src/runtime/routes/guardian-approval-prompt.ts +40 -24
- package/src/runtime/routes/guardian-approval-reply-helpers.ts +135 -0
- package/src/runtime/routes/guardian-bootstrap-routes.ts +55 -36
- package/src/runtime/routes/guardian-expiry-sweep.ts +63 -37
- package/src/runtime/routes/guardian-refresh-routes.ts +40 -19
- package/src/runtime/routes/identity-routes.ts +71 -42
- package/src/runtime/routes/inbound-conversation.ts +17 -11
- package/src/runtime/routes/inbound-message-handler.ts +278 -1460
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +658 -0
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +492 -0
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +214 -0
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +116 -0
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +167 -0
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +185 -0
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +132 -0
- package/src/runtime/routes/inbound-stages/verification-intercept.ts +340 -0
- package/src/runtime/routes/ingress-routes.ts +34 -23
- package/src/runtime/routes/integration-routes.ts +60 -21
- package/src/runtime/routes/migration-routes.ts +434 -0
- package/src/runtime/routes/pairing-routes.ts +157 -79
- package/src/runtime/routes/secret-routes.ts +6 -2
- package/src/runtime/routes/twilio-routes.ts +443 -249
- package/src/runtime/tool-grant-request-helper.ts +36 -27
- package/src/runtime/{guardian-context-resolver.ts → trust-context-resolver.ts} +29 -41
- package/src/schedule/integration-status.ts +44 -9
- package/src/schedule/recurrence-engine.ts +47 -24
- package/src/schedule/recurrence-types.ts +12 -7
- package/src/schedule/schedule-store.ts +166 -83
- package/src/schedule/scheduler.ts +26 -22
- package/src/security/encrypted-store.ts +68 -38
- package/src/security/keychain.ts +183 -120
- package/src/security/oauth-callback-registry.ts +3 -3
- package/src/security/oauth2.ts +226 -138
- package/src/security/redaction.ts +24 -24
- package/src/security/secret-allowlist.ts +46 -21
- package/src/security/secret-ingress.ts +15 -7
- package/src/security/secret-scanner.ts +193 -104
- package/src/security/secure-keys.ts +9 -3
- package/src/security/token-manager.ts +99 -40
- package/src/security/tool-approval-digest.ts +3 -3
- package/src/sequence/analytics.ts +52 -27
- package/src/sequence/engine.ts +135 -72
- package/src/sequence/guardrails.ts +32 -20
- package/src/sequence/importer.ts +75 -37
- package/src/sequence/reply-matcher.ts +36 -18
- package/src/sequence/store.ts +137 -75
- package/src/sequence/types.ts +30 -16
- package/src/services/published-app-updater.ts +26 -16
- package/src/services/vercel-deploy.ts +19 -15
- package/src/skills/active-skill-tools.ts +3 -3
- package/src/skills/clawhub.ts +178 -90
- package/src/skills/include-graph.ts +24 -17
- package/src/skills/managed-store.ts +89 -42
- package/src/skills/path-classifier.ts +10 -10
- package/src/skills/remote-skill-policy.ts +31 -22
- package/src/skills/slash-commands.ts +36 -30
- package/src/skills/tool-manifest.ts +60 -31
- package/src/skills/version-hash.ts +25 -15
- package/src/slack/slack-webhook.ts +19 -15
- package/src/subagent/index.ts +4 -8
- package/src/subagent/manager.ts +119 -69
- package/src/subagent/types.ts +9 -12
- package/src/swarm/backend-claude-code.ts +124 -45
- package/src/swarm/checkpoint.ts +36 -16
- package/src/swarm/graph-utils.ts +1 -3
- package/src/swarm/index.ts +38 -19
- package/src/swarm/limits.ts +13 -4
- package/src/swarm/orchestrator.ts +108 -57
- package/src/swarm/plan-validator.ts +23 -17
- package/src/swarm/router-planner.ts +51 -22
- package/src/swarm/router-prompts.ts +4 -1
- package/src/swarm/synthesizer.ts +26 -18
- package/src/swarm/types.ts +14 -4
- package/src/swarm/worker-backend.ts +36 -26
- package/src/swarm/worker-prompts.ts +13 -9
- package/src/swarm/worker-runner.ts +40 -34
- package/src/tasks/candidate-store.ts +14 -6
- package/src/tasks/ephemeral-permissions.ts +9 -5
- package/src/tasks/task-compiler.ts +41 -38
- package/src/tasks/task-runner.ts +54 -26
- package/src/tasks/task-scheduler.ts +1 -1
- package/src/tasks/task-store.ts +20 -7
- package/src/tasks/tool-sanitizer.ts +3 -3
- package/src/tools/apps/definitions.ts +23 -15
- package/src/tools/apps/executors.ts +118 -37
- package/src/tools/apps/open-proxy.ts +5 -5
- package/src/tools/apps/registry.ts +2 -2
- package/src/tools/assets/materialize.ts +59 -41
- package/src/tools/assets/search.ts +86 -48
- package/src/tools/browser/api-map.ts +52 -36
- package/src/tools/browser/auth-cache.ts +21 -18
- package/src/tools/browser/auth-detector.ts +43 -28
- package/src/tools/browser/auto-navigate.ts +149 -68
- package/src/tools/browser/browser-execution.ts +9 -3
- package/src/tools/browser/headless-browser.ts +287 -150
- package/src/tools/browser/jit-auth.ts +37 -21
- package/src/tools/browser/network-recorder.ts +138 -56
- package/src/tools/browser/recording-store.ts +22 -15
- package/src/tools/browser/runtime-check.ts +8 -5
- package/src/tools/browser/x-auto-navigate.ts +88 -47
- package/src/tools/calls/call-end.ts +9 -6
- package/src/tools/calls/call-start.ts +30 -20
- package/src/tools/calls/call-status.ts +8 -5
- package/src/tools/claude-code/claude-code.ts +301 -165
- package/src/tools/computer-use/definitions.ts +159 -130
- package/src/tools/computer-use/registry.ts +2 -2
- package/src/tools/computer-use/request-computer-control.ts +21 -13
- package/src/tools/computer-use/skill-proxy-bridge.ts +1 -1
- package/src/tools/credentials/account-registry.ts +52 -35
- package/src/tools/credentials/broker-types.ts +1 -1
- package/src/tools/credentials/broker.ts +97 -55
- package/src/tools/credentials/domain-policy.ts +5 -2
- package/src/tools/credentials/host-pattern-match.ts +15 -8
- package/src/tools/credentials/metadata-store.ts +93 -43
- package/src/tools/credentials/policy-types.ts +5 -2
- package/src/tools/credentials/policy-validate.ts +21 -14
- package/src/tools/credentials/post-connect-hooks.ts +18 -7
- package/src/tools/credentials/resolve.ts +11 -10
- package/src/tools/credentials/selection.ts +30 -25
- package/src/tools/credentials/tool-policy.ts +5 -2
- package/src/tools/credentials/vault.ts +452 -183
- package/src/tools/document/document-tool.ts +23 -17
- package/src/tools/document/editor-template.ts +12 -7
- package/src/tools/execution-target.ts +13 -10
- package/src/tools/execution-timeout.ts +6 -5
- package/src/tools/executor.ts +141 -74
- package/src/tools/filesystem/edit.ts +82 -45
- package/src/tools/filesystem/fuzzy-match.ts +70 -32
- package/src/tools/filesystem/read.ts +46 -28
- package/src/tools/filesystem/view-image.ts +86 -42
- package/src/tools/filesystem/write.ts +53 -32
- package/src/tools/followups/followup_create.ts +43 -17
- package/src/tools/followups/followup_list.ts +28 -13
- package/src/tools/followups/followup_resolve.ts +9 -6
- package/src/tools/guardian-control-plane-policy.ts +15 -14
- package/src/tools/host-filesystem/edit.ts +77 -42
- package/src/tools/host-filesystem/read.ts +52 -33
- package/src/tools/host-filesystem/write.ts +50 -29
- package/src/tools/host-terminal/host-shell.ts +97 -61
- package/src/tools/mcp/mcp-tool-factory.ts +21 -14
- package/src/tools/memory/definitions.ts +60 -28
- package/src/tools/memory/handlers.ts +149 -77
- package/src/tools/memory/register.ts +39 -16
- package/src/tools/network/__tests__/web-search.test.ts +236 -177
- package/src/tools/network/domain-normalize.ts +13 -9
- package/src/tools/network/script-proxy/__tests__/logging.test.ts +193 -123
- package/src/tools/network/script-proxy/__tests__/policy.test.ts +225 -127
- package/src/tools/network/script-proxy/index.ts +1 -17
- package/src/tools/network/script-proxy/session-manager.ts +151 -84
- package/src/tools/network/url-safety.ts +56 -34
- package/src/tools/network/web-fetch.ts +273 -155
- package/src/tools/network/web-search.ts +166 -81
- package/src/tools/permission-checker.ts +6 -25
- package/src/tools/policy-context.ts +8 -5
- package/src/tools/registry.ts +73 -46
- package/src/tools/reminder/reminder-store.ts +65 -44
- package/src/tools/reminder/reminder.ts +76 -35
- package/src/tools/schedule/create.ts +44 -21
- package/src/tools/schedule/delete.ts +8 -5
- package/src/tools/schedule/list.ts +39 -19
- package/src/tools/schedule/update.ts +49 -26
- package/src/tools/secret-detection-handler.ts +130 -49
- package/src/tools/sensitive-output-placeholders.ts +15 -8
- package/src/tools/shared/filesystem/edit-engine.ts +45 -14
- package/src/tools/shared/filesystem/errors.ts +18 -18
- package/src/tools/shared/filesystem/file-ops-service.ts +59 -32
- package/src/tools/shared/filesystem/format-diff.ts +21 -11
- package/src/tools/shared/filesystem/path-policy.ts +17 -13
- package/src/tools/shared/filesystem/size-guard.ts +8 -4
- package/src/tools/shared/filesystem/types.ts +2 -2
- package/src/tools/shared/shell-output.ts +4 -3
- package/src/tools/side-effects.ts +36 -28
- package/src/tools/skills/delete-managed.ts +30 -17
- package/src/tools/skills/load.ts +88 -46
- package/src/tools/skills/sandbox-runner.ts +62 -46
- package/src/tools/skills/scaffold-managed.ts +98 -48
- package/src/tools/skills/script-contract.ts +5 -2
- package/src/tools/skills/skill-script-runner.ts +29 -13
- package/src/tools/skills/skill-tool-factory.ts +20 -10
- package/src/tools/subagent/abort.ts +10 -4
- package/src/tools/subagent/message.ts +14 -8
- package/src/tools/subagent/read.ts +20 -11
- package/src/tools/subagent/spawn.ts +14 -6
- package/src/tools/subagent/status.ts +7 -4
- package/src/tools/swarm/delegate.ts +75 -49
- package/src/tools/system/avatar-generator.ts +46 -33
- package/src/tools/system/navigate-settings.ts +29 -19
- package/src/tools/system/open-system-settings.ts +30 -20
- package/src/tools/system/request-permission.ts +59 -44
- package/src/tools/system/version.ts +27 -16
- package/src/tools/system/voice-config.ts +116 -53
- package/src/tools/tasks/index.ts +8 -8
- package/src/tools/tasks/task-delete.ts +61 -22
- package/src/tools/tasks/task-list.ts +23 -11
- package/src/tools/tasks/task-run.ts +41 -16
- package/src/tools/tasks/task-save.ts +27 -10
- package/src/tools/tasks/work-item-enqueue.ts +114 -48
- package/src/tools/tasks/work-item-list.ts +20 -10
- package/src/tools/tasks/work-item-remove.ts +49 -15
- package/src/tools/tasks/work-item-run.ts +34 -13
- package/src/tools/tasks/work-item-update.ts +84 -31
- package/src/tools/terminal/backends/native.ts +64 -35
- package/src/tools/terminal/backends/types.ts +6 -2
- package/src/tools/terminal/parser.ts +200 -125
- package/src/tools/terminal/safe-env.ts +27 -21
- package/src/tools/terminal/sandbox-diagnostics.ts +31 -13
- package/src/tools/terminal/sandbox.ts +10 -6
- package/src/tools/terminal/shell.ts +124 -68
- package/src/tools/tool-approval-handler.ts +193 -138
- package/src/tools/types.ts +43 -23
- package/src/tools/ui-surface/definitions.ts +124 -89
- package/src/tools/ui-surface/registry.ts +2 -2
- package/src/tools/watch/screen-watch.ts +50 -32
- package/src/tools/watch/watch-state.ts +41 -15
- package/src/tools/watcher/create.ts +37 -15
- package/src/tools/watcher/delete.ts +9 -6
- package/src/tools/watcher/digest.ts +10 -6
- package/src/tools/watcher/list.ts +37 -14
- package/src/tools/watcher/update.ts +33 -18
- package/src/tools/weather/service.ts +331 -174
- package/src/twitter/client.ts +261 -138
- package/src/twitter/oauth-client.ts +17 -13
- package/src/twitter/router.ts +51 -23
- package/src/twitter/session.ts +27 -18
- package/src/types/qrcode.d.ts +6 -3
- package/src/usage/actors.ts +16 -16
- package/src/usage/types.ts +3 -3
- package/src/util/bundled-asset.ts +10 -6
- package/src/util/canonicalize-identity.ts +11 -4
- package/src/util/clipboard.ts +7 -7
- package/src/util/content-id.ts +3 -3
- package/src/util/debounce.ts +3 -2
- package/src/util/diff.ts +55 -33
- package/src/util/errors.ts +26 -26
- package/src/util/fs.ts +8 -2
- package/src/util/log-redact.ts +12 -12
- package/src/util/logger.ts +112 -51
- package/src/util/network-info.ts +13 -5
- package/src/util/object.ts +4 -2
- package/src/util/phone.ts +4 -4
- package/src/util/platform.ts +80 -58
- package/src/util/pricing.ts +49 -31
- package/src/util/retry.ts +18 -7
- package/src/util/row-mapper.ts +7 -4
- package/src/util/silently.ts +7 -4
- package/src/util/spawn.ts +48 -0
- package/src/util/spinner.ts +9 -7
- package/src/util/time.ts +16 -3
- package/src/util/truncate.ts +1 -1
- package/src/util/voice-code.ts +6 -4
- package/src/util/xml.ts +5 -1
- package/src/version.ts +12 -8
- package/src/watcher/engine.ts +71 -44
- package/src/watcher/provider-registry.ts +1 -1
- package/src/watcher/providers/github.ts +40 -23
- package/src/watcher/providers/gmail.ts +59 -38
- package/src/watcher/providers/google-calendar.ts +62 -48
- package/src/watcher/providers/linear.ts +219 -150
- package/src/watcher/providers/slack.ts +93 -27
- package/src/watcher/watcher-store.ts +75 -55
- package/src/work-items/work-item-runner.ts +62 -29
- package/src/work-items/work-item-store.ts +137 -47
- package/src/workspace/commit-message-enrichment-service.ts +65 -25
- package/src/workspace/commit-message-provider.ts +14 -12
- package/src/workspace/git-service.ts +355 -239
- package/src/workspace/heartbeat-service.ts +74 -37
- package/src/workspace/provider-commit-message-generator.ts +95 -70
- package/src/workspace/top-level-renderer.ts +10 -8
- package/src/workspace/top-level-scanner.ts +9 -3
- package/src/workspace/turn-commit.ts +63 -36
- package/src/__tests__/ingress-member-store.test.ts +0 -294
- package/src/__tests__/script-proxy-router.test.ts +0 -215
- package/src/config/bundled-skills/trusted-contacts/SKILL.md +0 -372
- package/src/memory/guardian-bindings.ts +0 -158
- package/src/memory/ingress-member-store.ts +0 -352
- package/src/tools/network/script-proxy/__tests__/router.test.ts +0 -77
- package/src/tools/network/script-proxy/certs.ts +0 -7
- package/src/tools/network/script-proxy/connect-tunnel.ts +0 -1
- package/src/tools/network/script-proxy/http-forwarder.ts +0 -2
- package/src/tools/network/script-proxy/logging.ts +0 -12
- package/src/tools/network/script-proxy/mitm-handler.ts +0 -2
- package/src/tools/network/script-proxy/policy.ts +0 -4
- package/src/tools/network/script-proxy/router.ts +0 -2
- package/src/tools/network/script-proxy/server.ts +0 -5
- package/src/tools/network/script-proxy/types.ts +0 -19
|
@@ -6,38 +6,46 @@
|
|
|
6
6
|
* from Twilio and can send text tokens back for TTS.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { randomInt } from
|
|
10
|
-
|
|
11
|
-
import type { ServerWebSocket } from
|
|
12
|
-
|
|
13
|
-
import { getConfig } from
|
|
14
|
-
import { resolveUserReference } from
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
9
|
+
import { randomInt } from "node:crypto";
|
|
10
|
+
|
|
11
|
+
import type { ServerWebSocket } from "bun";
|
|
12
|
+
|
|
13
|
+
import { getConfig } from "../config/loader.js";
|
|
14
|
+
import { resolveUserReference } from "../config/user-reference.js";
|
|
15
|
+
import {
|
|
16
|
+
findContactChannel,
|
|
17
|
+
findGuardianForChannel,
|
|
18
|
+
listGuardianChannels,
|
|
19
|
+
} from "../contacts/contact-store.js";
|
|
20
|
+
import {
|
|
21
|
+
createGuardianBinding,
|
|
22
|
+
revokeGuardianBinding,
|
|
23
|
+
upsertMember,
|
|
24
|
+
} from "../contacts/contacts-write.js";
|
|
25
|
+
import { getAssistantName } from "../daemon/identity-helpers.js";
|
|
26
|
+
import { getCanonicalGuardianRequest } from "../memory/canonical-guardian-store.js";
|
|
27
|
+
import * as conversationStore from "../memory/conversation-store.js";
|
|
28
|
+
import { findActiveVoiceInvites } from "../memory/ingress-invite-store.js";
|
|
29
|
+
import { revokeScopedApprovalGrantsForContext } from "../memory/scoped-approval-grants.js";
|
|
30
|
+
import { emitNotificationSignal } from "../notifications/emit-signal.js";
|
|
31
|
+
import { notifyGuardianOfAccessRequest } from "../runtime/access-request-helper.js";
|
|
24
32
|
import {
|
|
25
33
|
resolveActorTrust,
|
|
26
|
-
|
|
27
|
-
} from
|
|
28
|
-
import { DAEMON_INTERNAL_ASSISTANT_ID } from
|
|
34
|
+
toTrustContext,
|
|
35
|
+
} from "../runtime/actor-trust-resolver.js";
|
|
36
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
|
|
29
37
|
import {
|
|
30
38
|
getGuardianBinding,
|
|
31
39
|
getPendingChallenge,
|
|
32
40
|
validateAndConsumeChallenge,
|
|
33
|
-
} from
|
|
41
|
+
} from "../runtime/channel-guardian-service.js";
|
|
34
42
|
import {
|
|
35
43
|
composeVerificationVoice,
|
|
36
44
|
GUARDIAN_VERIFY_TEMPLATE_KEYS,
|
|
37
|
-
} from
|
|
38
|
-
import { redeemVoiceInviteCode } from
|
|
39
|
-
import { parseJsonSafe } from
|
|
40
|
-
import { getLogger } from
|
|
45
|
+
} from "../runtime/guardian-verification-templates.js";
|
|
46
|
+
import { redeemVoiceInviteCode } from "../runtime/ingress-service.js";
|
|
47
|
+
import { parseJsonSafe } from "../util/json.js";
|
|
48
|
+
import { getLogger } from "../util/logger.js";
|
|
41
49
|
import {
|
|
42
50
|
getAccessRequestPollIntervalMs,
|
|
43
51
|
getGuardianWaitUpdateInitialIntervalMs,
|
|
@@ -46,31 +54,34 @@ import {
|
|
|
46
54
|
getGuardianWaitUpdateSteadyMinIntervalMs,
|
|
47
55
|
getTtsPlaybackDelayMs,
|
|
48
56
|
getUserConsultationTimeoutMs,
|
|
49
|
-
} from
|
|
50
|
-
import { CallController } from
|
|
51
|
-
import { persistCallCompletionMessage } from
|
|
52
|
-
import { addPointerMessage, formatDuration } from
|
|
53
|
-
import {
|
|
54
|
-
|
|
57
|
+
} from "./call-constants.js";
|
|
58
|
+
import { CallController } from "./call-controller.js";
|
|
59
|
+
import { persistCallCompletionMessage } from "./call-conversation-messages.js";
|
|
60
|
+
import { addPointerMessage, formatDuration } from "./call-pointer-messages.js";
|
|
61
|
+
import {
|
|
62
|
+
fireCallCompletionNotifier,
|
|
63
|
+
fireCallTranscriptNotifier,
|
|
64
|
+
} from "./call-state.js";
|
|
65
|
+
import { isTerminalState } from "./call-state-machine.js";
|
|
55
66
|
import {
|
|
56
67
|
expirePendingQuestions,
|
|
57
68
|
getCallSession,
|
|
58
69
|
recordCallEvent,
|
|
59
70
|
updateCallSession,
|
|
60
|
-
} from
|
|
71
|
+
} from "./call-store.js";
|
|
61
72
|
import {
|
|
62
73
|
extractPromptSpeakerMetadata,
|
|
63
74
|
type PromptSpeakerContext,
|
|
64
75
|
SpeakerIdentityTracker,
|
|
65
|
-
} from
|
|
76
|
+
} from "./speaker-identification.js";
|
|
66
77
|
|
|
67
|
-
const log = getLogger(
|
|
78
|
+
const log = getLogger("relay-server");
|
|
68
79
|
|
|
69
80
|
// ── ConversationRelay message types ──────────────────────────────────
|
|
70
81
|
|
|
71
82
|
// Messages FROM Twilio
|
|
72
83
|
export interface RelaySetupMessage {
|
|
73
|
-
type:
|
|
84
|
+
type: "setup";
|
|
74
85
|
callSid: string;
|
|
75
86
|
from: string;
|
|
76
87
|
to: string;
|
|
@@ -78,7 +89,7 @@ export interface RelaySetupMessage {
|
|
|
78
89
|
}
|
|
79
90
|
|
|
80
91
|
export interface RelayPromptMessage {
|
|
81
|
-
type:
|
|
92
|
+
type: "prompt";
|
|
82
93
|
voicePrompt: string;
|
|
83
94
|
lang: string;
|
|
84
95
|
last: boolean;
|
|
@@ -102,17 +113,17 @@ export interface RelayPromptMessage {
|
|
|
102
113
|
}
|
|
103
114
|
|
|
104
115
|
export interface RelayInterruptMessage {
|
|
105
|
-
type:
|
|
116
|
+
type: "interrupt";
|
|
106
117
|
utteranceUntilInterrupt: string;
|
|
107
118
|
}
|
|
108
119
|
|
|
109
120
|
export interface RelayDtmfMessage {
|
|
110
|
-
type:
|
|
121
|
+
type: "dtmf";
|
|
111
122
|
digit: string;
|
|
112
123
|
}
|
|
113
124
|
|
|
114
125
|
export interface RelayErrorMessage {
|
|
115
|
-
type:
|
|
126
|
+
type: "error";
|
|
116
127
|
description: string;
|
|
117
128
|
}
|
|
118
129
|
|
|
@@ -125,13 +136,13 @@ export type RelayInboundMessage =
|
|
|
125
136
|
|
|
126
137
|
// Messages TO Twilio
|
|
127
138
|
export interface RelayTextMessage {
|
|
128
|
-
type:
|
|
139
|
+
type: "text";
|
|
129
140
|
token: string;
|
|
130
141
|
last: boolean;
|
|
131
142
|
}
|
|
132
143
|
|
|
133
144
|
export interface RelayEndMessage {
|
|
134
|
-
type:
|
|
145
|
+
type: "end";
|
|
135
146
|
handoffData?: string;
|
|
136
147
|
}
|
|
137
148
|
|
|
@@ -147,10 +158,14 @@ export interface RelayWebSocketData {
|
|
|
147
158
|
export const activeRelayConnections = new Map<string, RelayConnection>();
|
|
148
159
|
|
|
149
160
|
/** Module-level broadcast function, set by the HTTP server during startup. */
|
|
150
|
-
let globalBroadcast:
|
|
161
|
+
let globalBroadcast:
|
|
162
|
+
| ((msg: import("../daemon/ipc-contract.js").ServerMessage) => void)
|
|
163
|
+
| undefined;
|
|
151
164
|
|
|
152
165
|
/** Register a broadcast function so RelayConnection can forward IPC events. */
|
|
153
|
-
export function setRelayBroadcast(
|
|
166
|
+
export function setRelayBroadcast(
|
|
167
|
+
fn: (msg: import("../daemon/ipc-contract.js").ServerMessage) => void,
|
|
168
|
+
): void {
|
|
154
169
|
globalBroadcast = fn;
|
|
155
170
|
}
|
|
156
171
|
|
|
@@ -159,13 +174,18 @@ export function setRelayBroadcast(fn: (msg: import('../daemon/ipc-contract.js').
|
|
|
159
174
|
/**
|
|
160
175
|
* Manages a single WebSocket connection for one call.
|
|
161
176
|
*/
|
|
162
|
-
export type RelayConnectionState =
|
|
177
|
+
export type RelayConnectionState =
|
|
178
|
+
| "connected"
|
|
179
|
+
| "verification_pending"
|
|
180
|
+
| "awaiting_name"
|
|
181
|
+
| "awaiting_guardian_decision"
|
|
182
|
+
| "disconnecting";
|
|
163
183
|
|
|
164
184
|
export class RelayConnection {
|
|
165
185
|
private ws: ServerWebSocket<RelayWebSocketData>;
|
|
166
186
|
private callSessionId: string;
|
|
167
187
|
private conversationHistory: Array<{
|
|
168
|
-
role:
|
|
188
|
+
role: "caller" | "assistant";
|
|
169
189
|
text: string;
|
|
170
190
|
timestamp: number;
|
|
171
191
|
speaker?: PromptSpeakerContext;
|
|
@@ -175,12 +195,12 @@ export class RelayConnection {
|
|
|
175
195
|
private speakerIdentityTracker: SpeakerIdentityTracker;
|
|
176
196
|
|
|
177
197
|
// Verification state (outbound callee verification)
|
|
178
|
-
private connectionState: RelayConnectionState =
|
|
198
|
+
private connectionState: RelayConnectionState = "connected";
|
|
179
199
|
private verificationCode: string | null = null;
|
|
180
200
|
private verificationAttempts = 0;
|
|
181
201
|
private verificationMaxAttempts = 3;
|
|
182
202
|
private verificationCodeLength = 6;
|
|
183
|
-
private dtmfBuffer =
|
|
203
|
+
private dtmfBuffer = "";
|
|
184
204
|
|
|
185
205
|
// Inbound voice guardian verification state
|
|
186
206
|
private guardianVerificationActive = false;
|
|
@@ -204,14 +224,16 @@ export class RelayConnection {
|
|
|
204
224
|
private accessRequestAssistantId: string | null = null;
|
|
205
225
|
private accessRequestFromNumber: string | null = null;
|
|
206
226
|
private accessRequestPollTimer: ReturnType<typeof setInterval> | null = null;
|
|
207
|
-
private accessRequestTimeoutTimer: ReturnType<typeof setTimeout> | null =
|
|
227
|
+
private accessRequestTimeoutTimer: ReturnType<typeof setTimeout> | null =
|
|
228
|
+
null;
|
|
208
229
|
private accessRequestCallerName: string | null = null;
|
|
209
230
|
|
|
210
231
|
// Name capture timeout (unknown inbound callers)
|
|
211
232
|
private nameCaptureTimeoutTimer: ReturnType<typeof setTimeout> | null = null;
|
|
212
233
|
|
|
213
234
|
// Guardian wait heartbeat state
|
|
214
|
-
private accessRequestHeartbeatTimer: ReturnType<typeof setTimeout> | null =
|
|
235
|
+
private accessRequestHeartbeatTimer: ReturnType<typeof setTimeout> | null =
|
|
236
|
+
null;
|
|
215
237
|
private accessRequestWaitStartedAt: number = 0;
|
|
216
238
|
private heartbeatSequence = 0;
|
|
217
239
|
|
|
@@ -259,28 +281,37 @@ export class RelayConnection {
|
|
|
259
281
|
async handleMessage(data: string): Promise<void> {
|
|
260
282
|
const parsed = parseJsonSafe<RelayInboundMessage>(data);
|
|
261
283
|
if (!parsed) {
|
|
262
|
-
log.warn(
|
|
284
|
+
log.warn(
|
|
285
|
+
{ callSessionId: this.callSessionId, data },
|
|
286
|
+
"Failed to parse relay message",
|
|
287
|
+
);
|
|
263
288
|
return;
|
|
264
289
|
}
|
|
265
290
|
|
|
266
291
|
switch (parsed.type) {
|
|
267
|
-
case
|
|
292
|
+
case "setup":
|
|
268
293
|
await this.handleSetup(parsed);
|
|
269
294
|
break;
|
|
270
|
-
case
|
|
295
|
+
case "prompt":
|
|
271
296
|
await this.handlePrompt(parsed);
|
|
272
297
|
break;
|
|
273
|
-
case
|
|
298
|
+
case "interrupt":
|
|
274
299
|
this.handleInterrupt(parsed);
|
|
275
300
|
break;
|
|
276
|
-
case
|
|
301
|
+
case "dtmf":
|
|
277
302
|
this.handleDtmf(parsed);
|
|
278
303
|
break;
|
|
279
|
-
case
|
|
304
|
+
case "error":
|
|
280
305
|
this.handleError(parsed);
|
|
281
306
|
break;
|
|
282
307
|
default:
|
|
283
|
-
log.warn(
|
|
308
|
+
log.warn(
|
|
309
|
+
{
|
|
310
|
+
callSessionId: this.callSessionId,
|
|
311
|
+
type: (parsed as { type: unknown }).type,
|
|
312
|
+
},
|
|
313
|
+
"Unknown relay message type",
|
|
314
|
+
);
|
|
284
315
|
}
|
|
285
316
|
}
|
|
286
317
|
|
|
@@ -288,11 +319,14 @@ export class RelayConnection {
|
|
|
288
319
|
* Send a text token to the caller for TTS playback.
|
|
289
320
|
*/
|
|
290
321
|
sendTextToken(token: string, last: boolean): void {
|
|
291
|
-
const message: RelayTextMessage = { type:
|
|
322
|
+
const message: RelayTextMessage = { type: "text", token, last };
|
|
292
323
|
try {
|
|
293
324
|
this.ws.send(JSON.stringify(message));
|
|
294
325
|
} catch (err) {
|
|
295
|
-
log.error(
|
|
326
|
+
log.error(
|
|
327
|
+
{ err, callSessionId: this.callSessionId },
|
|
328
|
+
"Failed to send text token",
|
|
329
|
+
);
|
|
296
330
|
}
|
|
297
331
|
}
|
|
298
332
|
|
|
@@ -300,22 +334,33 @@ export class RelayConnection {
|
|
|
300
334
|
* End the ConversationRelay session.
|
|
301
335
|
*/
|
|
302
336
|
endSession(reason?: string): void {
|
|
303
|
-
const message: RelayEndMessage = { type:
|
|
337
|
+
const message: RelayEndMessage = { type: "end" };
|
|
304
338
|
if (reason) {
|
|
305
339
|
message.handoffData = JSON.stringify({ reason });
|
|
306
340
|
}
|
|
307
341
|
try {
|
|
308
342
|
this.ws.send(JSON.stringify(message));
|
|
309
343
|
} catch (err) {
|
|
310
|
-
log.error(
|
|
344
|
+
log.error(
|
|
345
|
+
{ err, callSessionId: this.callSessionId },
|
|
346
|
+
"Failed to send end message",
|
|
347
|
+
);
|
|
311
348
|
}
|
|
312
349
|
}
|
|
313
350
|
|
|
314
351
|
/**
|
|
315
352
|
* Get the conversation history for context.
|
|
316
353
|
*/
|
|
317
|
-
getConversationHistory(): Array<{
|
|
318
|
-
|
|
354
|
+
getConversationHistory(): Array<{
|
|
355
|
+
role: string;
|
|
356
|
+
text: string;
|
|
357
|
+
speaker?: PromptSpeakerContext;
|
|
358
|
+
}> {
|
|
359
|
+
return this.conversationHistory.map(({ role, text, speaker }) => ({
|
|
360
|
+
role,
|
|
361
|
+
text,
|
|
362
|
+
speaker,
|
|
363
|
+
}));
|
|
319
364
|
}
|
|
320
365
|
|
|
321
366
|
/**
|
|
@@ -365,7 +410,10 @@ export class RelayConnection {
|
|
|
365
410
|
}
|
|
366
411
|
this.accessRequestWaitActive = false;
|
|
367
412
|
this.abortController.abort();
|
|
368
|
-
log.info(
|
|
413
|
+
log.info(
|
|
414
|
+
{ callSessionId: this.callSessionId },
|
|
415
|
+
"RelayConnection destroyed",
|
|
416
|
+
);
|
|
369
417
|
}
|
|
370
418
|
|
|
371
419
|
/**
|
|
@@ -378,7 +426,7 @@ export class RelayConnection {
|
|
|
378
426
|
// If the call was still in guardian-wait with callback opt-in, emit the
|
|
379
427
|
// handoff notification before cleaning up wait state.
|
|
380
428
|
if (this.accessRequestWaitActive && this.callbackOptIn) {
|
|
381
|
-
this.emitAccessRequestCallbackHandoff(
|
|
429
|
+
this.emitAccessRequestCallbackHandoff("transport_closed");
|
|
382
430
|
}
|
|
383
431
|
|
|
384
432
|
// Clean up access request wait state on disconnect to stop polling
|
|
@@ -395,41 +443,60 @@ export class RelayConnection {
|
|
|
395
443
|
const isNormalClose = code === 1000;
|
|
396
444
|
if (isNormalClose) {
|
|
397
445
|
updateCallSession(this.callSessionId, {
|
|
398
|
-
status:
|
|
446
|
+
status: "completed",
|
|
399
447
|
endedAt: Date.now(),
|
|
400
448
|
});
|
|
401
|
-
recordCallEvent(this.callSessionId,
|
|
402
|
-
reason: reason ||
|
|
449
|
+
recordCallEvent(this.callSessionId, "call_ended", {
|
|
450
|
+
reason: reason || "relay_closed",
|
|
403
451
|
closeCode: code,
|
|
404
452
|
});
|
|
405
453
|
|
|
406
454
|
// Post a pointer message in the initiating conversation
|
|
407
455
|
if (session.initiatedFromConversationId) {
|
|
408
|
-
const durationMs = session.startedAt
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
456
|
+
const durationMs = session.startedAt
|
|
457
|
+
? Date.now() - session.startedAt
|
|
458
|
+
: 0;
|
|
459
|
+
addPointerMessage(
|
|
460
|
+
session.initiatedFromConversationId,
|
|
461
|
+
"completed",
|
|
462
|
+
session.toNumber,
|
|
463
|
+
{
|
|
464
|
+
duration: durationMs > 0 ? formatDuration(durationMs) : undefined,
|
|
465
|
+
},
|
|
466
|
+
).catch((err) => {
|
|
467
|
+
log.warn(
|
|
468
|
+
{ conversationId: session.initiatedFromConversationId, err },
|
|
469
|
+
"Skipping pointer write — origin conversation may no longer exist",
|
|
470
|
+
);
|
|
413
471
|
});
|
|
414
472
|
}
|
|
415
473
|
} else {
|
|
416
|
-
const detail =
|
|
474
|
+
const detail =
|
|
475
|
+
reason || (code ? `relay_closed_${code}` : "relay_closed_abnormal");
|
|
417
476
|
updateCallSession(this.callSessionId, {
|
|
418
|
-
status:
|
|
477
|
+
status: "failed",
|
|
419
478
|
endedAt: Date.now(),
|
|
420
479
|
lastError: `Relay websocket closed unexpectedly: ${detail}`,
|
|
421
480
|
});
|
|
422
|
-
recordCallEvent(this.callSessionId,
|
|
481
|
+
recordCallEvent(this.callSessionId, "call_failed", {
|
|
423
482
|
reason: detail,
|
|
424
483
|
closeCode: code,
|
|
425
484
|
});
|
|
426
485
|
|
|
427
486
|
// Post a failure pointer message in the initiating conversation
|
|
428
487
|
if (session.initiatedFromConversationId) {
|
|
429
|
-
addPointerMessage(
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
488
|
+
addPointerMessage(
|
|
489
|
+
session.initiatedFromConversationId,
|
|
490
|
+
"failed",
|
|
491
|
+
session.toNumber,
|
|
492
|
+
{
|
|
493
|
+
reason: detail,
|
|
494
|
+
},
|
|
495
|
+
).catch((err) => {
|
|
496
|
+
log.warn(
|
|
497
|
+
{ conversationId: session.initiatedFromConversationId, err },
|
|
498
|
+
"Skipping pointer write — origin conversation may no longer exist",
|
|
499
|
+
);
|
|
433
500
|
});
|
|
434
501
|
}
|
|
435
502
|
}
|
|
@@ -441,14 +508,31 @@ export class RelayConnection {
|
|
|
441
508
|
// guardian-approval-interception minting path sets callSessionId: null
|
|
442
509
|
// but always sets conversationId.
|
|
443
510
|
try {
|
|
444
|
-
revokeScopedApprovalGrantsForContext({
|
|
445
|
-
|
|
511
|
+
revokeScopedApprovalGrantsForContext({
|
|
512
|
+
callSessionId: this.callSessionId,
|
|
513
|
+
});
|
|
514
|
+
revokeScopedApprovalGrantsForContext({
|
|
515
|
+
conversationId: session.conversationId,
|
|
516
|
+
});
|
|
446
517
|
} catch (err) {
|
|
447
|
-
log.warn(
|
|
518
|
+
log.warn(
|
|
519
|
+
{ err, callSessionId: this.callSessionId },
|
|
520
|
+
"Failed to revoke scoped grants on transport close",
|
|
521
|
+
);
|
|
448
522
|
}
|
|
449
523
|
|
|
450
|
-
persistCallCompletionMessage(
|
|
451
|
-
|
|
524
|
+
persistCallCompletionMessage(
|
|
525
|
+
session.conversationId,
|
|
526
|
+
this.callSessionId,
|
|
527
|
+
).catch((err) => {
|
|
528
|
+
log.error(
|
|
529
|
+
{
|
|
530
|
+
err,
|
|
531
|
+
conversationId: session.conversationId,
|
|
532
|
+
callSessionId: this.callSessionId,
|
|
533
|
+
},
|
|
534
|
+
"Failed to persist call completion message",
|
|
535
|
+
);
|
|
452
536
|
});
|
|
453
537
|
fireCallCompletionNotifier(session.conversationId, this.callSessionId);
|
|
454
538
|
}
|
|
@@ -457,8 +541,13 @@ export class RelayConnection {
|
|
|
457
541
|
|
|
458
542
|
private async handleSetup(msg: RelaySetupMessage): Promise<void> {
|
|
459
543
|
log.info(
|
|
460
|
-
{
|
|
461
|
-
|
|
544
|
+
{
|
|
545
|
+
callSessionId: this.callSessionId,
|
|
546
|
+
callSid: msg.callSid,
|
|
547
|
+
from: msg.from,
|
|
548
|
+
to: msg.to,
|
|
549
|
+
},
|
|
550
|
+
"ConversationRelay setup received",
|
|
462
551
|
);
|
|
463
552
|
|
|
464
553
|
// Store the callSid association on the call session
|
|
@@ -467,8 +556,12 @@ export class RelayConnection {
|
|
|
467
556
|
const updates: Parameters<typeof updateCallSession>[1] = {
|
|
468
557
|
providerCallSid: msg.callSid,
|
|
469
558
|
};
|
|
470
|
-
if (
|
|
471
|
-
|
|
559
|
+
if (
|
|
560
|
+
!isTerminalState(session.status) &&
|
|
561
|
+
session.status !== "in_progress" &&
|
|
562
|
+
session.status !== "waiting_on_user"
|
|
563
|
+
) {
|
|
564
|
+
updates.status = "in_progress";
|
|
472
565
|
if (!session.startedAt) {
|
|
473
566
|
updates.startedAt = Date.now();
|
|
474
567
|
}
|
|
@@ -481,12 +574,12 @@ export class RelayConnection {
|
|
|
481
574
|
const safeCustomParameters = msg.customParameters
|
|
482
575
|
? Object.fromEntries(
|
|
483
576
|
Object.entries(msg.customParameters).filter(
|
|
484
|
-
([key]) => !key.toLowerCase().includes(
|
|
577
|
+
([key]) => !key.toLowerCase().includes("secret"),
|
|
485
578
|
),
|
|
486
579
|
)
|
|
487
580
|
: undefined;
|
|
488
581
|
|
|
489
|
-
recordCallEvent(this.callSessionId,
|
|
582
|
+
recordCallEvent(this.callSessionId, "call_connected", {
|
|
490
583
|
callSid: msg.callSid,
|
|
491
584
|
from: msg.from,
|
|
492
585
|
to: msg.to,
|
|
@@ -511,17 +604,25 @@ export class RelayConnection {
|
|
|
511
604
|
const otherPartyNumber = isInbound ? msg.from : msg.to;
|
|
512
605
|
const initialActorTrust = resolveActorTrust({
|
|
513
606
|
assistantId,
|
|
514
|
-
sourceChannel:
|
|
607
|
+
sourceChannel: "voice",
|
|
515
608
|
conversationExternalId: otherPartyNumber,
|
|
516
609
|
actorExternalId: otherPartyNumber || undefined,
|
|
517
610
|
});
|
|
518
|
-
const
|
|
611
|
+
const initialTrustContext = toTrustContext(
|
|
612
|
+
initialActorTrust,
|
|
613
|
+
otherPartyNumber,
|
|
614
|
+
);
|
|
519
615
|
|
|
520
|
-
const controller = new CallController(
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
616
|
+
const controller = new CallController(
|
|
617
|
+
this.callSessionId,
|
|
618
|
+
this,
|
|
619
|
+
session?.task ?? null,
|
|
620
|
+
{
|
|
621
|
+
broadcast: globalBroadcast,
|
|
622
|
+
assistantId,
|
|
623
|
+
trustContext: initialTrustContext,
|
|
624
|
+
},
|
|
625
|
+
);
|
|
525
626
|
this.setController(controller);
|
|
526
627
|
|
|
527
628
|
// Detect outbound guardian verification call from persisted call session
|
|
@@ -529,21 +630,37 @@ export class RelayConnection {
|
|
|
529
630
|
// as secondary signal for backward compatibility and observability.
|
|
530
631
|
const persistedMode = session?.callMode;
|
|
531
632
|
const persistedGvSessionId = session?.guardianVerificationSessionId;
|
|
532
|
-
const customParamGvSessionId =
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
633
|
+
const customParamGvSessionId =
|
|
634
|
+
msg.customParameters?.guardianVerificationSessionId;
|
|
635
|
+
const guardianVerificationSessionId =
|
|
636
|
+
persistedGvSessionId ?? customParamGvSessionId;
|
|
637
|
+
|
|
638
|
+
if (
|
|
639
|
+
persistedMode === "guardian_verification" &&
|
|
640
|
+
guardianVerificationSessionId
|
|
641
|
+
) {
|
|
642
|
+
this.startOutboundGuardianVerification(
|
|
643
|
+
assistantId,
|
|
644
|
+
guardianVerificationSessionId,
|
|
645
|
+
msg.to,
|
|
646
|
+
);
|
|
537
647
|
return;
|
|
538
648
|
}
|
|
539
649
|
|
|
540
650
|
// Secondary signal: custom parameter without persisted mode (pre-migration sessions)
|
|
541
651
|
if (!persistedMode && customParamGvSessionId) {
|
|
542
652
|
log.warn(
|
|
543
|
-
{
|
|
544
|
-
|
|
653
|
+
{
|
|
654
|
+
callSessionId: this.callSessionId,
|
|
655
|
+
guardianVerificationSessionId: customParamGvSessionId,
|
|
656
|
+
},
|
|
657
|
+
"Guardian verification detected via setup custom parameter (no persisted call_mode) — entering verification path",
|
|
658
|
+
);
|
|
659
|
+
this.startOutboundGuardianVerification(
|
|
660
|
+
assistantId,
|
|
661
|
+
customParamGvSessionId,
|
|
662
|
+
msg.to,
|
|
545
663
|
);
|
|
546
|
-
this.startOutboundGuardianVerification(assistantId, customParamGvSessionId, msg.to);
|
|
547
664
|
return;
|
|
548
665
|
}
|
|
549
666
|
|
|
@@ -562,7 +679,7 @@ export class RelayConnection {
|
|
|
562
679
|
// verification flow.
|
|
563
680
|
const actorTrust = resolveActorTrust({
|
|
564
681
|
assistantId,
|
|
565
|
-
sourceChannel:
|
|
682
|
+
sourceChannel: "voice",
|
|
566
683
|
conversationExternalId: msg.from,
|
|
567
684
|
actorExternalId: msg.from || undefined,
|
|
568
685
|
});
|
|
@@ -570,9 +687,9 @@ export class RelayConnection {
|
|
|
570
687
|
// Check for a pending voice guardian challenge before the ACL deny
|
|
571
688
|
// gate. An unknown caller with a pending challenge is expected —
|
|
572
689
|
// they need to complete verification to establish a binding.
|
|
573
|
-
const pendingChallenge = getPendingChallenge(assistantId,
|
|
690
|
+
const pendingChallenge = getPendingChallenge(assistantId, "voice");
|
|
574
691
|
|
|
575
|
-
if (actorTrust.trustClass ===
|
|
692
|
+
if (actorTrust.trustClass === "unknown" && !pendingChallenge) {
|
|
576
693
|
// Before entering the name capture flow, check if there is an
|
|
577
694
|
// active voice invite bound to the caller's phone number. If so,
|
|
578
695
|
// enter the invite redemption subflow instead.
|
|
@@ -583,42 +700,54 @@ export class RelayConnection {
|
|
|
583
700
|
expectedExternalUserId: msg.from,
|
|
584
701
|
});
|
|
585
702
|
} catch (err) {
|
|
586
|
-
log.warn(
|
|
703
|
+
log.warn(
|
|
704
|
+
{ err, callSessionId: this.callSessionId },
|
|
705
|
+
"Failed to check voice invites for unknown caller",
|
|
706
|
+
);
|
|
587
707
|
}
|
|
588
708
|
|
|
589
709
|
// Exclude invites that are past their expiresAt even if the DB
|
|
590
710
|
// status hasn't been lazily flipped to 'expired' yet.
|
|
591
711
|
const now = Date.now();
|
|
592
|
-
const nonExpiredInvites = voiceInvites.filter(
|
|
712
|
+
const nonExpiredInvites = voiceInvites.filter(
|
|
713
|
+
(i) => !i.expiresAt || i.expiresAt > now,
|
|
714
|
+
);
|
|
593
715
|
|
|
594
716
|
// Blocked members get immediate denial — the guardian already made
|
|
595
717
|
// an explicit decision to block them. This must be checked before
|
|
596
718
|
// invite redemption so a blocked caller cannot bypass the block by
|
|
597
719
|
// redeeming an active invite.
|
|
598
|
-
if (actorTrust.memberRecord?.status ===
|
|
720
|
+
if (actorTrust.memberRecord?.channel.status === "blocked") {
|
|
599
721
|
log.info(
|
|
600
|
-
{
|
|
601
|
-
|
|
722
|
+
{
|
|
723
|
+
callSessionId: this.callSessionId,
|
|
724
|
+
from: msg.from,
|
|
725
|
+
trustClass: actorTrust.trustClass,
|
|
726
|
+
},
|
|
727
|
+
"Inbound voice ACL: blocked caller denied",
|
|
602
728
|
);
|
|
603
729
|
|
|
604
|
-
recordCallEvent(this.callSessionId,
|
|
730
|
+
recordCallEvent(this.callSessionId, "inbound_acl_denied", {
|
|
605
731
|
from: msg.from,
|
|
606
732
|
trustClass: actorTrust.trustClass,
|
|
607
733
|
denialReason: actorTrust.denialReason,
|
|
608
734
|
});
|
|
609
735
|
|
|
610
|
-
this.sendTextToken(
|
|
736
|
+
this.sendTextToken(
|
|
737
|
+
"This number is not authorized to use this assistant.",
|
|
738
|
+
true,
|
|
739
|
+
);
|
|
611
740
|
|
|
612
|
-
this.connectionState =
|
|
741
|
+
this.connectionState = "disconnecting";
|
|
613
742
|
|
|
614
743
|
updateCallSession(this.callSessionId, {
|
|
615
|
-
status:
|
|
744
|
+
status: "failed",
|
|
616
745
|
endedAt: Date.now(),
|
|
617
|
-
lastError:
|
|
746
|
+
lastError: "Inbound voice ACL: caller blocked",
|
|
618
747
|
});
|
|
619
748
|
|
|
620
749
|
setTimeout(() => {
|
|
621
|
-
this.endSession(
|
|
750
|
+
this.endSession("Inbound voice ACL denied — blocked");
|
|
622
751
|
}, getTtsPlaybackDelayMs());
|
|
623
752
|
return;
|
|
624
753
|
}
|
|
@@ -628,23 +757,36 @@ export class RelayConnection {
|
|
|
628
757
|
const matchedInvite = nonExpiredInvites[0];
|
|
629
758
|
log.info(
|
|
630
759
|
{ callSessionId: this.callSessionId, from: msg.from },
|
|
631
|
-
|
|
760
|
+
"Inbound voice ACL: unknown caller has active voice invite — entering redemption flow",
|
|
761
|
+
);
|
|
762
|
+
this.startInviteRedemption(
|
|
763
|
+
assistantId,
|
|
764
|
+
msg.from,
|
|
765
|
+
matchedInvite.friendName,
|
|
766
|
+
matchedInvite.guardianName,
|
|
632
767
|
);
|
|
633
|
-
this.startInviteRedemption(assistantId, msg.from, matchedInvite.friendName, matchedInvite.guardianName);
|
|
634
768
|
return;
|
|
635
769
|
}
|
|
636
770
|
|
|
637
771
|
// Unknown/revoked/pending callers enter the name capture + guardian
|
|
638
772
|
// approval wait flow instead of being hard-rejected.
|
|
639
773
|
log.info(
|
|
640
|
-
{
|
|
641
|
-
|
|
774
|
+
{
|
|
775
|
+
callSessionId: this.callSessionId,
|
|
776
|
+
from: msg.from,
|
|
777
|
+
trustClass: actorTrust.trustClass,
|
|
778
|
+
},
|
|
779
|
+
"Inbound voice ACL: unknown caller — entering name capture flow",
|
|
642
780
|
);
|
|
643
781
|
|
|
644
|
-
recordCallEvent(
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
782
|
+
recordCallEvent(
|
|
783
|
+
this.callSessionId,
|
|
784
|
+
"inbound_acl_name_capture_started",
|
|
785
|
+
{
|
|
786
|
+
from: msg.from,
|
|
787
|
+
trustClass: actorTrust.trustClass,
|
|
788
|
+
},
|
|
789
|
+
);
|
|
648
790
|
|
|
649
791
|
this.startNameCapture(assistantId, msg.from);
|
|
650
792
|
return;
|
|
@@ -653,31 +795,39 @@ export class RelayConnection {
|
|
|
653
795
|
// Members with policy: 'deny' have status: 'active' so resolveActorTrust
|
|
654
796
|
// classifies them as trusted_contact, but the guardian has explicitly
|
|
655
797
|
// denied their access. Block them the same way the text-channel path does.
|
|
656
|
-
if (actorTrust.memberRecord?.policy ===
|
|
798
|
+
if (actorTrust.memberRecord?.channel.policy === "deny") {
|
|
657
799
|
log.info(
|
|
658
|
-
{
|
|
659
|
-
|
|
800
|
+
{
|
|
801
|
+
callSessionId: this.callSessionId,
|
|
802
|
+
from: msg.from,
|
|
803
|
+
channelId: actorTrust.memberRecord.channel.id,
|
|
804
|
+
trustClass: actorTrust.trustClass,
|
|
805
|
+
},
|
|
806
|
+
"Inbound voice ACL: member policy deny",
|
|
660
807
|
);
|
|
661
808
|
|
|
662
|
-
recordCallEvent(this.callSessionId,
|
|
809
|
+
recordCallEvent(this.callSessionId, "inbound_acl_denied", {
|
|
663
810
|
from: msg.from,
|
|
664
811
|
trustClass: actorTrust.trustClass,
|
|
665
|
-
|
|
666
|
-
memberPolicy: actorTrust.memberRecord.policy,
|
|
812
|
+
channelId: actorTrust.memberRecord.channel.id,
|
|
813
|
+
memberPolicy: actorTrust.memberRecord.channel.policy,
|
|
667
814
|
});
|
|
668
815
|
|
|
669
|
-
this.sendTextToken(
|
|
816
|
+
this.sendTextToken(
|
|
817
|
+
"This number is not authorized to use this assistant.",
|
|
818
|
+
true,
|
|
819
|
+
);
|
|
670
820
|
|
|
671
|
-
this.connectionState =
|
|
821
|
+
this.connectionState = "disconnecting";
|
|
672
822
|
|
|
673
823
|
updateCallSession(this.callSessionId, {
|
|
674
|
-
status:
|
|
824
|
+
status: "failed",
|
|
675
825
|
endedAt: Date.now(),
|
|
676
|
-
lastError:
|
|
826
|
+
lastError: "Inbound voice ACL: member policy deny",
|
|
677
827
|
});
|
|
678
828
|
|
|
679
829
|
setTimeout(() => {
|
|
680
|
-
this.endSession(
|
|
830
|
+
this.endSession("Inbound voice ACL: member policy deny");
|
|
681
831
|
}, getTtsPlaybackDelayMs());
|
|
682
832
|
return;
|
|
683
833
|
}
|
|
@@ -685,31 +835,40 @@ export class RelayConnection {
|
|
|
685
835
|
// Members with policy: 'escalate' require guardian approval, but a live
|
|
686
836
|
// voice call cannot be paused for async approval. Fail-closed by denying
|
|
687
837
|
// the call with an appropriate message — mirrors the deny block above.
|
|
688
|
-
if (actorTrust.memberRecord?.policy ===
|
|
838
|
+
if (actorTrust.memberRecord?.channel.policy === "escalate") {
|
|
689
839
|
log.info(
|
|
690
|
-
{
|
|
691
|
-
|
|
840
|
+
{
|
|
841
|
+
callSessionId: this.callSessionId,
|
|
842
|
+
from: msg.from,
|
|
843
|
+
channelId: actorTrust.memberRecord.channel.id,
|
|
844
|
+
trustClass: actorTrust.trustClass,
|
|
845
|
+
},
|
|
846
|
+
"Inbound voice ACL: member policy escalate — cannot hold live call for guardian approval",
|
|
692
847
|
);
|
|
693
848
|
|
|
694
|
-
recordCallEvent(this.callSessionId,
|
|
849
|
+
recordCallEvent(this.callSessionId, "inbound_acl_denied", {
|
|
695
850
|
from: msg.from,
|
|
696
851
|
trustClass: actorTrust.trustClass,
|
|
697
|
-
|
|
698
|
-
memberPolicy: actorTrust.memberRecord.policy,
|
|
852
|
+
channelId: actorTrust.memberRecord.channel.id,
|
|
853
|
+
memberPolicy: actorTrust.memberRecord.channel.policy,
|
|
699
854
|
});
|
|
700
855
|
|
|
701
|
-
this.sendTextToken(
|
|
856
|
+
this.sendTextToken(
|
|
857
|
+
"This number requires guardian approval for calls. Please have the account guardian update your permissions.",
|
|
858
|
+
true,
|
|
859
|
+
);
|
|
702
860
|
|
|
703
|
-
this.connectionState =
|
|
861
|
+
this.connectionState = "disconnecting";
|
|
704
862
|
|
|
705
863
|
updateCallSession(this.callSessionId, {
|
|
706
|
-
status:
|
|
864
|
+
status: "failed",
|
|
707
865
|
endedAt: Date.now(),
|
|
708
|
-
lastError:
|
|
866
|
+
lastError:
|
|
867
|
+
"Inbound voice ACL: member policy escalate — voice calls cannot await guardian approval",
|
|
709
868
|
});
|
|
710
869
|
|
|
711
870
|
setTimeout(() => {
|
|
712
|
-
this.endSession(
|
|
871
|
+
this.endSession("Inbound voice ACL: member policy escalate");
|
|
713
872
|
}, getTtsPlaybackDelayMs());
|
|
714
873
|
return;
|
|
715
874
|
}
|
|
@@ -717,9 +876,9 @@ export class RelayConnection {
|
|
|
717
876
|
// Guardian and trusted-contact callers proceed normally.
|
|
718
877
|
// Update the controller's guardian context with the trust-resolved
|
|
719
878
|
// context so downstream policy gates have accurate actor metadata.
|
|
720
|
-
if (this.controller && actorTrust.trustClass !==
|
|
721
|
-
const
|
|
722
|
-
this.controller.
|
|
879
|
+
if (this.controller && actorTrust.trustClass !== "unknown") {
|
|
880
|
+
const resolvedTrustContext = toTrustContext(actorTrust, msg.from);
|
|
881
|
+
this.controller.setTrustContext(resolvedTrustContext);
|
|
723
882
|
}
|
|
724
883
|
|
|
725
884
|
if (pendingChallenge) {
|
|
@@ -742,22 +901,27 @@ export class RelayConnection {
|
|
|
742
901
|
this.verificationMaxAttempts = verificationConfig.maxAttempts;
|
|
743
902
|
this.verificationCodeLength = verificationConfig.codeLength;
|
|
744
903
|
this.verificationAttempts = 0;
|
|
745
|
-
this.dtmfBuffer =
|
|
904
|
+
this.dtmfBuffer = "";
|
|
746
905
|
|
|
747
906
|
// Generate a random numeric code
|
|
748
907
|
const maxValue = Math.pow(10, this.verificationCodeLength);
|
|
749
|
-
const code = randomInt(0, maxValue)
|
|
908
|
+
const code = randomInt(0, maxValue)
|
|
909
|
+
.toString()
|
|
910
|
+
.padStart(this.verificationCodeLength, "0");
|
|
750
911
|
this.verificationCode = code;
|
|
751
|
-
this.connectionState =
|
|
912
|
+
this.connectionState = "verification_pending";
|
|
752
913
|
|
|
753
|
-
recordCallEvent(this.callSessionId,
|
|
914
|
+
recordCallEvent(this.callSessionId, "callee_verification_started", {
|
|
754
915
|
codeLength: this.verificationCodeLength,
|
|
755
916
|
maxAttempts: this.verificationMaxAttempts,
|
|
756
917
|
});
|
|
757
918
|
|
|
758
919
|
// Send a TTS prompt with the code spoken digit by digit
|
|
759
|
-
const spokenCode = code.split(
|
|
760
|
-
this.sendTextToken(
|
|
920
|
+
const spokenCode = code.split("").join(". ");
|
|
921
|
+
this.sendTextToken(
|
|
922
|
+
`Please enter the verification code: ${spokenCode}.`,
|
|
923
|
+
true,
|
|
924
|
+
);
|
|
761
925
|
|
|
762
926
|
// Post the verification code to the initiating conversation so the
|
|
763
927
|
// guardian (user) can share it with the callee.
|
|
@@ -765,15 +929,23 @@ export class RelayConnection {
|
|
|
765
929
|
const codeMsg = `\u{1F510} Verification code for call to ${session.toNumber}: ${code}`;
|
|
766
930
|
await conversationStore.addMessage(
|
|
767
931
|
session.initiatedFromConversationId,
|
|
768
|
-
|
|
769
|
-
JSON.stringify([{ type:
|
|
770
|
-
{
|
|
932
|
+
"assistant",
|
|
933
|
+
JSON.stringify([{ type: "text", text: codeMsg }]),
|
|
934
|
+
{
|
|
935
|
+
userMessageChannel: "voice",
|
|
936
|
+
assistantMessageChannel: "voice",
|
|
937
|
+
userMessageInterface: "voice",
|
|
938
|
+
assistantMessageInterface: "voice",
|
|
939
|
+
},
|
|
771
940
|
);
|
|
772
941
|
}
|
|
773
942
|
|
|
774
943
|
log.info(
|
|
775
|
-
{
|
|
776
|
-
|
|
944
|
+
{
|
|
945
|
+
callSessionId: this.callSessionId,
|
|
946
|
+
codeLength: this.verificationCodeLength,
|
|
947
|
+
},
|
|
948
|
+
"Callee verification started",
|
|
777
949
|
);
|
|
778
950
|
}
|
|
779
951
|
|
|
@@ -781,13 +953,91 @@ export class RelayConnection {
|
|
|
781
953
|
* Start normal call flow — fire the controller greeting unless a
|
|
782
954
|
* static welcome greeting is configured.
|
|
783
955
|
*/
|
|
784
|
-
private startNormalCallFlow(
|
|
956
|
+
private startNormalCallFlow(
|
|
957
|
+
controller: CallController,
|
|
958
|
+
isInbound: boolean,
|
|
959
|
+
): void {
|
|
785
960
|
const hasStaticGreeting = !!process.env.CALL_WELCOME_GREETING?.trim();
|
|
786
961
|
if (!hasStaticGreeting) {
|
|
787
|
-
controller
|
|
788
|
-
|
|
962
|
+
controller
|
|
963
|
+
.startInitialGreeting()
|
|
964
|
+
.catch((err) =>
|
|
965
|
+
log.error(
|
|
966
|
+
{ err, callSessionId: this.callSessionId },
|
|
967
|
+
`Failed to start initial ${isInbound ? "inbound" : "outbound"} greeting`,
|
|
968
|
+
),
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Shared post-activation handoff for all trusted-contact success paths
|
|
975
|
+
* (access-request approval, invite redemption, verification code).
|
|
976
|
+
* Activates the caller, updates guardian context, delivers deterministic
|
|
977
|
+
* transition copy, and marks the next utterance as opening-ack so the
|
|
978
|
+
* LLM continues naturally.
|
|
979
|
+
*/
|
|
980
|
+
private continueCallAfterTrustedContactActivation(params: {
|
|
981
|
+
assistantId: string;
|
|
982
|
+
fromNumber: string;
|
|
983
|
+
callerName?: string;
|
|
984
|
+
skipMemberActivation?: boolean;
|
|
985
|
+
}): void {
|
|
986
|
+
const { assistantId, fromNumber, callerName } = params;
|
|
987
|
+
|
|
988
|
+
if (!params.skipMemberActivation) {
|
|
989
|
+
try {
|
|
990
|
+
upsertMember({
|
|
991
|
+
assistantId,
|
|
992
|
+
sourceChannel: "voice",
|
|
993
|
+
externalUserId: fromNumber,
|
|
994
|
+
externalChatId: fromNumber,
|
|
995
|
+
displayName: callerName,
|
|
996
|
+
status: "active",
|
|
997
|
+
policy: "allow",
|
|
998
|
+
});
|
|
999
|
+
} catch (err) {
|
|
1000
|
+
log.error(
|
|
1001
|
+
{ err, callSessionId: this.callSessionId },
|
|
1002
|
+
"Failed to activate voice caller as trusted contact",
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
const updatedTrust = resolveActorTrust({
|
|
1008
|
+
assistantId,
|
|
1009
|
+
sourceChannel: "voice",
|
|
1010
|
+
conversationExternalId: fromNumber,
|
|
1011
|
+
actorExternalId: fromNumber,
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
if (this.controller) {
|
|
1015
|
+
this.controller.setTrustContext(toTrustContext(updatedTrust, fromNumber));
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
this.connectionState = "connected";
|
|
1019
|
+
updateCallSession(this.callSessionId, { status: "in_progress" });
|
|
1020
|
+
|
|
1021
|
+
const guardianLabel = this.resolveGuardianLabel();
|
|
1022
|
+
const handoffText = `Great! ${guardianLabel} said I can speak with you. How can I help?`;
|
|
1023
|
+
this.sendTextToken(handoffText, true);
|
|
1024
|
+
|
|
1025
|
+
recordCallEvent(this.callSessionId, "assistant_spoke", {
|
|
1026
|
+
text: handoffText,
|
|
1027
|
+
});
|
|
1028
|
+
const session = getCallSession(this.callSessionId);
|
|
1029
|
+
if (session) {
|
|
1030
|
+
fireCallTranscriptNotifier(
|
|
1031
|
+
session.conversationId,
|
|
1032
|
+
this.callSessionId,
|
|
1033
|
+
"assistant",
|
|
1034
|
+
handoffText,
|
|
789
1035
|
);
|
|
790
1036
|
}
|
|
1037
|
+
|
|
1038
|
+
if (this.controller) {
|
|
1039
|
+
this.controller.markNextCallerTurnAsOpeningAck();
|
|
1040
|
+
}
|
|
791
1041
|
}
|
|
792
1042
|
|
|
793
1043
|
/**
|
|
@@ -795,29 +1045,32 @@ export class RelayConnection {
|
|
|
795
1045
|
* voice guardian challenge. Prompts the caller to enter their six-digit
|
|
796
1046
|
* verification code via DTMF or by speaking it.
|
|
797
1047
|
*/
|
|
798
|
-
private startInboundGuardianVerification(
|
|
1048
|
+
private startInboundGuardianVerification(
|
|
1049
|
+
assistantId: string,
|
|
1050
|
+
fromNumber: string,
|
|
1051
|
+
): void {
|
|
799
1052
|
this.guardianVerificationActive = true;
|
|
800
1053
|
this.guardianChallengeAssistantId = assistantId;
|
|
801
1054
|
this.guardianVerificationFromNumber = fromNumber;
|
|
802
|
-
this.connectionState =
|
|
1055
|
+
this.connectionState = "verification_pending";
|
|
803
1056
|
this.verificationAttempts = 0;
|
|
804
1057
|
this.verificationMaxAttempts = 3;
|
|
805
1058
|
this.verificationCodeLength = 6;
|
|
806
|
-
this.dtmfBuffer =
|
|
1059
|
+
this.dtmfBuffer = "";
|
|
807
1060
|
|
|
808
|
-
recordCallEvent(this.callSessionId,
|
|
1061
|
+
recordCallEvent(this.callSessionId, "guardian_voice_verification_started", {
|
|
809
1062
|
assistantId,
|
|
810
1063
|
maxAttempts: this.verificationMaxAttempts,
|
|
811
1064
|
});
|
|
812
1065
|
|
|
813
1066
|
this.sendTextToken(
|
|
814
|
-
|
|
1067
|
+
"Welcome. Please enter your six-digit verification code using your keypad, or speak the digits now.",
|
|
815
1068
|
true,
|
|
816
1069
|
);
|
|
817
1070
|
|
|
818
1071
|
log.info(
|
|
819
1072
|
{ callSessionId: this.callSessionId, assistantId },
|
|
820
|
-
|
|
1073
|
+
"Inbound guardian voice verification started",
|
|
821
1074
|
);
|
|
822
1075
|
}
|
|
823
1076
|
|
|
@@ -836,17 +1089,21 @@ export class RelayConnection {
|
|
|
836
1089
|
this.guardianChallengeAssistantId = assistantId;
|
|
837
1090
|
// For outbound guardian calls, the "to" number is the guardian's phone
|
|
838
1091
|
this.guardianVerificationFromNumber = toNumber;
|
|
839
|
-
this.connectionState =
|
|
1092
|
+
this.connectionState = "verification_pending";
|
|
840
1093
|
this.verificationAttempts = 0;
|
|
841
1094
|
this.verificationMaxAttempts = 3;
|
|
842
1095
|
this.verificationCodeLength = 6;
|
|
843
|
-
this.dtmfBuffer =
|
|
1096
|
+
this.dtmfBuffer = "";
|
|
844
1097
|
|
|
845
|
-
recordCallEvent(
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1098
|
+
recordCallEvent(
|
|
1099
|
+
this.callSessionId,
|
|
1100
|
+
"outbound_guardian_voice_verification_started",
|
|
1101
|
+
{
|
|
1102
|
+
assistantId,
|
|
1103
|
+
guardianVerificationSessionId,
|
|
1104
|
+
maxAttempts: this.verificationMaxAttempts,
|
|
1105
|
+
},
|
|
1106
|
+
);
|
|
850
1107
|
|
|
851
1108
|
const introText = composeVerificationVoice(
|
|
852
1109
|
GUARDIAN_VERIFY_TEMPLATE_KEYS.VOICE_CALL_INTRO,
|
|
@@ -855,8 +1112,12 @@ export class RelayConnection {
|
|
|
855
1112
|
this.sendTextToken(introText, true);
|
|
856
1113
|
|
|
857
1114
|
log.info(
|
|
858
|
-
{
|
|
859
|
-
|
|
1115
|
+
{
|
|
1116
|
+
callSessionId: this.callSessionId,
|
|
1117
|
+
assistantId,
|
|
1118
|
+
guardianVerificationSessionId,
|
|
1119
|
+
},
|
|
1120
|
+
"Outbound guardian voice verification started",
|
|
860
1121
|
);
|
|
861
1122
|
}
|
|
862
1123
|
|
|
@@ -866,16 +1127,24 @@ export class RelayConnection {
|
|
|
866
1127
|
*/
|
|
867
1128
|
private static parseDigitsFromSpeech(transcript: string): string {
|
|
868
1129
|
const wordToDigit: Record<string, string> = {
|
|
869
|
-
zero:
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1130
|
+
zero: "0",
|
|
1131
|
+
oh: "0",
|
|
1132
|
+
o: "0",
|
|
1133
|
+
one: "1",
|
|
1134
|
+
won: "1",
|
|
1135
|
+
two: "2",
|
|
1136
|
+
too: "2",
|
|
1137
|
+
to: "2",
|
|
1138
|
+
three: "3",
|
|
1139
|
+
four: "4",
|
|
1140
|
+
for: "4",
|
|
1141
|
+
fore: "4",
|
|
1142
|
+
five: "5",
|
|
1143
|
+
six: "6",
|
|
1144
|
+
seven: "7",
|
|
1145
|
+
eight: "8",
|
|
1146
|
+
ate: "8",
|
|
1147
|
+
nine: "9",
|
|
879
1148
|
};
|
|
880
1149
|
|
|
881
1150
|
const digits: string[] = [];
|
|
@@ -890,11 +1159,11 @@ export class RelayConnection {
|
|
|
890
1159
|
digits.push(wordToDigit[token]);
|
|
891
1160
|
} else if (/^\d+$/.test(token)) {
|
|
892
1161
|
// Multi-digit number like "123456" — split into individual digits
|
|
893
|
-
digits.push(...token.split(
|
|
1162
|
+
digits.push(...token.split(""));
|
|
894
1163
|
}
|
|
895
1164
|
}
|
|
896
1165
|
|
|
897
|
-
return digits.join(
|
|
1166
|
+
return digits.join("");
|
|
898
1167
|
}
|
|
899
1168
|
|
|
900
1169
|
/**
|
|
@@ -906,7 +1175,10 @@ export class RelayConnection {
|
|
|
906
1175
|
* On failure, enforces max attempts and terminates the call if exhausted.
|
|
907
1176
|
*/
|
|
908
1177
|
private attemptGuardianCodeVerification(enteredCode: string): void {
|
|
909
|
-
if (
|
|
1178
|
+
if (
|
|
1179
|
+
!this.guardianChallengeAssistantId ||
|
|
1180
|
+
!this.guardianVerificationFromNumber
|
|
1181
|
+
) {
|
|
910
1182
|
return;
|
|
911
1183
|
}
|
|
912
1184
|
|
|
@@ -915,37 +1187,49 @@ export class RelayConnection {
|
|
|
915
1187
|
|
|
916
1188
|
const result = validateAndConsumeChallenge(
|
|
917
1189
|
this.guardianChallengeAssistantId,
|
|
918
|
-
|
|
1190
|
+
"voice",
|
|
919
1191
|
enteredCode,
|
|
920
1192
|
this.guardianVerificationFromNumber,
|
|
921
1193
|
this.guardianVerificationFromNumber,
|
|
922
1194
|
);
|
|
923
1195
|
|
|
924
1196
|
if (result.success) {
|
|
925
|
-
|
|
926
|
-
this.connectionState = 'connected';
|
|
1197
|
+
this.connectionState = "connected";
|
|
927
1198
|
this.guardianVerificationActive = false;
|
|
928
1199
|
this.verificationAttempts = 0;
|
|
929
|
-
this.dtmfBuffer =
|
|
1200
|
+
this.dtmfBuffer = "";
|
|
930
1201
|
|
|
931
1202
|
const eventName = isOutbound
|
|
932
|
-
?
|
|
933
|
-
:
|
|
1203
|
+
? "outbound_guardian_voice_verification_succeeded"
|
|
1204
|
+
: "guardian_voice_verification_succeeded";
|
|
934
1205
|
|
|
935
1206
|
recordCallEvent(this.callSessionId, eventName, {
|
|
936
|
-
|
|
1207
|
+
verificationType: result.verificationType,
|
|
937
1208
|
});
|
|
938
1209
|
log.info(
|
|
939
1210
|
{ callSessionId: this.callSessionId, isOutbound },
|
|
940
|
-
|
|
1211
|
+
"Guardian voice verification succeeded",
|
|
941
1212
|
);
|
|
942
1213
|
|
|
1214
|
+
// Create the guardian binding now that verification succeeded.
|
|
1215
|
+
if (result.verificationType === "guardian") {
|
|
1216
|
+
revokeGuardianBinding(this.guardianChallengeAssistantId, "voice");
|
|
1217
|
+
createGuardianBinding({
|
|
1218
|
+
assistantId: this.guardianChallengeAssistantId,
|
|
1219
|
+
channel: "voice",
|
|
1220
|
+
guardianExternalUserId: this.guardianVerificationFromNumber,
|
|
1221
|
+
guardianDeliveryChatId: this.guardianVerificationFromNumber,
|
|
1222
|
+
guardianPrincipalId: this.guardianVerificationFromNumber,
|
|
1223
|
+
verifiedVia: "challenge",
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
|
|
943
1227
|
if (isOutbound) {
|
|
944
1228
|
// Outbound guardian verification: play success and hang up.
|
|
945
1229
|
// There is no normal conversation to transition to.
|
|
946
1230
|
// Set disconnecting to ignore any further DTMF/speech input
|
|
947
1231
|
// during the brief delay before the session ends.
|
|
948
|
-
this.connectionState =
|
|
1232
|
+
this.connectionState = "disconnecting";
|
|
949
1233
|
|
|
950
1234
|
const successText = composeVerificationVoice(
|
|
951
1235
|
GUARDIAN_VERIFY_TEMPLATE_KEYS.VOICE_SUCCESS,
|
|
@@ -954,7 +1238,7 @@ export class RelayConnection {
|
|
|
954
1238
|
this.sendTextToken(successText, true);
|
|
955
1239
|
|
|
956
1240
|
updateCallSession(this.callSessionId, {
|
|
957
|
-
status:
|
|
1241
|
+
status: "completed",
|
|
958
1242
|
endedAt: Date.now(),
|
|
959
1243
|
});
|
|
960
1244
|
|
|
@@ -964,28 +1248,83 @@ export class RelayConnection {
|
|
|
964
1248
|
if (successSession?.initiatedFromConversationId) {
|
|
965
1249
|
addPointerMessage(
|
|
966
1250
|
successSession.initiatedFromConversationId,
|
|
967
|
-
|
|
1251
|
+
"guardian_verification_succeeded",
|
|
968
1252
|
successSession.toNumber,
|
|
969
|
-
{ channel:
|
|
1253
|
+
{ channel: "voice" },
|
|
970
1254
|
).catch((err) => {
|
|
971
|
-
log.warn(
|
|
1255
|
+
log.warn(
|
|
1256
|
+
{
|
|
1257
|
+
conversationId: successSession.initiatedFromConversationId,
|
|
1258
|
+
err,
|
|
1259
|
+
},
|
|
1260
|
+
"Skipping pointer write — origin conversation may no longer exist",
|
|
1261
|
+
);
|
|
972
1262
|
});
|
|
973
1263
|
}
|
|
974
1264
|
|
|
975
1265
|
setTimeout(() => {
|
|
976
|
-
this.endSession(
|
|
1266
|
+
this.endSession("Verified — guardian challenge passed");
|
|
977
1267
|
}, getTtsPlaybackDelayMs());
|
|
1268
|
+
} else if (result.verificationType === "trusted_contact") {
|
|
1269
|
+
// Inbound trusted-contact verification: activate and continue
|
|
1270
|
+
// the live call with the shared handoff primitive.
|
|
1271
|
+
this.continueCallAfterTrustedContactActivation({
|
|
1272
|
+
assistantId: this.guardianChallengeAssistantId,
|
|
1273
|
+
fromNumber: this.guardianVerificationFromNumber,
|
|
1274
|
+
});
|
|
978
1275
|
} else {
|
|
979
|
-
// Inbound:
|
|
1276
|
+
// Inbound guardian verification: create/update binding, then proceed
|
|
1277
|
+
// to normal call flow. Mirrors the binding creation logic in
|
|
1278
|
+
// verification-intercept.ts for the inbound channel path.
|
|
1279
|
+
const guardianAssistantId = this.guardianChallengeAssistantId;
|
|
1280
|
+
const callerNumber = this.guardianVerificationFromNumber;
|
|
1281
|
+
|
|
1282
|
+
const existingBinding = getGuardianBinding(
|
|
1283
|
+
guardianAssistantId,
|
|
1284
|
+
"voice",
|
|
1285
|
+
);
|
|
1286
|
+
if (
|
|
1287
|
+
existingBinding &&
|
|
1288
|
+
existingBinding.guardianExternalUserId !== callerNumber
|
|
1289
|
+
) {
|
|
1290
|
+
log.warn(
|
|
1291
|
+
{
|
|
1292
|
+
sourceChannel: "voice",
|
|
1293
|
+
existingGuardian: existingBinding.guardianExternalUserId,
|
|
1294
|
+
},
|
|
1295
|
+
"Guardian binding conflict: another user already holds the voice channel binding",
|
|
1296
|
+
);
|
|
1297
|
+
} else {
|
|
1298
|
+
revokeGuardianBinding(guardianAssistantId, "voice");
|
|
1299
|
+
|
|
1300
|
+
// Resolve canonical principal from the vellum channel binding
|
|
1301
|
+
// so all channel bindings share a single principal identity.
|
|
1302
|
+
const vellumBinding = getGuardianBinding(
|
|
1303
|
+
guardianAssistantId,
|
|
1304
|
+
"vellum",
|
|
1305
|
+
);
|
|
1306
|
+
const canonicalPrincipal =
|
|
1307
|
+
vellumBinding?.guardianPrincipalId ?? callerNumber;
|
|
1308
|
+
|
|
1309
|
+
createGuardianBinding({
|
|
1310
|
+
assistantId: guardianAssistantId,
|
|
1311
|
+
channel: "voice",
|
|
1312
|
+
guardianExternalUserId: callerNumber,
|
|
1313
|
+
guardianDeliveryChatId: callerNumber,
|
|
1314
|
+
guardianPrincipalId: canonicalPrincipal,
|
|
1315
|
+
verifiedVia: "challenge",
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
|
|
980
1319
|
if (this.controller) {
|
|
981
1320
|
const verifiedActorTrust = resolveActorTrust({
|
|
982
|
-
assistantId:
|
|
983
|
-
sourceChannel:
|
|
984
|
-
conversationExternalId:
|
|
985
|
-
actorExternalId:
|
|
1321
|
+
assistantId: guardianAssistantId,
|
|
1322
|
+
sourceChannel: "voice",
|
|
1323
|
+
conversationExternalId: callerNumber,
|
|
1324
|
+
actorExternalId: callerNumber,
|
|
986
1325
|
});
|
|
987
|
-
this.controller.
|
|
988
|
-
|
|
1326
|
+
this.controller.setTrustContext(
|
|
1327
|
+
toTrustContext(verifiedActorTrust, callerNumber),
|
|
989
1328
|
);
|
|
990
1329
|
this.startNormalCallFlow(this.controller, true);
|
|
991
1330
|
}
|
|
@@ -999,61 +1338,99 @@ export class RelayConnection {
|
|
|
999
1338
|
this.guardianVerificationActive = false;
|
|
1000
1339
|
|
|
1001
1340
|
const failEventName = isOutbound
|
|
1002
|
-
?
|
|
1003
|
-
:
|
|
1341
|
+
? "outbound_guardian_voice_verification_failed"
|
|
1342
|
+
: "guardian_voice_verification_failed";
|
|
1004
1343
|
|
|
1005
1344
|
recordCallEvent(this.callSessionId, failEventName, {
|
|
1006
1345
|
attempts: this.verificationAttempts,
|
|
1007
1346
|
});
|
|
1008
1347
|
log.warn(
|
|
1009
|
-
{
|
|
1010
|
-
|
|
1348
|
+
{
|
|
1349
|
+
callSessionId: this.callSessionId,
|
|
1350
|
+
attempts: this.verificationAttempts,
|
|
1351
|
+
isOutbound,
|
|
1352
|
+
},
|
|
1353
|
+
"Guardian voice verification failed — max attempts reached",
|
|
1011
1354
|
);
|
|
1012
1355
|
|
|
1013
1356
|
const failureText = isOutbound
|
|
1014
|
-
? composeVerificationVoice(
|
|
1015
|
-
|
|
1357
|
+
? composeVerificationVoice(
|
|
1358
|
+
GUARDIAN_VERIFY_TEMPLATE_KEYS.VOICE_FAILURE,
|
|
1359
|
+
{ codeDigits },
|
|
1360
|
+
)
|
|
1361
|
+
: "Verification failed. Goodbye.";
|
|
1016
1362
|
this.sendTextToken(failureText, true);
|
|
1017
1363
|
|
|
1018
1364
|
updateCallSession(this.callSessionId, {
|
|
1019
|
-
status:
|
|
1365
|
+
status: "failed",
|
|
1020
1366
|
endedAt: Date.now(),
|
|
1021
|
-
lastError:
|
|
1367
|
+
lastError:
|
|
1368
|
+
"Guardian voice verification failed — max attempts exceeded",
|
|
1022
1369
|
});
|
|
1023
1370
|
|
|
1024
1371
|
const failSession = getCallSession(this.callSessionId);
|
|
1025
1372
|
if (failSession) {
|
|
1026
1373
|
expirePendingQuestions(this.callSessionId);
|
|
1027
|
-
persistCallCompletionMessage(
|
|
1028
|
-
|
|
1374
|
+
persistCallCompletionMessage(
|
|
1375
|
+
failSession.conversationId,
|
|
1376
|
+
this.callSessionId,
|
|
1377
|
+
).catch((err) => {
|
|
1378
|
+
log.error(
|
|
1379
|
+
{
|
|
1380
|
+
err,
|
|
1381
|
+
conversationId: failSession.conversationId,
|
|
1382
|
+
callSessionId: this.callSessionId,
|
|
1383
|
+
},
|
|
1384
|
+
"Failed to persist call completion message",
|
|
1385
|
+
);
|
|
1029
1386
|
});
|
|
1030
|
-
fireCallCompletionNotifier(
|
|
1387
|
+
fireCallCompletionNotifier(
|
|
1388
|
+
failSession.conversationId,
|
|
1389
|
+
this.callSessionId,
|
|
1390
|
+
);
|
|
1031
1391
|
|
|
1032
1392
|
// Emit a pointer message to the origin conversation so the
|
|
1033
1393
|
// requesting chat sees a deterministic failure notice.
|
|
1034
1394
|
if (isOutbound && failSession.initiatedFromConversationId) {
|
|
1035
1395
|
addPointerMessage(
|
|
1036
1396
|
failSession.initiatedFromConversationId,
|
|
1037
|
-
|
|
1397
|
+
"guardian_verification_failed",
|
|
1038
1398
|
failSession.toNumber,
|
|
1039
|
-
{
|
|
1399
|
+
{
|
|
1400
|
+
channel: "voice",
|
|
1401
|
+
reason: "Max verification attempts exceeded",
|
|
1402
|
+
},
|
|
1040
1403
|
).catch((err) => {
|
|
1041
|
-
log.warn(
|
|
1404
|
+
log.warn(
|
|
1405
|
+
{
|
|
1406
|
+
conversationId: failSession.initiatedFromConversationId,
|
|
1407
|
+
err,
|
|
1408
|
+
},
|
|
1409
|
+
"Skipping pointer write — origin conversation may no longer exist",
|
|
1410
|
+
);
|
|
1042
1411
|
});
|
|
1043
1412
|
}
|
|
1044
1413
|
}
|
|
1045
1414
|
|
|
1046
1415
|
setTimeout(() => {
|
|
1047
|
-
this.endSession(
|
|
1416
|
+
this.endSession("Verification failed — challenge rejected");
|
|
1048
1417
|
}, getTtsPlaybackDelayMs());
|
|
1049
1418
|
} else {
|
|
1050
1419
|
const retryText = isOutbound
|
|
1051
|
-
? composeVerificationVoice(
|
|
1052
|
-
|
|
1420
|
+
? composeVerificationVoice(
|
|
1421
|
+
GUARDIAN_VERIFY_TEMPLATE_KEYS.VOICE_RETRY,
|
|
1422
|
+
{ codeDigits },
|
|
1423
|
+
)
|
|
1424
|
+
: "That code was incorrect. Please try again.";
|
|
1053
1425
|
|
|
1054
1426
|
log.info(
|
|
1055
|
-
{
|
|
1056
|
-
|
|
1427
|
+
{
|
|
1428
|
+
callSessionId: this.callSessionId,
|
|
1429
|
+
attempt: this.verificationAttempts,
|
|
1430
|
+
maxAttempts: this.verificationMaxAttempts,
|
|
1431
|
+
isOutbound,
|
|
1432
|
+
},
|
|
1433
|
+
"Guardian voice verification attempt failed — retrying",
|
|
1057
1434
|
);
|
|
1058
1435
|
this.sendTextToken(retryText, true);
|
|
1059
1436
|
}
|
|
@@ -1065,26 +1442,31 @@ export class RelayConnection {
|
|
|
1065
1442
|
* who has an active voice invite. Prompts the caller to enter their
|
|
1066
1443
|
* invite code via DTMF or speech.
|
|
1067
1444
|
*/
|
|
1068
|
-
private startInviteRedemption(
|
|
1445
|
+
private startInviteRedemption(
|
|
1446
|
+
assistantId: string,
|
|
1447
|
+
fromNumber: string,
|
|
1448
|
+
friendName: string | null,
|
|
1449
|
+
guardianName: string | null,
|
|
1450
|
+
): void {
|
|
1069
1451
|
this.inviteRedemptionActive = true;
|
|
1070
1452
|
this.inviteRedemptionAssistantId = assistantId;
|
|
1071
1453
|
this.inviteRedemptionFromNumber = fromNumber;
|
|
1072
1454
|
this.inviteRedemptionFriendName = friendName;
|
|
1073
1455
|
this.inviteRedemptionGuardianName = guardianName;
|
|
1074
|
-
this.connectionState =
|
|
1456
|
+
this.connectionState = "verification_pending";
|
|
1075
1457
|
this.verificationAttempts = 0;
|
|
1076
1458
|
this.verificationMaxAttempts = 1;
|
|
1077
1459
|
this.inviteRedemptionCodeLength = 6;
|
|
1078
|
-
this.dtmfBuffer =
|
|
1460
|
+
this.dtmfBuffer = "";
|
|
1079
1461
|
|
|
1080
|
-
recordCallEvent(this.callSessionId,
|
|
1462
|
+
recordCallEvent(this.callSessionId, "invite_redemption_started", {
|
|
1081
1463
|
assistantId,
|
|
1082
1464
|
codeLength: 6,
|
|
1083
1465
|
maxAttempts: this.verificationMaxAttempts,
|
|
1084
1466
|
});
|
|
1085
1467
|
|
|
1086
|
-
const displayFriend = friendName ??
|
|
1087
|
-
const displayGuardian = guardianName ??
|
|
1468
|
+
const displayFriend = friendName ?? "there";
|
|
1469
|
+
const displayGuardian = guardianName ?? "your contact";
|
|
1088
1470
|
this.sendTextToken(
|
|
1089
1471
|
`Welcome ${displayFriend}. Please enter the 6-digit code that ${displayGuardian} provided you to verify your identity.`,
|
|
1090
1472
|
true,
|
|
@@ -1092,7 +1474,7 @@ export class RelayConnection {
|
|
|
1092
1474
|
|
|
1093
1475
|
log.info(
|
|
1094
1476
|
{ callSessionId: this.callSessionId, assistantId },
|
|
1095
|
-
|
|
1477
|
+
"Inbound voice invite redemption started",
|
|
1096
1478
|
);
|
|
1097
1479
|
}
|
|
1098
1480
|
|
|
@@ -1104,7 +1486,7 @@ export class RelayConnection {
|
|
|
1104
1486
|
private startNameCapture(assistantId: string, fromNumber: string): void {
|
|
1105
1487
|
this.accessRequestAssistantId = assistantId;
|
|
1106
1488
|
this.accessRequestFromNumber = fromNumber;
|
|
1107
|
-
this.connectionState =
|
|
1489
|
+
this.connectionState = "awaiting_name";
|
|
1108
1490
|
|
|
1109
1491
|
const guardianLabel = this.resolveGuardianLabel();
|
|
1110
1492
|
const assistantName = this.resolveAssistantLabel();
|
|
@@ -1120,13 +1502,17 @@ export class RelayConnection {
|
|
|
1120
1502
|
// to avoid wasting resources on callers who never respond.
|
|
1121
1503
|
const NAME_CAPTURE_TIMEOUT_MS = 30_000;
|
|
1122
1504
|
this.nameCaptureTimeoutTimer = setTimeout(() => {
|
|
1123
|
-
if (this.connectionState !==
|
|
1505
|
+
if (this.connectionState !== "awaiting_name") return;
|
|
1124
1506
|
this.handleNameCaptureTimeout();
|
|
1125
1507
|
}, NAME_CAPTURE_TIMEOUT_MS);
|
|
1126
1508
|
|
|
1127
1509
|
log.info(
|
|
1128
|
-
{
|
|
1129
|
-
|
|
1510
|
+
{
|
|
1511
|
+
callSessionId: this.callSessionId,
|
|
1512
|
+
assistantId,
|
|
1513
|
+
timeoutMs: NAME_CAPTURE_TIMEOUT_MS,
|
|
1514
|
+
},
|
|
1515
|
+
"Name capture started for unknown inbound caller",
|
|
1130
1516
|
);
|
|
1131
1517
|
}
|
|
1132
1518
|
|
|
@@ -1148,7 +1534,7 @@ export class RelayConnection {
|
|
|
1148
1534
|
|
|
1149
1535
|
this.accessRequestCallerName = callerName;
|
|
1150
1536
|
|
|
1151
|
-
recordCallEvent(this.callSessionId,
|
|
1537
|
+
recordCallEvent(this.callSessionId, "inbound_acl_name_captured", {
|
|
1152
1538
|
from: this.accessRequestFromNumber,
|
|
1153
1539
|
callerName,
|
|
1154
1540
|
});
|
|
@@ -1158,7 +1544,7 @@ export class RelayConnection {
|
|
|
1158
1544
|
try {
|
|
1159
1545
|
const accessResult = notifyGuardianOfAccessRequest({
|
|
1160
1546
|
canonicalAssistantId: this.accessRequestAssistantId,
|
|
1161
|
-
sourceChannel:
|
|
1547
|
+
sourceChannel: "voice",
|
|
1162
1548
|
conversationExternalId: this.accessRequestFromNumber,
|
|
1163
1549
|
actorExternalId: this.accessRequestFromNumber,
|
|
1164
1550
|
actorDisplayName: callerName,
|
|
@@ -1167,17 +1553,24 @@ export class RelayConnection {
|
|
|
1167
1553
|
if (accessResult.notified) {
|
|
1168
1554
|
this.accessRequestId = accessResult.requestId;
|
|
1169
1555
|
log.info(
|
|
1170
|
-
{
|
|
1171
|
-
|
|
1556
|
+
{
|
|
1557
|
+
callSessionId: this.callSessionId,
|
|
1558
|
+
requestId: accessResult.requestId,
|
|
1559
|
+
callerName,
|
|
1560
|
+
},
|
|
1561
|
+
"Guardian notified of voice access request with caller name",
|
|
1172
1562
|
);
|
|
1173
1563
|
} else {
|
|
1174
1564
|
log.warn(
|
|
1175
1565
|
{ callSessionId: this.callSessionId },
|
|
1176
|
-
|
|
1566
|
+
"Failed to notify guardian of voice access request — no sender ID",
|
|
1177
1567
|
);
|
|
1178
1568
|
}
|
|
1179
1569
|
} catch (err) {
|
|
1180
|
-
log.error(
|
|
1570
|
+
log.error(
|
|
1571
|
+
{ err, callSessionId: this.callSessionId },
|
|
1572
|
+
"Failed to create access request for voice caller",
|
|
1573
|
+
);
|
|
1181
1574
|
}
|
|
1182
1575
|
|
|
1183
1576
|
// If the access request was not successfully created (notifyGuardianOfAccessRequest
|
|
@@ -1186,7 +1579,7 @@ export class RelayConnection {
|
|
|
1186
1579
|
if (!this.accessRequestId) {
|
|
1187
1580
|
log.warn(
|
|
1188
1581
|
{ callSessionId: this.callSessionId },
|
|
1189
|
-
|
|
1582
|
+
"Access request ID is null after notification attempt — failing closed",
|
|
1190
1583
|
);
|
|
1191
1584
|
this.handleAccessRequestTimeout();
|
|
1192
1585
|
return;
|
|
@@ -1202,7 +1595,7 @@ export class RelayConnection {
|
|
|
1202
1595
|
*/
|
|
1203
1596
|
private startAccessRequestWait(): void {
|
|
1204
1597
|
this.accessRequestWaitActive = true;
|
|
1205
|
-
this.connectionState =
|
|
1598
|
+
this.connectionState = "awaiting_guardian_decision";
|
|
1206
1599
|
|
|
1207
1600
|
const timeoutMs = getUserConsultationTimeoutMs();
|
|
1208
1601
|
const pollIntervalMs = getAccessRequestPollIntervalMs();
|
|
@@ -1213,12 +1606,17 @@ export class RelayConnection {
|
|
|
1213
1606
|
true,
|
|
1214
1607
|
);
|
|
1215
1608
|
|
|
1216
|
-
updateCallSession(this.callSessionId, { status:
|
|
1609
|
+
updateCallSession(this.callSessionId, { status: "waiting_on_user" });
|
|
1217
1610
|
|
|
1218
1611
|
// Start the heartbeat timer for periodic progress updates.
|
|
1219
1612
|
// Delay the first heartbeat by the estimated TTS playback duration so
|
|
1220
1613
|
// the initial hold message finishes before any heartbeat fires.
|
|
1221
1614
|
this.heartbeatSequence = 0;
|
|
1615
|
+
// Set the wait start time now so scheduleNextHeartbeat() always has a
|
|
1616
|
+
// valid reference point — even if the TTS delay timer is cancelled early
|
|
1617
|
+
// (e.g. by handleWaitStatePrompt when the caller speaks during playback).
|
|
1618
|
+
// The callback below re-stamps it to exclude the TTS delay if it fires.
|
|
1619
|
+
this.accessRequestWaitStartedAt = Date.now();
|
|
1222
1620
|
this.accessRequestHeartbeatTimer = setTimeout(() => {
|
|
1223
1621
|
this.accessRequestWaitStartedAt = Date.now();
|
|
1224
1622
|
this.scheduleNextHeartbeat();
|
|
@@ -1236,9 +1634,9 @@ export class RelayConnection {
|
|
|
1236
1634
|
return;
|
|
1237
1635
|
}
|
|
1238
1636
|
|
|
1239
|
-
if (request.status ===
|
|
1637
|
+
if (request.status === "approved") {
|
|
1240
1638
|
this.handleAccessRequestApproved();
|
|
1241
|
-
} else if (request.status ===
|
|
1639
|
+
} else if (request.status === "denied") {
|
|
1242
1640
|
this.handleAccessRequestDenied();
|
|
1243
1641
|
}
|
|
1244
1642
|
// 'pending' continues polling; 'expired'/'cancelled' handled by timeout
|
|
@@ -1250,15 +1648,19 @@ export class RelayConnection {
|
|
|
1250
1648
|
|
|
1251
1649
|
log.info(
|
|
1252
1650
|
{ callSessionId: this.callSessionId, requestId: this.accessRequestId },
|
|
1253
|
-
|
|
1651
|
+
"Access request in-call wait timed out",
|
|
1254
1652
|
);
|
|
1255
1653
|
|
|
1256
1654
|
this.handleAccessRequestTimeout();
|
|
1257
1655
|
}, timeoutMs);
|
|
1258
1656
|
|
|
1259
1657
|
log.info(
|
|
1260
|
-
{
|
|
1261
|
-
|
|
1658
|
+
{
|
|
1659
|
+
callSessionId: this.callSessionId,
|
|
1660
|
+
requestId: this.accessRequestId,
|
|
1661
|
+
timeoutMs,
|
|
1662
|
+
},
|
|
1663
|
+
"Access request in-call wait started",
|
|
1262
1664
|
);
|
|
1263
1665
|
}
|
|
1264
1666
|
|
|
@@ -1287,80 +1689,35 @@ export class RelayConnection {
|
|
|
1287
1689
|
*/
|
|
1288
1690
|
private handleAccessRequestApproved(): void {
|
|
1289
1691
|
this.clearAccessRequestWait();
|
|
1290
|
-
this.connectionState = 'connected';
|
|
1291
1692
|
|
|
1292
1693
|
const assistantId = this.accessRequestAssistantId!;
|
|
1293
1694
|
const fromNumber = this.accessRequestFromNumber!;
|
|
1294
1695
|
const callerName = this.accessRequestCallerName;
|
|
1295
1696
|
|
|
1296
|
-
recordCallEvent(this.callSessionId,
|
|
1697
|
+
recordCallEvent(this.callSessionId, "inbound_acl_access_approved", {
|
|
1297
1698
|
from: fromNumber,
|
|
1298
1699
|
callerName,
|
|
1299
1700
|
requestId: this.accessRequestId,
|
|
1300
1701
|
});
|
|
1301
1702
|
|
|
1302
|
-
// Activate the caller as a trusted contact via the existing upsert path
|
|
1303
|
-
try {
|
|
1304
|
-
upsertMember({
|
|
1305
|
-
assistantId,
|
|
1306
|
-
sourceChannel: 'voice',
|
|
1307
|
-
externalUserId: fromNumber,
|
|
1308
|
-
externalChatId: fromNumber,
|
|
1309
|
-
displayName: callerName ?? undefined,
|
|
1310
|
-
status: 'active',
|
|
1311
|
-
policy: 'allow',
|
|
1312
|
-
});
|
|
1313
|
-
} catch (err) {
|
|
1314
|
-
log.error({ err, callSessionId: this.callSessionId }, 'Failed to activate voice caller as trusted contact');
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
// Re-resolve actor trust now that the member is active
|
|
1318
|
-
const updatedTrust = resolveActorTrust({
|
|
1319
|
-
assistantId,
|
|
1320
|
-
sourceChannel: 'voice',
|
|
1321
|
-
conversationExternalId: fromNumber,
|
|
1322
|
-
actorExternalId: fromNumber,
|
|
1323
|
-
});
|
|
1324
|
-
|
|
1325
|
-
if (this.controller) {
|
|
1326
|
-
this.controller.setGuardianContext(
|
|
1327
|
-
toGuardianRuntimeContextFromTrust(updatedTrust, fromNumber),
|
|
1328
|
-
);
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
updateCallSession(this.callSessionId, { status: 'in_progress' });
|
|
1332
|
-
|
|
1333
1703
|
log.info(
|
|
1334
1704
|
{ callSessionId: this.callSessionId, from: fromNumber },
|
|
1335
|
-
|
|
1705
|
+
"Access request approved — caller activated and continuing call",
|
|
1336
1706
|
);
|
|
1337
1707
|
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
const handoffText = `Great! ${guardianLabel} said I can speak with you. How can I help?`;
|
|
1343
|
-
this.sendTextToken(handoffText, true);
|
|
1344
|
-
|
|
1345
|
-
// Record the deterministic handoff as an assistant_spoke event and
|
|
1346
|
-
// fire the transcript notifier so it appears in conversation history
|
|
1347
|
-
// and real-time transcript subscribers — matching the parity of text
|
|
1348
|
-
// spoken through the normal runTurn() pipeline.
|
|
1349
|
-
recordCallEvent(this.callSessionId, 'assistant_spoke', { text: handoffText });
|
|
1350
|
-
const session = getCallSession(this.callSessionId);
|
|
1351
|
-
if (session) {
|
|
1352
|
-
fireCallTranscriptNotifier(session.conversationId, this.callSessionId, 'assistant', handoffText);
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
recordCallEvent(this.callSessionId, 'inbound_acl_post_approval_handoff_spoken', {
|
|
1356
|
-
from: fromNumber,
|
|
1708
|
+
this.continueCallAfterTrustedContactActivation({
|
|
1709
|
+
assistantId,
|
|
1710
|
+
fromNumber,
|
|
1711
|
+
callerName: callerName ?? undefined,
|
|
1357
1712
|
});
|
|
1358
1713
|
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1714
|
+
recordCallEvent(
|
|
1715
|
+
this.callSessionId,
|
|
1716
|
+
"inbound_acl_post_approval_handoff_spoken",
|
|
1717
|
+
{
|
|
1718
|
+
from: fromNumber,
|
|
1719
|
+
},
|
|
1720
|
+
);
|
|
1364
1721
|
}
|
|
1365
1722
|
|
|
1366
1723
|
/**
|
|
@@ -1371,7 +1728,7 @@ export class RelayConnection {
|
|
|
1371
1728
|
|
|
1372
1729
|
const guardianLabel = this.resolveGuardianLabel();
|
|
1373
1730
|
|
|
1374
|
-
recordCallEvent(this.callSessionId,
|
|
1731
|
+
recordCallEvent(this.callSessionId, "inbound_acl_access_denied", {
|
|
1375
1732
|
from: this.accessRequestFromNumber,
|
|
1376
1733
|
requestId: this.accessRequestId,
|
|
1377
1734
|
});
|
|
@@ -1381,21 +1738,21 @@ export class RelayConnection {
|
|
|
1381
1738
|
true,
|
|
1382
1739
|
);
|
|
1383
1740
|
|
|
1384
|
-
this.connectionState =
|
|
1741
|
+
this.connectionState = "disconnecting";
|
|
1385
1742
|
|
|
1386
1743
|
updateCallSession(this.callSessionId, {
|
|
1387
|
-
status:
|
|
1744
|
+
status: "failed",
|
|
1388
1745
|
endedAt: Date.now(),
|
|
1389
|
-
lastError:
|
|
1746
|
+
lastError: "Inbound voice ACL: guardian denied access request",
|
|
1390
1747
|
});
|
|
1391
1748
|
|
|
1392
1749
|
log.info(
|
|
1393
1750
|
{ callSessionId: this.callSessionId },
|
|
1394
|
-
|
|
1751
|
+
"Access request denied — ending call",
|
|
1395
1752
|
);
|
|
1396
1753
|
|
|
1397
1754
|
setTimeout(() => {
|
|
1398
|
-
this.endSession(
|
|
1755
|
+
this.endSession("Access request denied");
|
|
1399
1756
|
}, getTtsPlaybackDelayMs());
|
|
1400
1757
|
}
|
|
1401
1758
|
|
|
@@ -1404,13 +1761,13 @@ export class RelayConnection {
|
|
|
1404
1761
|
*/
|
|
1405
1762
|
private handleAccessRequestTimeout(): void {
|
|
1406
1763
|
// Emit callback handoff notification before clearing wait state
|
|
1407
|
-
this.emitAccessRequestCallbackHandoff(
|
|
1764
|
+
this.emitAccessRequestCallbackHandoff("timeout");
|
|
1408
1765
|
|
|
1409
1766
|
this.clearAccessRequestWait();
|
|
1410
1767
|
|
|
1411
1768
|
const guardianLabel = this.resolveGuardianLabel();
|
|
1412
1769
|
|
|
1413
|
-
recordCallEvent(this.callSessionId,
|
|
1770
|
+
recordCallEvent(this.callSessionId, "inbound_acl_access_timeout", {
|
|
1414
1771
|
from: this.accessRequestFromNumber,
|
|
1415
1772
|
requestId: this.accessRequestId,
|
|
1416
1773
|
callbackOptIn: this.callbackOptIn,
|
|
@@ -1418,27 +1775,27 @@ export class RelayConnection {
|
|
|
1418
1775
|
|
|
1419
1776
|
const callbackNote = this.callbackOptIn
|
|
1420
1777
|
? ` I've noted that you'd like a callback — I'll pass that along to ${guardianLabel}.`
|
|
1421
|
-
:
|
|
1778
|
+
: "";
|
|
1422
1779
|
this.sendTextToken(
|
|
1423
1780
|
`Sorry, I can't get ahold of ${guardianLabel} right now. I'll let them know you called.${callbackNote}`,
|
|
1424
1781
|
true,
|
|
1425
1782
|
);
|
|
1426
1783
|
|
|
1427
|
-
this.connectionState =
|
|
1784
|
+
this.connectionState = "disconnecting";
|
|
1428
1785
|
|
|
1429
1786
|
updateCallSession(this.callSessionId, {
|
|
1430
|
-
status:
|
|
1787
|
+
status: "failed",
|
|
1431
1788
|
endedAt: Date.now(),
|
|
1432
|
-
lastError:
|
|
1789
|
+
lastError: "Inbound voice ACL: guardian approval wait timed out",
|
|
1433
1790
|
});
|
|
1434
1791
|
|
|
1435
1792
|
log.info(
|
|
1436
1793
|
{ callSessionId: this.callSessionId },
|
|
1437
|
-
|
|
1794
|
+
"Access request timed out — ending call",
|
|
1438
1795
|
);
|
|
1439
1796
|
|
|
1440
1797
|
setTimeout(() => {
|
|
1441
|
-
this.endSession(
|
|
1798
|
+
this.endSession("Access request timed out");
|
|
1442
1799
|
}, getTtsPlaybackDelayMs());
|
|
1443
1800
|
}
|
|
1444
1801
|
|
|
@@ -1450,14 +1807,17 @@ export class RelayConnection {
|
|
|
1450
1807
|
* Idempotent: uses callbackHandoffNotified guard + deterministic dedupeKey
|
|
1451
1808
|
* to ensure at most one notification per call/request.
|
|
1452
1809
|
*/
|
|
1453
|
-
private emitAccessRequestCallbackHandoff(
|
|
1810
|
+
private emitAccessRequestCallbackHandoff(
|
|
1811
|
+
reason: "timeout" | "transport_closed",
|
|
1812
|
+
): void {
|
|
1454
1813
|
if (!this.callbackOptIn) return;
|
|
1455
1814
|
if (!this.accessRequestId) return;
|
|
1456
1815
|
if (this.callbackHandoffNotified) return;
|
|
1457
1816
|
|
|
1458
1817
|
this.callbackHandoffNotified = true;
|
|
1459
1818
|
|
|
1460
|
-
const assistantId =
|
|
1819
|
+
const assistantId =
|
|
1820
|
+
this.accessRequestAssistantId ?? DAEMON_INTERNAL_ASSISTANT_ID;
|
|
1461
1821
|
const fromNumber = this.accessRequestFromNumber ?? null;
|
|
1462
1822
|
|
|
1463
1823
|
// Resolve canonical request for requestCode and conversationId
|
|
@@ -1469,32 +1829,39 @@ export class RelayConnection {
|
|
|
1469
1829
|
let requesterMemberId: string | null = null;
|
|
1470
1830
|
if (fromNumber) {
|
|
1471
1831
|
try {
|
|
1472
|
-
const
|
|
1473
|
-
|
|
1474
|
-
sourceChannel: 'voice',
|
|
1832
|
+
const contactResult = findContactChannel({
|
|
1833
|
+
channelType: "voice",
|
|
1475
1834
|
externalUserId: fromNumber,
|
|
1476
1835
|
externalChatId: fromNumber,
|
|
1477
1836
|
});
|
|
1478
|
-
if (
|
|
1479
|
-
|
|
1837
|
+
if (
|
|
1838
|
+
contactResult &&
|
|
1839
|
+
contactResult.channel.status === "active" &&
|
|
1840
|
+
contactResult.channel.policy === "allow"
|
|
1841
|
+
) {
|
|
1842
|
+
requesterMemberId = contactResult.channel.id;
|
|
1480
1843
|
}
|
|
1481
1844
|
} catch (err) {
|
|
1482
|
-
log.warn(
|
|
1845
|
+
log.warn(
|
|
1846
|
+
{ err, callSessionId: this.callSessionId },
|
|
1847
|
+
"Failed to resolve member for callback handoff",
|
|
1848
|
+
);
|
|
1483
1849
|
}
|
|
1484
1850
|
}
|
|
1485
1851
|
|
|
1486
1852
|
const dedupeKey = `access-request-callback-handoff:${this.accessRequestId}`;
|
|
1487
|
-
const sourceSessionId =
|
|
1488
|
-
??
|
|
1853
|
+
const sourceSessionId =
|
|
1854
|
+
canonicalRequest?.conversationId ??
|
|
1855
|
+
`access-req-callback-${this.accessRequestId}`;
|
|
1489
1856
|
|
|
1490
1857
|
void emitNotificationSignal({
|
|
1491
|
-
sourceEventName:
|
|
1492
|
-
sourceChannel:
|
|
1858
|
+
sourceEventName: "ingress.access_request.callback_handoff",
|
|
1859
|
+
sourceChannel: "voice",
|
|
1493
1860
|
sourceSessionId,
|
|
1494
1861
|
assistantId,
|
|
1495
1862
|
attentionHints: {
|
|
1496
1863
|
requiresAction: false,
|
|
1497
|
-
urgency:
|
|
1864
|
+
urgency: "medium",
|
|
1498
1865
|
isAsyncBackground: true,
|
|
1499
1866
|
visibleInSourceNow: false,
|
|
1500
1867
|
},
|
|
@@ -1502,7 +1869,7 @@ export class RelayConnection {
|
|
|
1502
1869
|
requestId: this.accessRequestId,
|
|
1503
1870
|
requestCode: canonicalRequest?.requestCode ?? null,
|
|
1504
1871
|
callSessionId: this.callSessionId,
|
|
1505
|
-
sourceChannel:
|
|
1872
|
+
sourceChannel: "voice",
|
|
1506
1873
|
reason,
|
|
1507
1874
|
callbackOptIn: true,
|
|
1508
1875
|
callerPhoneNumber: fromNumber,
|
|
@@ -1510,30 +1877,40 @@ export class RelayConnection {
|
|
|
1510
1877
|
requesterExternalUserId: fromNumber,
|
|
1511
1878
|
requesterChatId: fromNumber,
|
|
1512
1879
|
requesterMemberId,
|
|
1513
|
-
requesterMemberSourceChannel: requesterMemberId ?
|
|
1880
|
+
requesterMemberSourceChannel: requesterMemberId ? "voice" : null,
|
|
1514
1881
|
},
|
|
1515
1882
|
dedupeKey,
|
|
1516
|
-
})
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1883
|
+
})
|
|
1884
|
+
.then(() => {
|
|
1885
|
+
recordCallEvent(this.callSessionId, "callback_handoff_notified", {
|
|
1886
|
+
requestId: this.accessRequestId,
|
|
1887
|
+
reason,
|
|
1888
|
+
requesterMemberId,
|
|
1889
|
+
});
|
|
1890
|
+
log.info(
|
|
1891
|
+
{
|
|
1892
|
+
callSessionId: this.callSessionId,
|
|
1893
|
+
requestId: this.accessRequestId,
|
|
1894
|
+
reason,
|
|
1895
|
+
},
|
|
1896
|
+
"Callback handoff notification emitted",
|
|
1897
|
+
);
|
|
1898
|
+
})
|
|
1899
|
+
.catch((err) => {
|
|
1900
|
+
recordCallEvent(this.callSessionId, "callback_handoff_failed", {
|
|
1901
|
+
requestId: this.accessRequestId,
|
|
1902
|
+
reason,
|
|
1903
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1904
|
+
});
|
|
1905
|
+
log.error(
|
|
1906
|
+
{
|
|
1907
|
+
err,
|
|
1908
|
+
callSessionId: this.callSessionId,
|
|
1909
|
+
requestId: this.accessRequestId,
|
|
1910
|
+
},
|
|
1911
|
+
"Failed to emit callback handoff notification",
|
|
1912
|
+
);
|
|
1531
1913
|
});
|
|
1532
|
-
log.error(
|
|
1533
|
-
{ err, callSessionId: this.callSessionId, requestId: this.accessRequestId },
|
|
1534
|
-
'Failed to emit callback handoff notification',
|
|
1535
|
-
);
|
|
1536
|
-
});
|
|
1537
1914
|
}
|
|
1538
1915
|
|
|
1539
1916
|
/**
|
|
@@ -1546,7 +1923,7 @@ export class RelayConnection {
|
|
|
1546
1923
|
this.nameCaptureTimeoutTimer = null;
|
|
1547
1924
|
}
|
|
1548
1925
|
|
|
1549
|
-
recordCallEvent(this.callSessionId,
|
|
1926
|
+
recordCallEvent(this.callSessionId, "inbound_acl_name_capture_timeout", {
|
|
1550
1927
|
from: this.accessRequestFromNumber,
|
|
1551
1928
|
});
|
|
1552
1929
|
|
|
@@ -1555,27 +1932,27 @@ export class RelayConnection {
|
|
|
1555
1932
|
true,
|
|
1556
1933
|
);
|
|
1557
1934
|
|
|
1558
|
-
this.connectionState =
|
|
1935
|
+
this.connectionState = "disconnecting";
|
|
1559
1936
|
|
|
1560
1937
|
updateCallSession(this.callSessionId, {
|
|
1561
|
-
status:
|
|
1938
|
+
status: "failed",
|
|
1562
1939
|
endedAt: Date.now(),
|
|
1563
|
-
lastError:
|
|
1940
|
+
lastError: "Inbound voice ACL: name capture timed out",
|
|
1564
1941
|
});
|
|
1565
1942
|
|
|
1566
1943
|
log.info(
|
|
1567
1944
|
{ callSessionId: this.callSessionId },
|
|
1568
|
-
|
|
1945
|
+
"Name capture timed out — ending call",
|
|
1569
1946
|
);
|
|
1570
1947
|
|
|
1571
1948
|
setTimeout(() => {
|
|
1572
|
-
this.endSession(
|
|
1949
|
+
this.endSession("Name capture timed out");
|
|
1573
1950
|
}, getTtsPlaybackDelayMs());
|
|
1574
1951
|
}
|
|
1575
1952
|
|
|
1576
1953
|
/**
|
|
1577
1954
|
* Validate an entered invite code against active voice invites for the
|
|
1578
|
-
* caller. On success, create/activate the
|
|
1955
|
+
* caller. On success, create/activate the contact and transition
|
|
1579
1956
|
* to the normal call flow. On failure, allow retries up to max attempts.
|
|
1580
1957
|
*/
|
|
1581
1958
|
private attemptInviteCodeRedemption(enteredCode: string): void {
|
|
@@ -1586,74 +1963,85 @@ export class RelayConnection {
|
|
|
1586
1963
|
const result = redeemVoiceInviteCode({
|
|
1587
1964
|
assistantId: this.inviteRedemptionAssistantId,
|
|
1588
1965
|
callerExternalUserId: this.inviteRedemptionFromNumber,
|
|
1589
|
-
sourceChannel:
|
|
1966
|
+
sourceChannel: "voice",
|
|
1590
1967
|
code: enteredCode,
|
|
1591
1968
|
});
|
|
1592
1969
|
|
|
1593
1970
|
if (result.ok) {
|
|
1594
|
-
this.connectionState = 'connected';
|
|
1595
1971
|
this.inviteRedemptionActive = false;
|
|
1596
1972
|
this.verificationAttempts = 0;
|
|
1597
|
-
this.dtmfBuffer =
|
|
1973
|
+
this.dtmfBuffer = "";
|
|
1598
1974
|
|
|
1599
|
-
recordCallEvent(this.callSessionId,
|
|
1975
|
+
recordCallEvent(this.callSessionId, "invite_redemption_succeeded", {
|
|
1600
1976
|
memberId: result.memberId,
|
|
1601
|
-
...(result.type ===
|
|
1977
|
+
...(result.type === "redeemed" ? { inviteId: result.inviteId } : {}),
|
|
1602
1978
|
});
|
|
1603
1979
|
log.info(
|
|
1604
|
-
{
|
|
1605
|
-
|
|
1980
|
+
{
|
|
1981
|
+
callSessionId: this.callSessionId,
|
|
1982
|
+
memberId: result.memberId,
|
|
1983
|
+
type: result.type,
|
|
1984
|
+
},
|
|
1985
|
+
"Voice invite redemption succeeded",
|
|
1606
1986
|
);
|
|
1607
1987
|
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
});
|
|
1615
|
-
this.controller.setGuardianContext(
|
|
1616
|
-
toGuardianRuntimeContextFromTrust(redeemedActorTrust, this.inviteRedemptionFromNumber),
|
|
1617
|
-
);
|
|
1618
|
-
this.startNormalCallFlow(this.controller, true);
|
|
1619
|
-
}
|
|
1988
|
+
this.continueCallAfterTrustedContactActivation({
|
|
1989
|
+
assistantId: this.inviteRedemptionAssistantId,
|
|
1990
|
+
fromNumber: this.inviteRedemptionFromNumber,
|
|
1991
|
+
callerName: this.inviteRedemptionFriendName ?? undefined,
|
|
1992
|
+
skipMemberActivation: true,
|
|
1993
|
+
});
|
|
1620
1994
|
} else {
|
|
1621
1995
|
// On any invalid/expired code, emit exact deterministic failure copy and end call immediately.
|
|
1622
1996
|
this.inviteRedemptionActive = false;
|
|
1623
1997
|
|
|
1624
|
-
recordCallEvent(this.callSessionId,
|
|
1998
|
+
recordCallEvent(this.callSessionId, "invite_redemption_failed", {
|
|
1625
1999
|
attempts: 1,
|
|
1626
2000
|
});
|
|
1627
2001
|
log.warn(
|
|
1628
2002
|
{ callSessionId: this.callSessionId },
|
|
1629
|
-
|
|
2003
|
+
"Voice invite redemption failed — invalid or expired code",
|
|
1630
2004
|
);
|
|
1631
2005
|
|
|
1632
|
-
const displayGuardian =
|
|
2006
|
+
const displayGuardian =
|
|
2007
|
+
this.inviteRedemptionGuardianName ?? "your contact";
|
|
1633
2008
|
this.sendTextToken(
|
|
1634
2009
|
`Sorry, the code you provided is incorrect or has since expired. Please ask ${displayGuardian} for a new code. Goodbye.`,
|
|
1635
2010
|
true,
|
|
1636
2011
|
);
|
|
1637
2012
|
|
|
1638
|
-
this.connectionState =
|
|
2013
|
+
this.connectionState = "disconnecting";
|
|
1639
2014
|
|
|
1640
2015
|
updateCallSession(this.callSessionId, {
|
|
1641
|
-
status:
|
|
2016
|
+
status: "failed",
|
|
1642
2017
|
endedAt: Date.now(),
|
|
1643
|
-
lastError:
|
|
2018
|
+
lastError: "Voice invite redemption failed — invalid or expired code",
|
|
1644
2019
|
});
|
|
1645
2020
|
|
|
1646
2021
|
const failSession = getCallSession(this.callSessionId);
|
|
1647
2022
|
if (failSession) {
|
|
1648
2023
|
expirePendingQuestions(this.callSessionId);
|
|
1649
|
-
persistCallCompletionMessage(
|
|
1650
|
-
|
|
2024
|
+
persistCallCompletionMessage(
|
|
2025
|
+
failSession.conversationId,
|
|
2026
|
+
this.callSessionId,
|
|
2027
|
+
).catch((err) => {
|
|
2028
|
+
log.error(
|
|
2029
|
+
{
|
|
2030
|
+
err,
|
|
2031
|
+
conversationId: failSession.conversationId,
|
|
2032
|
+
callSessionId: this.callSessionId,
|
|
2033
|
+
},
|
|
2034
|
+
"Failed to persist call completion message",
|
|
2035
|
+
);
|
|
1651
2036
|
});
|
|
1652
|
-
fireCallCompletionNotifier(
|
|
2037
|
+
fireCallCompletionNotifier(
|
|
2038
|
+
failSession.conversationId,
|
|
2039
|
+
this.callSessionId,
|
|
2040
|
+
);
|
|
1653
2041
|
}
|
|
1654
2042
|
|
|
1655
2043
|
setTimeout(() => {
|
|
1656
|
-
this.endSession(
|
|
2044
|
+
this.endSession("Invite redemption failed");
|
|
1657
2045
|
}, getTtsPlaybackDelayMs());
|
|
1658
2046
|
}
|
|
1659
2047
|
}
|
|
@@ -1666,29 +2054,58 @@ export class RelayConnection {
|
|
|
1666
2054
|
* to @username, then the user's preferred name from USER.md.
|
|
1667
2055
|
*/
|
|
1668
2056
|
private resolveGuardianLabel(): string {
|
|
1669
|
-
const assistantId =
|
|
2057
|
+
const assistantId =
|
|
2058
|
+
this.accessRequestAssistantId ?? DAEMON_INTERNAL_ASSISTANT_ID;
|
|
1670
2059
|
|
|
1671
2060
|
// Try the voice-channel binding first, then fall back to any active
|
|
1672
2061
|
// binding for the assistant (mirrors the cross-channel fallback pattern
|
|
1673
2062
|
// in access-request-helper.ts).
|
|
1674
2063
|
let metadataJson: string | null = null;
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
2064
|
+
// Contacts-first: prefer the voice-bound guardian, then fall back to
|
|
2065
|
+
// any guardian channel (mirrors the voice-first pattern in the legacy path).
|
|
2066
|
+
const voiceGuardian = findGuardianForChannel("voice", assistantId);
|
|
2067
|
+
const guardianChannels = voiceGuardian
|
|
2068
|
+
? null
|
|
2069
|
+
: listGuardianChannels(assistantId);
|
|
2070
|
+
const guardianContact = voiceGuardian?.contact ?? guardianChannels?.contact;
|
|
2071
|
+
if (guardianContact) {
|
|
2072
|
+
const meta: Record<string, string> = {};
|
|
2073
|
+
if (guardianContact.displayName) {
|
|
2074
|
+
meta.displayName = guardianContact.displayName;
|
|
2075
|
+
}
|
|
2076
|
+
// Preserve the username fallback: use the voice channel's externalUserId
|
|
2077
|
+
// so downstream parsing can fall back to @username when displayName is a
|
|
2078
|
+
// raw external ID (e.g., phone number from contact-sync).
|
|
2079
|
+
const voiceChannel =
|
|
2080
|
+
voiceGuardian?.channel ??
|
|
2081
|
+
guardianChannels?.channels.find((ch) => ch.type === "voice");
|
|
2082
|
+
if (voiceChannel?.externalUserId) {
|
|
2083
|
+
meta.username = voiceChannel.externalUserId;
|
|
2084
|
+
}
|
|
2085
|
+
if (Object.keys(meta).length > 0) {
|
|
2086
|
+
metadataJson = JSON.stringify(meta);
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
if (!metadataJson) {
|
|
2090
|
+
const voiceBinding = getGuardianBinding(assistantId, "voice");
|
|
2091
|
+
if (voiceBinding?.metadataJson) {
|
|
2092
|
+
metadataJson = voiceBinding.metadataJson;
|
|
1682
2093
|
}
|
|
1683
2094
|
}
|
|
1684
2095
|
|
|
1685
2096
|
if (metadataJson) {
|
|
1686
2097
|
try {
|
|
1687
2098
|
const parsed = JSON.parse(metadataJson) as Record<string, unknown>;
|
|
1688
|
-
if (
|
|
2099
|
+
if (
|
|
2100
|
+
typeof parsed.displayName === "string" &&
|
|
2101
|
+
parsed.displayName.trim().length > 0
|
|
2102
|
+
) {
|
|
1689
2103
|
return parsed.displayName.trim();
|
|
1690
2104
|
}
|
|
1691
|
-
if (
|
|
2105
|
+
if (
|
|
2106
|
+
typeof parsed.username === "string" &&
|
|
2107
|
+
parsed.username.trim().length > 0
|
|
2108
|
+
) {
|
|
1692
2109
|
return `@${parsed.username.trim()}`;
|
|
1693
2110
|
}
|
|
1694
2111
|
} catch {
|
|
@@ -1738,10 +2155,18 @@ export class RelayConnection {
|
|
|
1738
2155
|
|
|
1739
2156
|
const elapsed = Date.now() - this.accessRequestWaitStartedAt;
|
|
1740
2157
|
const initialWindow = getGuardianWaitUpdateInitialWindowMs();
|
|
1741
|
-
const intervalMs =
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
2158
|
+
const intervalMs =
|
|
2159
|
+
elapsed < initialWindow
|
|
2160
|
+
? getGuardianWaitUpdateInitialIntervalMs()
|
|
2161
|
+
: getGuardianWaitUpdateSteadyMinIntervalMs() +
|
|
2162
|
+
Math.floor(
|
|
2163
|
+
Math.random() *
|
|
2164
|
+
Math.max(
|
|
2165
|
+
0,
|
|
2166
|
+
getGuardianWaitUpdateSteadyMaxIntervalMs() -
|
|
2167
|
+
getGuardianWaitUpdateSteadyMinIntervalMs(),
|
|
2168
|
+
),
|
|
2169
|
+
);
|
|
1745
2170
|
|
|
1746
2171
|
this.accessRequestHeartbeatTimer = setTimeout(() => {
|
|
1747
2172
|
if (!this.accessRequestWaitActive) return;
|
|
@@ -1749,14 +2174,21 @@ export class RelayConnection {
|
|
|
1749
2174
|
const message = this.getHeartbeatMessage();
|
|
1750
2175
|
this.sendTextToken(message, true);
|
|
1751
2176
|
|
|
1752
|
-
recordCallEvent(
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
2177
|
+
recordCallEvent(
|
|
2178
|
+
this.callSessionId,
|
|
2179
|
+
"voice_guardian_wait_heartbeat_sent",
|
|
2180
|
+
{
|
|
2181
|
+
sequence: this.heartbeatSequence - 1,
|
|
2182
|
+
message,
|
|
2183
|
+
},
|
|
2184
|
+
);
|
|
1756
2185
|
|
|
1757
2186
|
log.debug(
|
|
1758
|
-
{
|
|
1759
|
-
|
|
2187
|
+
{
|
|
2188
|
+
callSessionId: this.callSessionId,
|
|
2189
|
+
sequence: this.heartbeatSequence - 1,
|
|
2190
|
+
},
|
|
2191
|
+
"Guardian wait heartbeat sent",
|
|
1760
2192
|
);
|
|
1761
2193
|
|
|
1762
2194
|
// Schedule the next heartbeat
|
|
@@ -1773,52 +2205,72 @@ export class RelayConnection {
|
|
|
1773
2205
|
* - 'callback_decline': explicitly declining a callback
|
|
1774
2206
|
* - 'neutral': anything else
|
|
1775
2207
|
*/
|
|
1776
|
-
private classifyWaitUtterance(
|
|
2208
|
+
private classifyWaitUtterance(
|
|
2209
|
+
text: string,
|
|
2210
|
+
):
|
|
2211
|
+
| "empty"
|
|
2212
|
+
| "patience_check"
|
|
2213
|
+
| "impatient"
|
|
2214
|
+
| "callback_opt_in"
|
|
2215
|
+
| "callback_decline"
|
|
2216
|
+
| "neutral" {
|
|
1777
2217
|
const lower = text.toLowerCase().trim();
|
|
1778
|
-
if (lower.length === 0) return
|
|
2218
|
+
if (lower.length === 0) return "empty";
|
|
1779
2219
|
|
|
1780
2220
|
// Callback opt-in patterns (check before impatience to catch "yes call me back")
|
|
1781
2221
|
if (this.callbackOfferMade) {
|
|
1782
|
-
if (
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
||
|
|
1786
|
-
|
|
1787
|
-
|
|
2222
|
+
if (
|
|
2223
|
+
/\b(yes|yeah|yep|sure|okay|ok|please)\b.*\b(call\s*(me\s*)?back|callback)\b/.test(
|
|
2224
|
+
lower,
|
|
2225
|
+
) ||
|
|
2226
|
+
/\b(call\s*(me\s*)?back|callback)\b.*\b(yes|yeah|please|sure)\b/.test(
|
|
2227
|
+
lower,
|
|
2228
|
+
) ||
|
|
2229
|
+
/^(yes|yeah|yep|sure|okay|ok|please)\s*[.,!]?\s*$/.test(lower) ||
|
|
2230
|
+
/\bcall\s*(me\s*)?back\b/.test(lower) ||
|
|
2231
|
+
/\bplease\s+do\b/.test(lower)
|
|
2232
|
+
) {
|
|
2233
|
+
return "callback_opt_in";
|
|
1788
2234
|
}
|
|
1789
|
-
if (
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
2235
|
+
if (
|
|
2236
|
+
/\b(no|nah|nope)\b/.test(lower) ||
|
|
2237
|
+
/\bi('?ll| will)\s+hold\b/.test(lower) ||
|
|
2238
|
+
/\bi('?ll| will)\s+wait\b/.test(lower)
|
|
2239
|
+
) {
|
|
2240
|
+
return "callback_decline";
|
|
1793
2241
|
}
|
|
1794
2242
|
}
|
|
1795
2243
|
|
|
1796
2244
|
// Impatience patterns
|
|
1797
|
-
if (
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
2245
|
+
if (
|
|
2246
|
+
/\bhurry\s*(up)?\b/.test(lower) ||
|
|
2247
|
+
/\btaking\s+(too\s+|so\s+)?long\b/.test(lower) ||
|
|
2248
|
+
/\bforget\s+it\b/.test(lower) ||
|
|
2249
|
+
/\bnever\s*mind\b/.test(lower) ||
|
|
2250
|
+
/\bdon'?t\s+have\s+time\b/.test(lower) ||
|
|
2251
|
+
/\bhow\s+much\s+longer\b/.test(lower) ||
|
|
2252
|
+
/\bi('?m| am)\s+(getting\s+)?impatient\b/.test(lower) ||
|
|
2253
|
+
/\bthis\s+is\s+(ridiculous|absurd|crazy)\b/.test(lower) ||
|
|
2254
|
+
/\bcome\s+on\b/.test(lower) ||
|
|
2255
|
+
/\bi\s+(gotta|have\s+to|need\s+to)\s+go\b/.test(lower)
|
|
2256
|
+
) {
|
|
2257
|
+
return "impatient";
|
|
1808
2258
|
}
|
|
1809
2259
|
|
|
1810
2260
|
// Patience check / status inquiry patterns
|
|
1811
|
-
if (
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
2261
|
+
if (
|
|
2262
|
+
/\bhello\??\s*$/.test(lower) ||
|
|
2263
|
+
/\bstill\s+there\b/.test(lower) ||
|
|
2264
|
+
/\bany\s+(update|news)\b/.test(lower) ||
|
|
2265
|
+
/\bwhat('?s| is)\s+(happening|going\s+on)\b/.test(lower) ||
|
|
2266
|
+
/\bare\s+you\s+still\b/.test(lower) ||
|
|
2267
|
+
/\bhow\s+(long|much\s+longer)\b/.test(lower) ||
|
|
2268
|
+
/\banyone\s+there\b/.test(lower)
|
|
2269
|
+
) {
|
|
2270
|
+
return "patience_check";
|
|
1819
2271
|
}
|
|
1820
2272
|
|
|
1821
|
-
return
|
|
2273
|
+
return "neutral";
|
|
1822
2274
|
}
|
|
1823
2275
|
|
|
1824
2276
|
/**
|
|
@@ -1829,12 +2281,16 @@ export class RelayConnection {
|
|
|
1829
2281
|
const now = Date.now();
|
|
1830
2282
|
const classification = this.classifyWaitUtterance(text);
|
|
1831
2283
|
|
|
1832
|
-
recordCallEvent(
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
2284
|
+
recordCallEvent(
|
|
2285
|
+
this.callSessionId,
|
|
2286
|
+
"voice_guardian_wait_prompt_classified",
|
|
2287
|
+
{
|
|
2288
|
+
classification,
|
|
2289
|
+
transcript: text,
|
|
2290
|
+
},
|
|
2291
|
+
);
|
|
1836
2292
|
|
|
1837
|
-
if (classification ===
|
|
2293
|
+
if (classification === "empty") return;
|
|
1838
2294
|
|
|
1839
2295
|
const guardianLabel = this.resolveGuardianLabel();
|
|
1840
2296
|
|
|
@@ -1842,10 +2298,14 @@ export class RelayConnection {
|
|
|
1842
2298
|
// the caller is answering a direct question and dropping their response
|
|
1843
2299
|
// would silently discard their decision.
|
|
1844
2300
|
switch (classification) {
|
|
1845
|
-
case
|
|
2301
|
+
case "callback_opt_in": {
|
|
1846
2302
|
this.callbackOptIn = true;
|
|
1847
2303
|
this.lastInWaitReplyAt = now;
|
|
1848
|
-
recordCallEvent(
|
|
2304
|
+
recordCallEvent(
|
|
2305
|
+
this.callSessionId,
|
|
2306
|
+
"voice_guardian_wait_callback_opt_in_set",
|
|
2307
|
+
{},
|
|
2308
|
+
);
|
|
1849
2309
|
if (this.accessRequestHeartbeatTimer) {
|
|
1850
2310
|
clearTimeout(this.accessRequestHeartbeatTimer);
|
|
1851
2311
|
this.accessRequestHeartbeatTimer = null;
|
|
@@ -1857,10 +2317,14 @@ export class RelayConnection {
|
|
|
1857
2317
|
this.scheduleNextHeartbeat();
|
|
1858
2318
|
return;
|
|
1859
2319
|
}
|
|
1860
|
-
case
|
|
2320
|
+
case "callback_decline": {
|
|
1861
2321
|
this.callbackOptIn = false;
|
|
1862
2322
|
this.lastInWaitReplyAt = now;
|
|
1863
|
-
recordCallEvent(
|
|
2323
|
+
recordCallEvent(
|
|
2324
|
+
this.callSessionId,
|
|
2325
|
+
"voice_guardian_wait_callback_opt_in_declined",
|
|
2326
|
+
{},
|
|
2327
|
+
);
|
|
1864
2328
|
if (this.accessRequestHeartbeatTimer) {
|
|
1865
2329
|
clearTimeout(this.accessRequestHeartbeatTimer);
|
|
1866
2330
|
this.accessRequestHeartbeatTimer = null;
|
|
@@ -1877,21 +2341,31 @@ export class RelayConnection {
|
|
|
1877
2341
|
}
|
|
1878
2342
|
|
|
1879
2343
|
// Enforce cooldown on non-callback utterances to prevent spam
|
|
1880
|
-
if (
|
|
1881
|
-
|
|
2344
|
+
if (
|
|
2345
|
+
now - this.lastInWaitReplyAt <
|
|
2346
|
+
RelayConnection.IN_WAIT_REPLY_COOLDOWN_MS
|
|
2347
|
+
) {
|
|
2348
|
+
log.debug(
|
|
2349
|
+
{ callSessionId: this.callSessionId },
|
|
2350
|
+
"In-wait reply suppressed by cooldown",
|
|
2351
|
+
);
|
|
1882
2352
|
return;
|
|
1883
2353
|
}
|
|
1884
2354
|
this.lastInWaitReplyAt = now;
|
|
1885
2355
|
|
|
1886
2356
|
switch (classification) {
|
|
1887
|
-
case
|
|
2357
|
+
case "impatient": {
|
|
1888
2358
|
if (this.accessRequestHeartbeatTimer) {
|
|
1889
2359
|
clearTimeout(this.accessRequestHeartbeatTimer);
|
|
1890
2360
|
this.accessRequestHeartbeatTimer = null;
|
|
1891
2361
|
}
|
|
1892
2362
|
if (!this.callbackOfferMade) {
|
|
1893
2363
|
this.callbackOfferMade = true;
|
|
1894
|
-
recordCallEvent(
|
|
2364
|
+
recordCallEvent(
|
|
2365
|
+
this.callSessionId,
|
|
2366
|
+
"voice_guardian_wait_callback_offer_sent",
|
|
2367
|
+
{},
|
|
2368
|
+
);
|
|
1895
2369
|
this.sendTextToken(
|
|
1896
2370
|
`I understand this is taking a while. I can have ${guardianLabel} call you back once I hear from them. Would you like that, or would you prefer to keep holding?`,
|
|
1897
2371
|
true,
|
|
@@ -1906,7 +2380,7 @@ export class RelayConnection {
|
|
|
1906
2380
|
this.scheduleNextHeartbeat();
|
|
1907
2381
|
break;
|
|
1908
2382
|
}
|
|
1909
|
-
case
|
|
2383
|
+
case "patience_check": {
|
|
1910
2384
|
// Immediate reassurance — reset the heartbeat timer so we
|
|
1911
2385
|
// don't double up with a scheduled heartbeat
|
|
1912
2386
|
if (this.accessRequestHeartbeatTimer) {
|
|
@@ -1920,7 +2394,7 @@ export class RelayConnection {
|
|
|
1920
2394
|
this.scheduleNextHeartbeat();
|
|
1921
2395
|
break;
|
|
1922
2396
|
}
|
|
1923
|
-
case
|
|
2397
|
+
case "neutral":
|
|
1924
2398
|
default: {
|
|
1925
2399
|
if (this.accessRequestHeartbeatTimer) {
|
|
1926
2400
|
clearTimeout(this.accessRequestHeartbeatTimer);
|
|
@@ -1937,7 +2411,7 @@ export class RelayConnection {
|
|
|
1937
2411
|
}
|
|
1938
2412
|
|
|
1939
2413
|
private async handlePrompt(msg: RelayPromptMessage): Promise<void> {
|
|
1940
|
-
if (this.connectionState ===
|
|
2414
|
+
if (this.connectionState === "disconnecting") {
|
|
1941
2415
|
return;
|
|
1942
2416
|
}
|
|
1943
2417
|
|
|
@@ -1947,7 +2421,7 @@ export class RelayConnection {
|
|
|
1947
2421
|
}
|
|
1948
2422
|
|
|
1949
2423
|
// During name capture, the caller's response is their name.
|
|
1950
|
-
if (this.connectionState ===
|
|
2424
|
+
if (this.connectionState === "awaiting_name") {
|
|
1951
2425
|
const callerName = msg.voicePrompt.trim();
|
|
1952
2426
|
if (!callerName) {
|
|
1953
2427
|
// Whitespace-only or empty transcript (e.g. silence/noise) —
|
|
@@ -1957,7 +2431,7 @@ export class RelayConnection {
|
|
|
1957
2431
|
}
|
|
1958
2432
|
log.info(
|
|
1959
2433
|
{ callSessionId: this.callSessionId, callerName },
|
|
1960
|
-
|
|
2434
|
+
"Name captured from unknown inbound caller",
|
|
1961
2435
|
);
|
|
1962
2436
|
this.handleNameCaptureResponse(callerName);
|
|
1963
2437
|
return;
|
|
@@ -1965,18 +2439,27 @@ export class RelayConnection {
|
|
|
1965
2439
|
|
|
1966
2440
|
// During guardian decision wait, classify caller speech for
|
|
1967
2441
|
// reassurance, impatience detection, and callback offer.
|
|
1968
|
-
if (this.connectionState ===
|
|
2442
|
+
if (this.connectionState === "awaiting_guardian_decision") {
|
|
1969
2443
|
this.handleWaitStatePrompt(msg.voicePrompt);
|
|
1970
2444
|
return;
|
|
1971
2445
|
}
|
|
1972
2446
|
|
|
1973
2447
|
// During guardian verification (inbound or outbound), attempt to parse
|
|
1974
2448
|
// spoken digits from the transcript and validate them.
|
|
1975
|
-
if (
|
|
1976
|
-
|
|
2449
|
+
if (
|
|
2450
|
+
this.connectionState === "verification_pending" &&
|
|
2451
|
+
this.guardianVerificationActive
|
|
2452
|
+
) {
|
|
2453
|
+
const spokenDigits = RelayConnection.parseDigitsFromSpeech(
|
|
2454
|
+
msg.voicePrompt,
|
|
2455
|
+
);
|
|
1977
2456
|
log.info(
|
|
1978
|
-
{
|
|
1979
|
-
|
|
2457
|
+
{
|
|
2458
|
+
callSessionId: this.callSessionId,
|
|
2459
|
+
transcript: msg.voicePrompt,
|
|
2460
|
+
spokenDigits,
|
|
2461
|
+
},
|
|
2462
|
+
"Speech received during guardian voice verification",
|
|
1980
2463
|
);
|
|
1981
2464
|
if (spokenDigits.length >= this.verificationCodeLength) {
|
|
1982
2465
|
const enteredCode = spokenDigits.slice(0, this.verificationCodeLength);
|
|
@@ -1992,14 +2475,26 @@ export class RelayConnection {
|
|
|
1992
2475
|
|
|
1993
2476
|
// During invite redemption, attempt to parse spoken digits from the
|
|
1994
2477
|
// transcript and validate against the caller's active voice invite.
|
|
1995
|
-
if (
|
|
1996
|
-
|
|
2478
|
+
if (
|
|
2479
|
+
this.connectionState === "verification_pending" &&
|
|
2480
|
+
this.inviteRedemptionActive
|
|
2481
|
+
) {
|
|
2482
|
+
const spokenDigits = RelayConnection.parseDigitsFromSpeech(
|
|
2483
|
+
msg.voicePrompt,
|
|
2484
|
+
);
|
|
1997
2485
|
log.info(
|
|
1998
|
-
{
|
|
1999
|
-
|
|
2486
|
+
{
|
|
2487
|
+
callSessionId: this.callSessionId,
|
|
2488
|
+
transcript: msg.voicePrompt,
|
|
2489
|
+
spokenDigits,
|
|
2490
|
+
},
|
|
2491
|
+
"Speech received during invite redemption",
|
|
2000
2492
|
);
|
|
2001
2493
|
if (spokenDigits.length >= this.inviteRedemptionCodeLength) {
|
|
2002
|
-
const enteredCode = spokenDigits.slice(
|
|
2494
|
+
const enteredCode = spokenDigits.slice(
|
|
2495
|
+
0,
|
|
2496
|
+
this.inviteRedemptionCodeLength,
|
|
2497
|
+
);
|
|
2003
2498
|
this.attemptInviteCodeRedemption(enteredCode);
|
|
2004
2499
|
} else if (spokenDigits.length > 0) {
|
|
2005
2500
|
this.sendTextToken(
|
|
@@ -2012,31 +2507,39 @@ export class RelayConnection {
|
|
|
2012
2507
|
|
|
2013
2508
|
// During outbound callee verification, ignore voice prompts — the callee
|
|
2014
2509
|
// should be entering DTMF digits, not speaking.
|
|
2015
|
-
if (this.connectionState ===
|
|
2016
|
-
log.debug(
|
|
2510
|
+
if (this.connectionState === "verification_pending") {
|
|
2511
|
+
log.debug(
|
|
2512
|
+
{ callSessionId: this.callSessionId },
|
|
2513
|
+
"Ignoring voice prompt during callee verification",
|
|
2514
|
+
);
|
|
2017
2515
|
return;
|
|
2018
2516
|
}
|
|
2019
2517
|
|
|
2020
2518
|
log.info(
|
|
2021
|
-
{
|
|
2022
|
-
|
|
2519
|
+
{
|
|
2520
|
+
callSessionId: this.callSessionId,
|
|
2521
|
+
transcript: msg.voicePrompt,
|
|
2522
|
+
lang: msg.lang,
|
|
2523
|
+
},
|
|
2524
|
+
"Caller transcript received (final)",
|
|
2023
2525
|
);
|
|
2024
2526
|
|
|
2025
2527
|
// Spread to widen the typed message into a plain record — extractPromptSpeakerMetadata
|
|
2026
2528
|
// probes for snake_case and nested property variants not on RelayPromptMessage.
|
|
2027
2529
|
const speakerMetadata = extractPromptSpeakerMetadata({ ...msg });
|
|
2028
|
-
const speaker =
|
|
2530
|
+
const speaker =
|
|
2531
|
+
this.speakerIdentityTracker.identifySpeaker(speakerMetadata);
|
|
2029
2532
|
|
|
2030
2533
|
// Record in conversation history
|
|
2031
2534
|
this.conversationHistory.push({
|
|
2032
|
-
role:
|
|
2535
|
+
role: "caller",
|
|
2033
2536
|
text: msg.voicePrompt,
|
|
2034
2537
|
timestamp: Date.now(),
|
|
2035
2538
|
speaker,
|
|
2036
2539
|
});
|
|
2037
2540
|
|
|
2038
2541
|
// Record event
|
|
2039
|
-
recordCallEvent(this.callSessionId,
|
|
2542
|
+
recordCallEvent(this.callSessionId, "caller_spoke", {
|
|
2040
2543
|
transcript: msg.voicePrompt,
|
|
2041
2544
|
lang: msg.lang,
|
|
2042
2545
|
speakerId: speaker.speakerId,
|
|
@@ -2050,7 +2553,12 @@ export class RelayConnection {
|
|
|
2050
2553
|
// User message persistence is handled by the session pipeline
|
|
2051
2554
|
// (voice-session-bridge -> session.persistUserMessage) so we only
|
|
2052
2555
|
// need to fire the transcript notifier for UI subscribers here.
|
|
2053
|
-
fireCallTranscriptNotifier(
|
|
2556
|
+
fireCallTranscriptNotifier(
|
|
2557
|
+
session.conversationId,
|
|
2558
|
+
this.callSessionId,
|
|
2559
|
+
"caller",
|
|
2560
|
+
msg.voicePrompt,
|
|
2561
|
+
);
|
|
2054
2562
|
}
|
|
2055
2563
|
|
|
2056
2564
|
// Route to controller for session-backed response
|
|
@@ -2065,24 +2573,35 @@ export class RelayConnection {
|
|
|
2065
2573
|
try {
|
|
2066
2574
|
await conversationStore.addMessage(
|
|
2067
2575
|
session.conversationId,
|
|
2068
|
-
|
|
2069
|
-
JSON.stringify([{ type:
|
|
2070
|
-
{
|
|
2576
|
+
"user",
|
|
2577
|
+
JSON.stringify([{ type: "text", text: msg.voicePrompt }]),
|
|
2578
|
+
{
|
|
2579
|
+
userMessageChannel: "voice",
|
|
2580
|
+
assistantMessageChannel: "voice",
|
|
2581
|
+
userMessageInterface: "voice",
|
|
2582
|
+
assistantMessageInterface: "voice",
|
|
2583
|
+
},
|
|
2071
2584
|
);
|
|
2072
2585
|
} catch (err) {
|
|
2073
2586
|
// Best-effort — don't let persistence failures prevent the hold
|
|
2074
2587
|
// response from reaching the caller.
|
|
2075
|
-
log.warn(
|
|
2588
|
+
log.warn(
|
|
2589
|
+
{ err, callSessionId: this.callSessionId },
|
|
2590
|
+
"Failed to persist early caller utterance",
|
|
2591
|
+
);
|
|
2076
2592
|
}
|
|
2077
2593
|
}
|
|
2078
|
-
this.sendTextToken(
|
|
2594
|
+
this.sendTextToken("I'm still setting up. Please hold.", true);
|
|
2079
2595
|
}
|
|
2080
2596
|
}
|
|
2081
2597
|
|
|
2082
2598
|
private handleInterrupt(msg: RelayInterruptMessage): void {
|
|
2083
2599
|
log.info(
|
|
2084
|
-
{
|
|
2085
|
-
|
|
2600
|
+
{
|
|
2601
|
+
callSessionId: this.callSessionId,
|
|
2602
|
+
utteranceUntilInterrupt: msg.utteranceUntilInterrupt,
|
|
2603
|
+
},
|
|
2604
|
+
"Caller interrupted assistant",
|
|
2086
2605
|
);
|
|
2087
2606
|
|
|
2088
2607
|
// Abort any in-flight processing
|
|
@@ -2096,32 +2615,41 @@ export class RelayConnection {
|
|
|
2096
2615
|
}
|
|
2097
2616
|
|
|
2098
2617
|
private handleDtmf(msg: RelayDtmfMessage): void {
|
|
2099
|
-
if (this.connectionState ===
|
|
2618
|
+
if (this.connectionState === "disconnecting") {
|
|
2100
2619
|
return;
|
|
2101
2620
|
}
|
|
2102
2621
|
|
|
2103
2622
|
// Ignore DTMF during name capture and guardian decision wait
|
|
2104
|
-
if (
|
|
2623
|
+
if (
|
|
2624
|
+
this.connectionState === "awaiting_name" ||
|
|
2625
|
+
this.connectionState === "awaiting_guardian_decision"
|
|
2626
|
+
) {
|
|
2105
2627
|
return;
|
|
2106
2628
|
}
|
|
2107
2629
|
|
|
2108
2630
|
log.info(
|
|
2109
2631
|
{ callSessionId: this.callSessionId, digit: msg.digit },
|
|
2110
|
-
|
|
2632
|
+
"DTMF digit received",
|
|
2111
2633
|
);
|
|
2112
2634
|
|
|
2113
|
-
recordCallEvent(this.callSessionId,
|
|
2635
|
+
recordCallEvent(this.callSessionId, "caller_spoke", {
|
|
2114
2636
|
dtmfDigit: msg.digit,
|
|
2115
2637
|
});
|
|
2116
2638
|
|
|
2117
2639
|
// If guardian verification (inbound or outbound) is pending, accumulate
|
|
2118
2640
|
// digits and validate against the challenge via the guardian service.
|
|
2119
|
-
if (
|
|
2641
|
+
if (
|
|
2642
|
+
this.connectionState === "verification_pending" &&
|
|
2643
|
+
this.guardianVerificationActive
|
|
2644
|
+
) {
|
|
2120
2645
|
this.dtmfBuffer += msg.digit;
|
|
2121
2646
|
|
|
2122
2647
|
if (this.dtmfBuffer.length >= this.verificationCodeLength) {
|
|
2123
|
-
const enteredCode = this.dtmfBuffer.slice(
|
|
2124
|
-
|
|
2648
|
+
const enteredCode = this.dtmfBuffer.slice(
|
|
2649
|
+
0,
|
|
2650
|
+
this.verificationCodeLength,
|
|
2651
|
+
);
|
|
2652
|
+
this.dtmfBuffer = "";
|
|
2125
2653
|
this.attemptGuardianCodeVerification(enteredCode);
|
|
2126
2654
|
}
|
|
2127
2655
|
return;
|
|
@@ -2129,39 +2657,63 @@ export class RelayConnection {
|
|
|
2129
2657
|
|
|
2130
2658
|
// If invite redemption is pending, accumulate digits and validate
|
|
2131
2659
|
// the code against the caller's active voice invite.
|
|
2132
|
-
if (
|
|
2660
|
+
if (
|
|
2661
|
+
this.connectionState === "verification_pending" &&
|
|
2662
|
+
this.inviteRedemptionActive
|
|
2663
|
+
) {
|
|
2133
2664
|
this.dtmfBuffer += msg.digit;
|
|
2134
2665
|
|
|
2135
2666
|
if (this.dtmfBuffer.length >= this.inviteRedemptionCodeLength) {
|
|
2136
|
-
const enteredCode = this.dtmfBuffer.slice(
|
|
2137
|
-
|
|
2667
|
+
const enteredCode = this.dtmfBuffer.slice(
|
|
2668
|
+
0,
|
|
2669
|
+
this.inviteRedemptionCodeLength,
|
|
2670
|
+
);
|
|
2671
|
+
this.dtmfBuffer = "";
|
|
2138
2672
|
this.attemptInviteCodeRedemption(enteredCode);
|
|
2139
2673
|
}
|
|
2140
2674
|
return;
|
|
2141
2675
|
}
|
|
2142
2676
|
|
|
2143
2677
|
// If outbound callee verification is pending, accumulate digits and check the code
|
|
2144
|
-
if (
|
|
2678
|
+
if (
|
|
2679
|
+
this.connectionState === "verification_pending" &&
|
|
2680
|
+
this.verificationCode
|
|
2681
|
+
) {
|
|
2145
2682
|
this.dtmfBuffer += msg.digit;
|
|
2146
2683
|
|
|
2147
2684
|
if (this.dtmfBuffer.length >= this.verificationCodeLength) {
|
|
2148
|
-
const enteredCode = this.dtmfBuffer.slice(
|
|
2149
|
-
|
|
2685
|
+
const enteredCode = this.dtmfBuffer.slice(
|
|
2686
|
+
0,
|
|
2687
|
+
this.verificationCodeLength,
|
|
2688
|
+
);
|
|
2689
|
+
this.dtmfBuffer = "";
|
|
2150
2690
|
|
|
2151
2691
|
if (enteredCode === this.verificationCode) {
|
|
2152
2692
|
// Verification succeeded
|
|
2153
|
-
this.connectionState =
|
|
2693
|
+
this.connectionState = "connected";
|
|
2154
2694
|
this.verificationCode = null;
|
|
2155
2695
|
this.verificationAttempts = 0;
|
|
2156
2696
|
|
|
2157
|
-
recordCallEvent(
|
|
2158
|
-
|
|
2697
|
+
recordCallEvent(
|
|
2698
|
+
this.callSessionId,
|
|
2699
|
+
"callee_verification_succeeded",
|
|
2700
|
+
{},
|
|
2701
|
+
);
|
|
2702
|
+
log.info(
|
|
2703
|
+
{ callSessionId: this.callSessionId },
|
|
2704
|
+
"Callee verification succeeded",
|
|
2705
|
+
);
|
|
2159
2706
|
|
|
2160
2707
|
// Proceed to the normal call flow
|
|
2161
2708
|
if (this.controller) {
|
|
2162
|
-
this.controller
|
|
2163
|
-
|
|
2164
|
-
|
|
2709
|
+
this.controller
|
|
2710
|
+
.startInitialGreeting()
|
|
2711
|
+
.catch((err) =>
|
|
2712
|
+
log.error(
|
|
2713
|
+
{ err, callSessionId: this.callSessionId },
|
|
2714
|
+
"Failed to start initial outbound greeting after verification",
|
|
2715
|
+
),
|
|
2716
|
+
);
|
|
2165
2717
|
}
|
|
2166
2718
|
} else {
|
|
2167
2719
|
// Verification failed for this attempt
|
|
@@ -2169,48 +2721,85 @@ export class RelayConnection {
|
|
|
2169
2721
|
|
|
2170
2722
|
if (this.verificationAttempts >= this.verificationMaxAttempts) {
|
|
2171
2723
|
// Max attempts reached — end the call
|
|
2172
|
-
recordCallEvent(this.callSessionId,
|
|
2724
|
+
recordCallEvent(this.callSessionId, "callee_verification_failed", {
|
|
2173
2725
|
attempts: this.verificationAttempts,
|
|
2174
2726
|
});
|
|
2175
|
-
log.warn(
|
|
2727
|
+
log.warn(
|
|
2728
|
+
{
|
|
2729
|
+
callSessionId: this.callSessionId,
|
|
2730
|
+
attempts: this.verificationAttempts,
|
|
2731
|
+
},
|
|
2732
|
+
"Callee verification failed — max attempts reached",
|
|
2733
|
+
);
|
|
2176
2734
|
|
|
2177
|
-
this.sendTextToken(
|
|
2735
|
+
this.sendTextToken("Verification failed. Goodbye.", true);
|
|
2178
2736
|
|
|
2179
2737
|
// Mark failed immediately so a relay close during the goodbye TTS
|
|
2180
2738
|
// window cannot race this into a terminal "completed" status.
|
|
2181
2739
|
updateCallSession(this.callSessionId, {
|
|
2182
|
-
status:
|
|
2740
|
+
status: "failed",
|
|
2183
2741
|
endedAt: Date.now(),
|
|
2184
|
-
lastError:
|
|
2742
|
+
lastError: "Callee verification failed — max attempts exceeded",
|
|
2185
2743
|
});
|
|
2186
2744
|
|
|
2187
2745
|
const session = getCallSession(this.callSessionId);
|
|
2188
2746
|
if (session) {
|
|
2189
2747
|
expirePendingQuestions(this.callSessionId);
|
|
2190
|
-
persistCallCompletionMessage(
|
|
2191
|
-
|
|
2748
|
+
persistCallCompletionMessage(
|
|
2749
|
+
session.conversationId,
|
|
2750
|
+
this.callSessionId,
|
|
2751
|
+
).catch((err) => {
|
|
2752
|
+
log.error(
|
|
2753
|
+
{
|
|
2754
|
+
err,
|
|
2755
|
+
conversationId: session.conversationId,
|
|
2756
|
+
callSessionId: this.callSessionId,
|
|
2757
|
+
},
|
|
2758
|
+
"Failed to persist call completion message",
|
|
2759
|
+
);
|
|
2192
2760
|
});
|
|
2193
|
-
fireCallCompletionNotifier(
|
|
2761
|
+
fireCallCompletionNotifier(
|
|
2762
|
+
session.conversationId,
|
|
2763
|
+
this.callSessionId,
|
|
2764
|
+
);
|
|
2194
2765
|
if (session.initiatedFromConversationId) {
|
|
2195
|
-
addPointerMessage(
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2766
|
+
addPointerMessage(
|
|
2767
|
+
session.initiatedFromConversationId,
|
|
2768
|
+
"failed",
|
|
2769
|
+
session.toNumber,
|
|
2770
|
+
{
|
|
2771
|
+
reason: "Callee verification failed",
|
|
2772
|
+
},
|
|
2773
|
+
).catch((err) => {
|
|
2774
|
+
log.warn(
|
|
2775
|
+
{
|
|
2776
|
+
conversationId: session.initiatedFromConversationId,
|
|
2777
|
+
err,
|
|
2778
|
+
},
|
|
2779
|
+
"Skipping pointer write — origin conversation may no longer exist",
|
|
2780
|
+
);
|
|
2199
2781
|
});
|
|
2200
2782
|
}
|
|
2201
2783
|
}
|
|
2202
2784
|
|
|
2203
2785
|
// End the call with failed status after TTS plays
|
|
2204
2786
|
setTimeout(() => {
|
|
2205
|
-
this.endSession(
|
|
2787
|
+
this.endSession("Verification failed");
|
|
2206
2788
|
}, getTtsPlaybackDelayMs());
|
|
2207
2789
|
} else {
|
|
2208
2790
|
// Allow another attempt
|
|
2209
2791
|
log.info(
|
|
2210
|
-
{
|
|
2211
|
-
|
|
2792
|
+
{
|
|
2793
|
+
callSessionId: this.callSessionId,
|
|
2794
|
+
attempt: this.verificationAttempts,
|
|
2795
|
+
maxAttempts: this.verificationMaxAttempts,
|
|
2796
|
+
},
|
|
2797
|
+
"Callee verification attempt failed — retrying",
|
|
2798
|
+
);
|
|
2799
|
+
this.sendTextToken(
|
|
2800
|
+
"That code was incorrect. Please try again.",
|
|
2801
|
+
true,
|
|
2212
2802
|
);
|
|
2213
|
-
this.sendTextToken('That code was incorrect. Please try again.', true);
|
|
2214
2803
|
}
|
|
2215
2804
|
}
|
|
2216
2805
|
}
|
|
@@ -2220,10 +2809,10 @@ export class RelayConnection {
|
|
|
2220
2809
|
private handleError(msg: RelayErrorMessage): void {
|
|
2221
2810
|
log.error(
|
|
2222
2811
|
{ callSessionId: this.callSessionId, description: msg.description },
|
|
2223
|
-
|
|
2812
|
+
"ConversationRelay error",
|
|
2224
2813
|
);
|
|
2225
2814
|
|
|
2226
|
-
recordCallEvent(this.callSessionId,
|
|
2815
|
+
recordCallEvent(this.callSessionId, "call_failed", {
|
|
2227
2816
|
error: msg.description,
|
|
2228
2817
|
});
|
|
2229
2818
|
}
|