@vellumai/assistant 0.10.4 → 0.10.5-dev.202607022329.d89bf2f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +94 -91
- package/Dockerfile +5 -16
- package/README.md +7 -10
- package/bun.lock +0 -4
- package/docs/architecture/security.md +20 -20
- package/docs/stt-provider-onboarding.md +4 -5
- package/docs/trusted-contact-access.md +7 -7
- package/eslint-rules/cli-no-daemon-internals.js +4 -0
- package/eslint.config.mjs +7 -1
- package/knip.json +0 -1
- package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +36 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/invite-contract.test.ts +233 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +40 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +20 -8
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +33 -0
- package/node_modules/@vellumai/gateway-client/src/invite-contract.ts +216 -0
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +91 -0
- package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +39 -3
- package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +0 -8
- package/node_modules/@vellumai/service-contracts/src/__tests__/package-boundary.test.ts +1 -5
- package/node_modules/@vellumai/service-contracts/src/twilio-ingress.ts +3 -14
- package/openapi.yaml +1073 -225
- package/package.json +1 -4
- package/scripts/test.sh +35 -13
- package/src/__tests__/access-request-seed-content-blocks.test.ts +60 -4
- package/src/__tests__/acp-session.test.ts +4 -1
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +0 -6
- package/src/__tests__/add-message-preassigned-id.test.ts +56 -0
- package/src/__tests__/agent-loop-set-system-prompt.test.ts +139 -0
- package/src/__tests__/app-conversation-ids-backfill.test.ts +3 -0
- package/src/__tests__/app-source-watcher.test.ts +11 -0
- package/src/__tests__/approval-interception-trust-gates.test.ts +148 -3
- package/src/__tests__/assistant-attachments.test.ts +62 -4
- package/src/__tests__/assistant-id-boundary-guard.test.ts +0 -1
- package/src/__tests__/attachments-store-heic-normalize.test.ts +230 -0
- package/src/__tests__/attachments-store.test.ts +3 -0
- package/src/__tests__/background-shell-bash.test.ts +34 -0
- package/src/__tests__/background-shell-host-bash.test.ts +12 -0
- package/src/__tests__/background-tool-registry.test.ts +97 -0
- package/src/__tests__/background-tool-routes.test.ts +72 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
- package/src/__tests__/btw-routes.test.ts +57 -3
- package/src/__tests__/call-controller.test.ts +474 -84
- package/src/__tests__/call-domain.test.ts +48 -1
- package/src/__tests__/call-pointer-messages.test.ts +57 -19
- package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +2 -6
- package/src/__tests__/call-routes-http.test.ts +15 -14
- package/src/__tests__/call-setup-flow-invite.test.ts +568 -0
- package/src/__tests__/call-setup-flow-midcall-trust.test.ts +231 -0
- package/src/__tests__/call-setup-flow-name-capture.test.ts +754 -0
- package/src/__tests__/call-setup-flow-verification.test.ts +925 -0
- package/src/__tests__/call-setup-flow.test.ts +311 -0
- package/src/__tests__/cancel-resolves-conversation-key.test.ts +0 -1
- package/src/__tests__/channel-guardian.test.ts +4 -7
- package/src/__tests__/channel-readiness-service.test.ts +0 -6
- package/src/__tests__/channel-readiness-slack-remote.test.ts +0 -4
- package/src/__tests__/channel-retry-sweep.test.ts +76 -64
- package/src/__tests__/channel-setup-panel-ack.test.ts +184 -0
- package/src/__tests__/clear-all-lexical.test.ts +90 -0
- package/src/__tests__/compactor-media-strip.test.ts +177 -0
- package/src/__tests__/config-loader-backfill.test.ts +5 -5
- package/src/__tests__/config-schema.test.ts +17 -0
- package/src/__tests__/contact-store-interaction-info.test.ts +27 -70
- package/src/__tests__/contacts-relay-reads.test.ts +188 -23
- package/src/__tests__/contacts-tools.test.ts +42 -1
- package/src/__tests__/contacts-write.test.ts +181 -0
- package/src/__tests__/context-search-conversations-source.test.ts +220 -81
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop.test.ts +39 -0
- package/src/__tests__/conversation-analysis-routes.test.ts +1 -1
- package/src/__tests__/conversation-app-control-instantiation.test.ts +2 -0
- package/src/__tests__/conversation-clear-safety.test.ts +0 -1
- package/src/__tests__/conversation-crud-enabled-plugins.test.ts +60 -0
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -1
- package/src/__tests__/conversation-disk-view.test.ts +1 -0
- package/src/__tests__/conversation-enabledplugins-route.test.ts +184 -0
- package/src/__tests__/conversation-error.test.ts +0 -18
- package/src/__tests__/conversation-fork-crud.test.ts +67 -0
- package/src/__tests__/conversation-history-web-search.test.ts +7 -137
- package/src/__tests__/conversation-initial-prompt.test.ts +110 -0
- package/src/__tests__/conversation-loop-db-lock-resilience.test.ts +191 -0
- package/src/__tests__/conversation-process-callsite.test.ts +37 -0
- package/src/__tests__/conversation-queue.test.ts +149 -0
- package/src/__tests__/conversation-routes-enabled-plugins.test.ts +392 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +84 -0
- package/src/__tests__/conversation-routes-hidden-queue.test.ts +407 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +36 -16
- package/src/__tests__/conversation-skill-tools.test.ts +73 -0
- package/src/__tests__/conversation-wipe.test.ts +20 -5
- package/src/__tests__/credential-prompt-route.test.ts +69 -0
- package/src/__tests__/credential-security-invariants.test.ts +1 -3
- package/src/__tests__/db-drop-messages-fts.test.ts +115 -0
- package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -62
- package/src/__tests__/edit-propagation.test.ts +56 -2
- package/src/__tests__/empty-state-greeting-cache.test.ts +9 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +15 -110
- package/src/__tests__/guardian-expiry-notifier.test.ts +1 -0
- package/src/__tests__/guardian-question-mode.test.ts +57 -0
- package/src/__tests__/guardian-verification-voice-binding.test.ts +1 -1
- package/src/__tests__/guardian-wait-controller.test.ts +619 -0
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +33 -2
- package/src/__tests__/heic-fixture.ts +55 -0
- package/src/__tests__/helpers/seed-contact-channel.ts +0 -2
- package/src/__tests__/http-conversation-lineage.test.ts +6 -1
- package/src/__tests__/image-conversion.test.ts +170 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +72 -341
- package/src/__tests__/introduction-card-resolver.test.ts +354 -0
- package/src/__tests__/invite-redeemed-ipc.test.ts +97 -0
- package/src/__tests__/invite-routes-http.test.ts +246 -420
- package/src/__tests__/job-handler-registration-guard.test.ts +104 -0
- package/src/__tests__/lexical-index-dual-write.test.ts +426 -0
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +2 -2
- package/src/__tests__/list-messages-attachments.test.ts +159 -0
- package/src/__tests__/list-messages-background-tool-completion.test.ts +141 -0
- package/src/__tests__/list-messages-queued.test.ts +25 -0
- package/src/__tests__/managed-store.test.ts +129 -1
- package/src/__tests__/media-stream-audio-transcode.test.ts +113 -0
- package/src/__tests__/media-stream-output.test.ts +350 -19
- package/src/__tests__/media-stream-server-integration.test.ts +943 -169
- package/src/__tests__/media-stream-stt-session.test.ts +498 -1
- package/src/__tests__/memory-jobs-worker-lanes.test.ts +2 -2
- package/src/__tests__/model-intents.test.ts +1 -1
- package/src/__tests__/mtime-cache.test.ts +19 -0
- package/src/__tests__/non-member-access-request.test.ts +349 -3
- package/src/__tests__/notification-decision-fallback.test.ts +104 -8
- package/src/__tests__/notification-decision-strategy.test.ts +57 -8
- package/src/__tests__/onboarding-template-contract.test.ts +27 -43
- package/src/__tests__/openai-provider.test.ts +33 -0
- package/src/__tests__/openai-responses-provider.test.ts +40 -0
- package/src/__tests__/persistence-layering-guard.test.ts +29 -20
- package/src/__tests__/persona-resolver.test.ts +56 -2
- package/src/__tests__/personal-memory-auth-bypass.test.ts +62 -0
- package/src/__tests__/phone.test.ts +1 -15
- package/src/__tests__/plugin-bootstrap.test.ts +25 -196
- package/src/__tests__/plugin-disabled-state.test.ts +33 -0
- package/src/__tests__/plugin-effective-enabled-set.test.ts +227 -0
- package/src/__tests__/plugin-event-hub-facade.test.ts +323 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +156 -42
- package/src/__tests__/plugin-injector-contribution.test.ts +4 -20
- package/src/__tests__/plugin-pipeline.test.ts +63 -1
- package/src/__tests__/plugin-route-contribution.test.ts +62 -115
- package/src/__tests__/plugin-tool-contribution.test.ts +0 -22
- package/src/__tests__/plugin-types.test.ts +0 -1
- package/src/__tests__/pointer-conversation-trust.test.ts +38 -6
- package/src/__tests__/pre-model-call-sanitize.test.ts +1 -1
- package/src/__tests__/prune-old-tool-invocations-job.test.ts +97 -0
- package/src/__tests__/public-ingress-urls.test.ts +0 -48
- package/src/__tests__/qdrant-collection-migration.test.ts +145 -2
- package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +0 -1
- package/src/__tests__/resolve-trust-class.test.ts +28 -10
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +137 -0
- package/src/__tests__/schedule-retry.test.ts +38 -14
- package/src/__tests__/schedule-routes-workflow-validation.test.ts +3 -3
- package/src/__tests__/schedule-routes.test.ts +14 -6
- package/src/__tests__/schedule-store.test.ts +3 -1
- package/src/__tests__/scheduler-disk-pressure.test.ts +17 -6
- package/src/__tests__/scheduler-recurrence.test.ts +57 -107
- package/src/__tests__/scheduler-reuse-conversation.test.ts +51 -46
- package/src/__tests__/scheduler-wake.test.ts +13 -23
- package/src/__tests__/scheduler-worker-standdown.test.ts +220 -0
- package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
- package/src/__tests__/shell-observability.test.ts +1 -0
- package/src/__tests__/skill-boundary-guard.test.ts +3 -4
- package/src/__tests__/skill-load-plugin-scope.test.ts +238 -0
- package/src/__tests__/skill-projection-feature-flag.test.ts +2 -0
- package/src/__tests__/skill-script-runner-host.test.ts +53 -0
- package/src/__tests__/skill-script-runner-sandbox.test.ts +5 -2
- package/src/__tests__/skill-tool-factory.test.ts +73 -1
- package/src/__tests__/slack-app-setup-skill-regression.test.ts +37 -14
- package/src/__tests__/slack-inbound-verification.test.ts +36 -0
- package/src/__tests__/slack-notification-approval-card.test.ts +89 -2
- package/src/__tests__/slack-reaction-canonical-approval.test.ts +51 -1
- package/src/__tests__/stt-hints.test.ts +2 -256
- package/src/__tests__/subagent-allowlist-validation.test.ts +47 -0
- package/src/__tests__/subagent-call-site-routing.test.ts +72 -0
- package/src/__tests__/subagent-fork-notifications.test.ts +14 -2
- package/src/__tests__/subagent-persistence.test.ts +122 -0
- package/src/__tests__/subagent-spawn-and-await.test.ts +1 -0
- package/src/__tests__/subagent-terminal-message.test.ts +99 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +78 -1
- package/src/__tests__/system-prompt.test.ts +17 -118
- package/src/__tests__/task-scheduler.test.ts +68 -89
- package/src/__tests__/telephony-credential-preflight.test.ts +312 -0
- package/src/__tests__/telephony-tts-guard.test.ts +538 -0
- package/src/__tests__/title-generate-hook.test.ts +16 -0
- package/src/__tests__/tool-approval-handler.test.ts +87 -1
- package/src/__tests__/tool-approval-seed-content-blocks.test.ts +45 -20
- package/src/__tests__/tool-grant-request-escalation.test.ts +39 -0
- package/src/__tests__/trust-context-guards.test.ts +21 -10
- package/src/__tests__/trusted-contact-multichannel.test.ts +4 -0
- package/src/__tests__/twilio-provider.test.ts +17 -17
- package/src/__tests__/twilio-routes-twiml.test.ts +6 -543
- package/src/__tests__/twilio-routes.test.ts +257 -387
- package/src/__tests__/twilio-validation.test.ts +1 -1
- package/src/__tests__/unified-turn-context-client-os.test.ts +45 -0
- package/src/__tests__/usage-cache-backfill-migration.test.ts +5 -0
- package/src/__tests__/voice-approval-resolver-relay.test.ts +150 -0
- package/src/__tests__/voice-ingress-preflight.test.ts +61 -0
- package/src/__tests__/voice-quality.test.ts +2 -2
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +1 -7
- package/src/__tests__/voice-session-bridge-processing-wait.test.ts +27 -0
- package/src/__tests__/voice-session-bridge.test.ts +1 -10
- package/src/__tests__/workspace-git-service.test.ts +1 -1
- package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +1 -0
- package/src/__tests__/workspace-migration-118-seed-now-md.test.ts +120 -0
- package/src/__tests__/workspace-migration-120-revise-onboarding-threads.test.ts +175 -0
- package/src/__tests__/workspace-migration-121-seed-default-user-guardrails.test.ts +118 -0
- package/src/__tests__/workspace-migration-122-relocate-default-user-boundary.test.ts +119 -0
- package/src/__tests__/workspace-migration-123-swap-quality-profile-to-fable.test.ts +191 -0
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +235 -241
- package/src/acp/__tests__/prepare-agent-env.test.ts +0 -97
- package/src/acp/__tests__/session-manager-persistence.test.ts +2 -0
- package/src/acp/__tests__/session-manager-resume.test.ts +8 -0
- package/src/acp/__tests__/session-manager-usage.test.ts +6 -0
- package/src/acp/__tests__/session-manager.test.ts +6 -0
- package/src/acp/agent-process.test.ts +116 -0
- package/src/acp/agent-process.ts +71 -0
- package/src/acp/auto-install.ts +12 -8
- package/src/acp/failure-error.test.ts +109 -0
- package/src/acp/failure-error.ts +111 -0
- package/src/acp/prepare-agent-env.ts +0 -12
- package/src/acp/resolve-agent.test.ts +6 -45
- package/src/acp/resolve-agent.ts +2 -4
- package/src/acp/session-manager.test.ts +312 -0
- package/src/acp/session-manager.ts +102 -46
- package/src/agent/ax-tree-compaction.test.ts +4 -1
- package/src/agent/image-optimize.ts +17 -127
- package/src/agent/loop.ts +45 -192
- package/src/api/events/background-tool-completed.test.ts +51 -0
- package/src/api/events/background-tool-completed.ts +29 -0
- package/src/api/events/background-tool-started.test.ts +36 -0
- package/src/api/events/background-tool-started.ts +28 -0
- package/src/api/events/conversation-error.ts +0 -1
- package/src/api/events/open-panel.ts +33 -0
- package/src/api/events/subagent-status-changed.ts +4 -1
- package/src/api/index.ts +19 -0
- package/src/api/responses/conversation-message.ts +34 -0
- package/src/approvals/guardian-decision-primitive.ts +74 -24
- package/src/approvals/guardian-request-resolvers.ts +450 -122
- package/src/apps/app-store.ts +1 -0
- package/src/apps/shared-app-links-store.ts +1 -0
- package/src/avatar/identity-avatar.ts +3 -7
- package/src/background-wake/wake-intent-hooks.test.ts +1 -1
- package/src/calls/__tests__/fish-audio-client.test.ts +73 -0
- package/src/calls/__tests__/gateway-invite-reader.test.ts +170 -0
- package/src/calls/__tests__/relay-setup-router.test.ts +126 -102
- package/src/calls/call-controller.ts +134 -48
- package/src/calls/call-domain.ts +18 -42
- package/src/calls/call-pointer-messages.ts +51 -67
- package/src/calls/call-setup-flow-types.ts +76 -0
- package/src/calls/call-setup-flow.ts +1743 -0
- package/src/calls/call-speech-output.ts +87 -23
- package/src/calls/call-store.ts +4 -0
- package/src/calls/call-transport.ts +22 -36
- package/src/calls/fish-audio-client.ts +3 -0
- package/src/calls/gateway-invite-reader.ts +98 -0
- package/src/calls/guardian-wait-controller.ts +451 -0
- package/src/calls/media-stream-audio-transcode.ts +82 -2
- package/src/calls/media-stream-output.ts +142 -36
- package/src/calls/media-stream-server.ts +297 -178
- package/src/calls/media-stream-stt-session.ts +393 -50
- package/src/calls/media-turn-detector.ts +4 -6
- package/src/calls/relay-access-wait.ts +3 -4
- package/src/calls/relay-setup-router.ts +22 -43
- package/src/calls/relay-verification.ts +17 -24
- package/src/calls/resolve-call-tts-provider.ts +96 -29
- package/src/calls/stt-hints.ts +24 -87
- package/src/calls/telephony-credential-preflight.ts +159 -0
- package/src/calls/telephony-tts-capability.ts +183 -0
- package/src/calls/tts-call-strategy.ts +18 -20
- package/src/calls/twilio-provider.ts +3 -2
- package/src/calls/twilio-routes.ts +47 -351
- package/src/calls/types.ts +5 -3
- package/src/calls/voice-ingress-preflight.ts +9 -0
- package/src/calls/voice-quality.ts +13 -18
- package/src/calls/voice-session-bridge.ts +25 -35
- package/src/channels/__tests__/types.test.ts +28 -0
- package/src/channels/channel-verification-sessions.ts +7 -2
- package/src/channels/config.ts +0 -42
- package/src/channels/slack-thread-store.ts +12 -0
- package/src/channels/types.ts +23 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -1
- package/src/cli/commands/__tests__/schedules.test.ts +83 -34
- package/src/cli/commands/__tests__/status.test.ts +42 -2
- package/src/cli/commands/contacts.ts +2 -2
- package/src/cli/commands/credentials.ts +17 -0
- package/src/cli/commands/monitoring.ts +191 -0
- package/src/cli/commands/schedules-worker.ts +175 -0
- package/src/cli/commands/schedules.ts +169 -43
- package/src/cli/commands/status.ts +12 -1
- package/src/cli/commands/tools.ts +5 -1
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +1 -0
- package/src/cli/lib/__tests__/search-plugins.test.ts +29 -1
- package/src/cli/lib/__tests__/toggle-plugin.test.ts +7 -0
- package/src/cli/lib/search-plugins.ts +6 -0
- package/src/cli/lib/toggle-plugin.ts +21 -2
- package/src/cli/program.ts +3 -1
- package/src/config/__tests__/deployment-context-defaults.test.ts +23 -0
- package/src/config/acp-defaults.test.ts +0 -10
- package/src/config/acp-defaults.ts +0 -6
- package/src/config/bundled-skills/acp/SKILL.md +13 -45
- package/src/config/bundled-skills/acp/TOOLS.json +3 -3
- package/src/config/bundled-skills/app-builder/SKILL.md +3 -3
- package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +9 -9
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +1 -1
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +1 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +9 -9
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +10 -7
- package/src/config/bundled-skills/skill-management/TOOLS.json +10 -0
- package/src/config/call-site-defaults.ts +0 -2
- package/src/config/feature-flag-registry.json +16 -8
- package/src/config/loader.ts +11 -0
- package/src/config/schema.ts +9 -1
- package/src/config/schemas/__tests__/memory-v2.test.ts +2 -2
- package/src/config/schemas/__tests__/memory-v3.test.ts +64 -0
- package/src/config/schemas/call-site-catalog.ts +0 -12
- package/src/config/schemas/calls.ts +8 -0
- package/src/config/schemas/elevenlabs.ts +1 -1
- package/src/config/schemas/llm.ts +0 -2
- package/src/config/schemas/memory-v2.ts +1 -1
- package/src/config/schemas/memory-v3.ts +82 -0
- package/src/config/schemas/monitoring.ts +61 -0
- package/src/config/schemas/schedules.ts +35 -0
- package/src/config/seed-inference-profiles.ts +2 -2
- package/src/config/skills-plugin-filter.test.ts +96 -0
- package/src/config/skills.ts +26 -0
- package/src/contacts/__tests__/member-write-relay.test.ts +156 -4
- package/src/contacts/canonical-guardian-store.ts +5 -0
- package/src/contacts/contact-store.ts +81 -35
- package/src/contacts/contacts-write.ts +50 -25
- package/src/contacts/member-write-relay.ts +115 -31
- package/src/contacts/types.ts +0 -8
- package/src/context/compactor.ts +29 -15
- package/src/context/outbound-sanitize.ts +202 -0
- package/src/daemon/__tests__/abort-null-controller.test.ts +167 -0
- package/src/daemon/__tests__/conversation-tool-setup-plugin-scope.test.ts +122 -0
- package/src/daemon/__tests__/embedding-reconcile.test.ts +99 -16
- package/src/daemon/__tests__/wake-conversation-ops-completion.test.ts +107 -0
- package/src/daemon/abort-watchdog.ts +7 -0
- package/src/daemon/app-source-watcher.ts +9 -0
- package/src/daemon/assistant-attachments.ts +42 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +256 -147
- package/src/daemon/conversation-agent-loop.ts +61 -68
- package/src/daemon/conversation-error.ts +1 -15
- package/src/daemon/conversation-history.ts +32 -236
- package/src/daemon/conversation-initial-prompt.ts +77 -0
- package/src/daemon/conversation-lifecycle.ts +25 -1
- package/src/daemon/conversation-messaging.ts +2 -2
- package/src/daemon/conversation-plugin-scope.ts +38 -0
- package/src/daemon/conversation-process.ts +50 -14
- package/src/daemon/conversation-runtime-assembly.ts +36 -6
- package/src/daemon/conversation-skill-tools.ts +19 -3
- package/src/daemon/conversation-store.ts +3 -3
- package/src/daemon/conversation-surfaces.ts +171 -0
- package/src/daemon/conversation-tool-setup.test.ts +95 -0
- package/src/daemon/conversation-tool-setup.ts +116 -1
- package/src/daemon/conversation-turn-finalize.ts +211 -0
- package/src/daemon/conversation.ts +92 -1
- package/src/daemon/disk-pressure-policy.ts +2 -1
- package/src/daemon/event-loop-watchdog.ts +22 -6
- package/src/daemon/external-plugins-bootstrap.ts +47 -134
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +1 -1
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +26 -14
- package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +27 -14
- package/src/daemon/handlers/__tests__/config-channels.test.ts +0 -5
- package/src/daemon/handlers/config-a2a.ts +7 -14
- package/src/daemon/handlers/config-channels.ts +2 -10
- package/src/daemon/handlers/conversation-history.ts +3 -1
- package/src/daemon/handlers/conversations.ts +0 -52
- package/src/daemon/handlers/shared.ts +4 -24
- package/src/daemon/handlers/skills.ts +4 -1
- package/src/daemon/host-cu-proxy.ts +1 -1
- package/src/daemon/lifecycle.ts +59 -472
- package/src/daemon/message-protocol.ts +3 -3
- package/src/daemon/message-provenance.ts +12 -14
- package/src/daemon/message-types/background-tools.ts +28 -0
- package/src/daemon/message-types/contacts.ts +2 -10
- package/src/daemon/message-types/conversations.ts +10 -0
- package/src/daemon/message-types/integrations.ts +2 -0
- package/src/daemon/message-types/surfaces.ts +1 -0
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/pointer-conversation-trust.ts +23 -9
- package/src/daemon/pointer-turn-runner.ts +174 -0
- package/src/daemon/providers-setup.ts +16 -0
- package/src/daemon/shutdown-handlers.ts +139 -47
- package/src/daemon/skill-memory-refresh.ts +2 -2
- package/src/daemon/sqlite-corruption-watchdog.test.ts +217 -0
- package/src/daemon/sqlite-corruption-watchdog.ts +155 -0
- package/src/daemon/tool-setup-types.ts +8 -0
- package/src/daemon/trust-context.ts +21 -6
- package/src/daemon/turn-latency-tracker.test.ts +93 -0
- package/src/daemon/turn-latency-tracker.ts +38 -0
- package/src/daemon/wake-conversation-ops.ts +10 -8
- package/src/documents/document-comments-store.test.ts +7 -1
- package/src/documents/document-comments-store.ts +8 -0
- package/src/documents/document-store.ts +15 -0
- package/src/export/__tests__/transcript-formatter.test.ts +11 -0
- package/src/export/transcript-formatter.ts +49 -67
- package/src/home/__tests__/feed-writer.test.ts +247 -1
- package/src/home/conversation-starter-checkpoints.ts +1 -0
- package/src/home/feed-writer.ts +61 -0
- package/src/home/job-handlers/conversation-starters.ts +1 -0
- package/src/hooks/hook-loader.ts +12 -0
- package/src/hooks/registry.ts +32 -4
- package/src/inbound/public-ingress-urls.ts +0 -17
- package/src/ipc/__tests__/contacts-info-ipc.test.ts +285 -0
- package/src/ipc/assistant-server.ts +28 -7
- package/src/ipc/routes/__tests__/contacts-mirror-apply-atomicity.test.ts +62 -0
- package/src/ipc/routes/__tests__/contacts-mirror-apply.test.ts +272 -0
- package/src/ipc/routes/__tests__/contacts-mirror-ipc-routes.test.ts +322 -0
- package/src/ipc/routes/__tests__/invite-ipc-routes.test.ts +60 -4
- package/src/ipc/routes/contacts-info-ipc-routes.ts +239 -0
- package/src/ipc/routes/contacts-mirror-ipc-routes.ts +202 -0
- package/src/ipc/routes/db-proxy.ts +5 -2
- package/src/ipc/routes/invite-ipc-routes.ts +53 -39
- package/src/jobs/worker.ts +9 -2
- package/src/live-voice/__tests__/live-voice-archive.test.ts +10 -1
- package/src/live-voice/live-voice-archive.ts +5 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +22 -6
- package/src/messaging/providers/channel-transport.ts +6 -0
- package/src/messaging/providers/index.ts +3 -0
- package/src/messaging/providers/slack/api.test.ts +49 -1
- package/src/messaging/providers/slack/api.ts +110 -0
- package/src/messaging/providers/slack/send.test.ts +85 -0
- package/src/messaging/providers/slack/send.ts +100 -28
- package/src/messaging/providers/slack/transport.ts +7 -13
- package/src/messaging/providers/whatsapp/send.ts +29 -9
- package/src/monitoring/__tests__/control.test.ts +235 -0
- package/src/monitoring/__tests__/sample-ring-buffer.test.ts +96 -0
- package/src/monitoring/control.ts +105 -0
- package/src/monitoring/resource-sampler.ts +258 -0
- package/src/monitoring/sample-ring-buffer.ts +108 -0
- package/src/monitoring/worker.ts +94 -0
- package/src/notifications/README.md +1 -1
- package/src/notifications/access-request-copy.ts +94 -23
- package/src/notifications/adapters/slack.ts +89 -21
- package/src/notifications/approval-card-builder.ts +33 -18
- package/src/notifications/approval-card-data.ts +48 -15
- package/src/notifications/broadcaster.ts +25 -16
- package/src/notifications/decision-engine.ts +131 -32
- package/src/notifications/guardian-question-mode.ts +58 -10
- package/src/permissions/approval-policy.ts +8 -1
- package/src/permissions/checker.ts +2 -0
- package/src/permissions/gateway-threshold-reader.ts +7 -6
- package/src/permissions/ipc-risk-types.ts +1 -0
- package/src/permissions/secret-prompter.ts +11 -3
- package/src/permissions/types.ts +2 -2
- package/src/persistence/__tests__/conversation-queries-search.test.ts +397 -0
- package/src/persistence/__tests__/conversation-search-lexical.test.ts +103 -0
- package/src/persistence/__tests__/jobs-worker-memory-disabled.test.ts +95 -0
- package/src/persistence/__tests__/slow-query-log.test.ts +266 -0
- package/src/persistence/__tests__/slow-sync-log.test.ts +155 -0
- package/src/persistence/a2a-invite-store.ts +205 -0
- package/src/persistence/attachments-store.ts +94 -10
- package/src/persistence/checkpoints.ts +45 -0
- package/src/persistence/conversation-crud.ts +453 -260
- package/src/persistence/conversation-display-order-migration.ts +8 -2
- package/src/persistence/conversation-group-migration.test.ts +8 -0
- package/src/persistence/conversation-group-migration.ts +9 -0
- package/src/persistence/conversation-queries.ts +244 -125
- package/src/persistence/conversation-row-batch-delete.ts +9 -5
- package/src/persistence/conversation-search-lexical.ts +49 -0
- package/src/persistence/db-async-query.ts +42 -0
- package/src/persistence/db-connection.ts +13 -0
- package/src/persistence/delivery-channels.ts +0 -82
- package/src/persistence/delivery-crud.ts +27 -6
- package/src/persistence/embeddings/__tests__/embedding-identity.test.ts +18 -5
- package/src/persistence/embeddings/__tests__/messages-lexical-index.test.ts +334 -0
- package/src/persistence/embeddings/embedding-identity.ts +17 -5
- package/src/persistence/embeddings/embedding-runtime-manager.ts +31 -0
- package/src/persistence/embeddings/messages-lexical-index.ts +396 -0
- package/src/persistence/embeddings/qdrant-client.ts +81 -5
- package/src/persistence/group-crud.ts +13 -1
- package/src/persistence/job-handlers/__tests__/message-lexical-backfill.test.ts +506 -0
- package/src/persistence/job-handlers/__tests__/message-lexical-enqueue.test.ts +164 -0
- package/src/persistence/job-handlers/__tests__/message-lexical.test.ts +259 -0
- package/src/persistence/job-handlers/cleanup.ts +57 -9
- package/src/persistence/job-handlers/message-lexical-backfill.ts +187 -0
- package/src/persistence/job-handlers/message-lexical.ts +251 -0
- package/src/persistence/jobs-store.ts +102 -8
- package/src/persistence/jobs-worker.ts +68 -28
- package/src/persistence/llm-request-log-store.ts +35 -19
- package/src/persistence/llm-usage-store.ts +11 -0
- package/src/persistence/memory-lifecycle-hooks.test.ts +65 -0
- package/src/persistence/memory-lifecycle-hooks.ts +55 -1
- package/src/persistence/migrations/028-call-session-mode.ts +1 -1
- package/src/persistence/migrations/116-messages-fts.ts +8 -1
- package/src/persistence/migrations/311-create-subagents-table.ts +50 -0
- package/src/persistence/migrations/312-drop-inbox-conversation-state-table.ts +15 -0
- package/src/persistence/migrations/313-drop-messages-fts.ts +17 -0
- package/src/persistence/migrations/314-add-conversation-enabled-plugins.ts +25 -0
- package/src/persistence/migrations/315-create-a2a-invites.ts +58 -0
- package/src/persistence/migrations/316-drop-contact-channels-invite-id.ts +28 -0
- package/src/persistence/migrations/317-canonical-guardian-requester-signals.ts +24 -0
- package/src/persistence/migrations/318-drop-contact-channel-telemetry.ts +32 -0
- package/src/persistence/migrations/__tests__/315-create-a2a-invites.test.ts +161 -0
- package/src/persistence/migrations/__tests__/316-drop-contact-channels-invite-id.test.ts +156 -0
- package/src/persistence/migrations/__tests__/318-drop-contact-channel-telemetry.test.ts +134 -0
- package/src/persistence/migrations/__tests__/migration-prefix-guard.test.ts +99 -0
- package/src/persistence/raw-query.ts +50 -11
- package/src/persistence/schema/a2a.ts +14 -0
- package/src/persistence/schema/contacts.ts +0 -62
- package/src/persistence/schema/conversation-groups.ts +12 -0
- package/src/persistence/schema/conversations.ts +2 -0
- package/src/persistence/schema/documents.ts +65 -0
- package/src/persistence/schema/guardian.ts +4 -0
- package/src/persistence/schema/index.ts +4 -0
- package/src/persistence/schema/memory-injection.ts +62 -0
- package/src/persistence/schema/workflows.ts +55 -0
- package/src/persistence/slow-query-log.ts +437 -0
- package/src/persistence/slow-sync-log.ts +204 -0
- package/src/persistence/steps.ts +16 -0
- package/src/persistence/subagent-store.ts +125 -0
- package/src/persistence/worker-control.ts +45 -238
- package/src/plugin-api/event-hub-facade.ts +172 -0
- package/src/plugin-api/index.ts +9 -1
- package/src/plugin-api/types.ts +40 -9
- package/src/plugins/defaults/default-plugin-names.ts +60 -0
- package/src/plugins/defaults/index.ts +19 -29
- package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +46 -67
- package/src/plugins/defaults/memory/__tests__/db-async-query.test.ts +36 -1
- package/src/plugins/defaults/memory/__tests__/find-most-recent-retrospective-for.test.ts +2 -4
- package/src/plugins/defaults/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +2 -2
- package/src/plugins/defaults/memory/__tests__/jobs-worker-v2-schedule.test.ts +9 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +7 -4
- package/src/plugins/defaults/memory/config.ts +11 -0
- package/src/plugins/defaults/memory/context-search/agent-runner.ts +1 -1
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +124 -113
- package/src/plugins/defaults/memory/embeddings.ts +77 -0
- package/src/plugins/defaults/memory/find-most-recent-retrospective-for.ts +66 -0
- package/src/plugins/defaults/memory/frontmatter.ts +50 -0
- package/src/plugins/defaults/memory/graph/bootstrap.test.ts +36 -10
- package/src/plugins/defaults/memory/graph/bootstrap.ts +6 -5
- package/src/plugins/defaults/memory/graph/consolidation.ts +3 -5
- package/src/plugins/defaults/memory/graph/extraction.ts +3 -6
- package/src/plugins/defaults/memory/graph/graph-search.ts +12 -23
- package/src/plugins/defaults/memory/graph/narrative.ts +3 -5
- package/src/plugins/defaults/memory/graph/pattern-scan.ts +3 -5
- package/src/plugins/defaults/memory/graph/retriever.test.ts +1 -6
- package/src/plugins/defaults/memory/graph/retriever.ts +5 -10
- package/src/plugins/defaults/memory/graph/tools.ts +1 -1
- package/src/plugins/defaults/memory/hooks/init.ts +31 -5
- package/src/plugins/defaults/memory/indexer.ts +2 -3
- package/src/plugins/defaults/memory/injectors.ts +2 -2
- package/src/plugins/defaults/memory/job-handler-registration.ts +96 -0
- package/src/plugins/defaults/memory/job-handlers/backfill.ts +4 -26
- package/src/plugins/defaults/memory/job-handlers/embedding.test.ts +6 -19
- package/src/plugins/defaults/memory/job-handlers/embedding.ts +9 -22
- package/src/plugins/defaults/memory/job-handlers/index-maintenance.ts +2 -3
- package/src/plugins/defaults/memory/job-handlers.ts +13 -12
- package/src/plugins/defaults/memory/jobs/embed-concept-page.ts +1 -1
- package/src/plugins/defaults/memory/llm-helpers.ts +76 -0
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +8 -5
- package/src/plugins/defaults/memory/memory-retrospective-startup-cleanup.ts +2 -2
- package/src/plugins/defaults/memory/memory-v2-concept-frequency.ts +2 -0
- package/src/plugins/defaults/memory/persistence-hooks.ts +46 -1
- package/src/plugins/defaults/memory/pkb/pkb-index.ts +1 -5
- package/src/plugins/defaults/memory/pkb/pkb-search.ts +2 -2
- package/src/plugins/defaults/memory/routes/__tests__/memory-v2-simulate-route.test.ts +4 -6
- package/src/plugins/defaults/memory/routes/memory-item-routes.ts +1 -1
- package/src/plugins/defaults/memory/search/semantic.ts +2 -2
- package/src/plugins/defaults/memory/startup.ts +244 -0
- package/src/plugins/defaults/memory/task-memory-cleanup.ts +8 -0
- package/src/{daemon → plugins/defaults/memory/v2}/__tests__/memory-v2-startup.test.ts +6 -6
- package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +2 -13
- package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +4 -5
- package/src/plugins/defaults/memory/v2/__tests__/sweep-job.test.ts +1 -8
- package/src/plugins/defaults/memory/v2/activation.ts +2 -4
- package/src/plugins/defaults/memory/v2/cli-command-store.ts +2 -4
- package/src/plugins/defaults/memory/v2/frontmatter-sweep.ts +1 -1
- package/src/{daemon → plugins/defaults/memory/v2}/memory-v2-startup.test.ts +8 -8
- package/src/{daemon → plugins/defaults/memory/v2}/memory-v2-startup.ts +20 -22
- package/src/plugins/defaults/memory/v2/migration.ts +2 -5
- package/src/plugins/defaults/memory/v2/page-store.ts +1 -1
- package/src/plugins/defaults/memory/v2/qdrant.ts +2 -3
- package/src/plugins/defaults/memory/v2/router.ts +5 -5
- package/src/plugins/defaults/memory/v2/sim.ts +2 -4
- package/src/plugins/defaults/memory/v2/skill-store.ts +2 -4
- package/src/plugins/defaults/memory/v2/sweep-job.ts +6 -6
- package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +11 -9
- package/src/plugins/defaults/memory/v3/__tests__/dense.test.ts +107 -2
- package/src/plugins/defaults/memory/v3/__tests__/gate-flag.test.ts +69 -0
- package/src/plugins/defaults/memory/v3/__tests__/gate.test.ts +181 -0
- package/src/plugins/defaults/memory/v3/__tests__/live-integration.test.ts +7 -3
- package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +345 -8
- package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -4
- package/src/plugins/defaults/memory/v3/__tests__/section-needle.test.ts +64 -0
- package/src/plugins/defaults/memory/v3/__tests__/shadow-integration.test.ts +18 -13
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +62 -1
- package/src/plugins/defaults/memory/v3/card.ts +1 -4
- package/src/plugins/defaults/memory/v3/dense.ts +60 -25
- package/src/plugins/defaults/memory/v3/edge.ts +1 -1
- package/src/plugins/defaults/memory/v3/gate-flag.ts +20 -0
- package/src/plugins/defaults/memory/v3/gate.ts +158 -0
- package/src/plugins/defaults/memory/v3/maintain-job.ts +2 -4
- package/src/plugins/defaults/memory/v3/orchestrate.ts +186 -25
- package/src/plugins/defaults/memory/v3/pool-select.test.ts +7 -9
- package/src/plugins/defaults/memory/v3/pool-select.ts +6 -6
- package/src/plugins/defaults/memory/v3/prune.ts +2 -2
- package/src/plugins/defaults/memory/v3/section-dense-store.ts +8 -11
- package/src/plugins/defaults/memory/v3/section-needle.ts +27 -7
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +10 -0
- package/src/plugins/defaults/memory/v3-eval/eval-packets.ts +1 -4
- package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +5 -0
- package/src/plugins/defaults/turn-context/injectors.ts +1 -0
- package/src/plugins/defaults/turn-context/unified-turn-context.ts +12 -0
- package/src/plugins/injector-registry.ts +13 -2
- package/src/plugins/mtime-cache.ts +36 -47
- package/src/plugins/pipeline.ts +15 -1
- package/src/plugins/types.ts +14 -23
- package/src/prompts/__tests__/system-prompt.test.ts +170 -32
- package/src/prompts/persona-resolver.ts +13 -9
- package/src/prompts/system-prompt.ts +30 -9
- package/src/prompts/templates/BOOTSTRAP.md +16 -119
- package/src/prompts/templates/SOUL.md +43 -13
- package/src/prompts/templates/system-sections.ts +32 -27
- package/src/prompts/templates/users/default.md +19 -0
- package/src/providers/__tests__/placeholder-sentinels.test.ts +56 -0
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +96 -1
- package/src/providers/model-intents.ts +2 -2
- package/src/providers/openai/chat-completions-provider.ts +14 -1
- package/src/providers/openai/responses-provider.ts +18 -0
- package/src/providers/placeholder-sentinels.ts +18 -8
- package/src/providers/retry.ts +1 -0
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +0 -64
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +92 -79
- package/src/providers/speech-to-text/deepgram-realtime.test.ts +98 -0
- package/src/providers/speech-to-text/deepgram-realtime.ts +62 -3
- package/src/providers/speech-to-text/provider-catalog.ts +0 -89
- package/src/providers/speech-to-text/resolve.ts +55 -0
- package/src/providers/tool-progress-events.test.ts +50 -0
- package/src/providers/tool-progress-events.ts +99 -0
- package/src/providers/types.ts +1 -1
- package/src/providers/vellum-model-routing.test.ts +51 -0
- package/src/providers/vellum-model-routing.ts +84 -0
- package/src/runtime/AGENTS.md +5 -4
- package/src/runtime/__tests__/agent-wake.test.ts +158 -1
- package/src/runtime/__tests__/channel-verification-service.test.ts +2 -2
- package/src/runtime/__tests__/trust-verdict-consumer.test.ts +37 -8
- package/src/runtime/access-request-helper.ts +154 -3
- package/src/runtime/actor-trust-resolver.ts +7 -21
- package/src/runtime/agent-wake.ts +41 -1
- package/src/runtime/assistant-event.ts +86 -12
- package/src/runtime/auth/__tests__/route-policy.test.ts +31 -0
- package/src/runtime/channel-approval-types.ts +52 -2
- package/src/runtime/channel-approvals.ts +23 -8
- package/src/runtime/channel-invite-transport.ts +2 -2
- package/src/runtime/channel-invite-transports/voice.ts +1 -1
- package/src/runtime/channel-readiness-service.ts +3 -5
- package/src/runtime/channel-retry-sweep.ts +18 -24
- package/src/runtime/channel-verification-service.ts +9 -11
- package/src/runtime/finalize-event-delivery.ts +30 -16
- package/src/runtime/guardian-action-service.ts +10 -11
- package/src/runtime/guardian-reply-router.ts +122 -21
- package/src/runtime/http-server.ts +44 -101
- package/src/runtime/introduction-policy.test.ts +214 -0
- package/src/runtime/introduction-policy.ts +284 -0
- package/src/runtime/invite-service.ts +104 -361
- package/src/runtime/local-actor-identity.test.ts +38 -0
- package/src/runtime/local-actor-identity.ts +14 -0
- package/src/runtime/middleware/__tests__/rate-limiter.test.ts +22 -0
- package/src/runtime/middleware/rate-limiter.ts +30 -0
- package/src/runtime/middleware/twilio-validation.ts +5 -7
- package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +28 -0
- package/src/runtime/migrations/origin-mode.ts +6 -6
- package/src/runtime/pre-first-message-gate.ts +1 -0
- package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +6 -35
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +24 -3
- package/src/runtime/routes/__tests__/contact-routes.test.ts +80 -15
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +7 -1
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +0 -3
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +142 -1
- package/src/runtime/routes/__tests__/invite-relay-routes.test.ts +119 -30
- package/src/runtime/routes/__tests__/migration-import-allowed-hosts-env.test.ts +70 -0
- package/src/runtime/routes/__tests__/monitoring-routes.test.ts +194 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +755 -29
- package/src/runtime/routes/__tests__/retrospective-routes.test.ts +4 -0
- package/src/runtime/routes/__tests__/schedule-routes-create.test.ts +70 -0
- package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +161 -0
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/surface-content-routes.test.ts +1 -1
- package/src/runtime/routes/acp-routes.ts +15 -3
- package/src/runtime/routes/app-management-routes.ts +4 -7
- package/src/runtime/routes/approval-routes.ts +4 -0
- package/src/runtime/routes/attachment-routes.ts +54 -3
- package/src/runtime/routes/avatar-routes.ts +0 -4
- package/src/runtime/routes/background-tool-routes.ts +34 -5
- package/src/runtime/routes/btw-routes.ts +32 -4
- package/src/runtime/routes/channel-route-shared.ts +18 -14
- package/src/runtime/routes/channel-verification-routes.ts +0 -3
- package/src/runtime/routes/contact-routes.ts +298 -117
- package/src/runtime/routes/conversation-list-routes.ts +6 -0
- package/src/runtime/routes/conversation-management-routes.ts +74 -37
- package/src/runtime/routes/conversation-query-routes.ts +5 -2
- package/src/runtime/routes/conversation-routes.ts +222 -82
- package/src/runtime/routes/conversations-import-routes.ts +8 -1
- package/src/runtime/routes/credential-prompt-routes.ts +20 -5
- package/src/runtime/routes/debug-routes.ts +1 -0
- package/src/runtime/routes/documents-routes.ts +5 -2
- package/src/runtime/routes/empty-state-greeting-cache.ts +37 -10
- package/src/runtime/routes/global-search-routes.ts +2 -1
- package/src/runtime/routes/guardian-approval-interception.ts +51 -0
- package/src/runtime/routes/home-feed-routes.ts +60 -1
- package/src/runtime/routes/identity-routes.ts +4 -87
- package/src/runtime/routes/inbound-message-handler.ts +82 -41
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +430 -57
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +302 -572
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +76 -270
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +21 -116
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +9 -1
- package/src/runtime/routes/inbound-stages/escalation-intercept.test.ts +158 -0
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +22 -1
- package/src/runtime/routes/index.ts +6 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +22 -16
- package/src/runtime/routes/internal-twilio-routes.ts +0 -15
- package/src/runtime/routes/messages-lexical-routes.ts +75 -0
- package/src/runtime/routes/migration-routes.ts +69 -6
- package/src/runtime/routes/monitoring-routes.ts +237 -0
- package/src/runtime/routes/plugins-routes.ts +329 -8
- package/src/runtime/routes/schedule-routes.ts +49 -8
- package/src/runtime/routes/schedule-worker-routes.ts +210 -0
- package/src/runtime/routes/secret-routes.ts +1 -1
- package/src/runtime/routes/surface-conversation-resolver.ts +1 -0
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +54 -9
- package/src/runtime/services/__tests__/auto-analysis-enqueue.test.ts +30 -6
- package/src/runtime/services/__tests__/conversation-analyze-job.test.ts +43 -8
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +20 -0
- package/src/runtime/services/analyze-conversation.ts +14 -4
- package/src/runtime/services/conversation-serializer.ts +5 -0
- package/src/runtime/slack-no-response.ts +47 -0
- package/src/runtime/slack-reply-session.test.ts +633 -0
- package/src/runtime/slack-reply-session.ts +382 -0
- package/src/runtime/slack-task-progress.ts +95 -0
- package/src/runtime/sync/resource-sync-events.ts +25 -0
- package/src/runtime/tool-grant-request-helper.ts +17 -1
- package/src/runtime/trust-class.test.ts +141 -0
- package/src/runtime/trust-class.ts +121 -0
- package/src/runtime/trust-verdict-consumer.ts +7 -6
- package/src/schedule/__tests__/run-script.test.ts +21 -0
- package/src/schedule/__tests__/worker-control.test.ts +241 -0
- package/src/schedule/run-script.ts +13 -6
- package/src/schedule/schedule-usage-store.ts +1 -0
- package/src/schedule/scheduler.ts +178 -127
- package/src/schedule/worker-control.ts +118 -0
- package/src/schedule/worker.ts +131 -0
- package/src/skills/__tests__/categories-cache.test.ts +38 -0
- package/src/skills/categories-cache.ts +40 -5
- package/src/skills/managed-store.ts +41 -6
- package/src/skills/validate-input.ts +47 -0
- package/src/stt/types.ts +5 -9
- package/src/subagent/manager.ts +267 -39
- package/src/subagent/types.ts +6 -2
- package/src/subagent/validate-allowlists.ts +65 -0
- package/src/telemetry/watchdog-direct-emit.test.ts +109 -0
- package/src/telemetry/watchdog-direct-emit.ts +89 -0
- package/src/tools/acp/list-agents.test.ts +0 -6
- package/src/tools/background-tool-registry.ts +61 -0
- package/src/tools/document/document-comment-tool.test.ts +4 -0
- package/src/tools/host-terminal/host-shell.test.ts +397 -0
- package/src/tools/host-terminal/host-shell.ts +203 -14
- package/src/tools/skills/find-similar-skills.test.ts +152 -1
- package/src/tools/skills/find-similar-skills.ts +30 -4
- package/src/tools/skills/load.ts +36 -0
- package/src/tools/skills/scaffold-managed.ts +66 -34
- package/src/tools/skills/skill-script-runner.ts +17 -0
- package/src/tools/skills/skill-tool-factory.ts +5 -1
- package/src/tools/terminal/safe-env.ts +3 -0
- package/src/tools/terminal/shell.test.ts +177 -0
- package/src/tools/terminal/shell.ts +98 -4
- package/src/tools/tool-approval-handler.ts +106 -29
- package/{node_modules/@vellumai/skill-host-contracts/src → src/tools}/tool-types.ts +16 -17
- package/src/tools/types.ts +29 -17
- package/src/tools/ui-surface/definitions.ts +3 -1
- package/src/tts/__tests__/provider-adapters.test.ts +37 -0
- package/src/tts/__tests__/provider-catalog.test.ts +21 -0
- package/src/tts/provider-catalog.ts +40 -9
- package/src/tts/providers/fish-audio-provider.ts +19 -10
- package/src/tts/types.ts +3 -4
- package/src/util/__tests__/process-tree.test.ts +10 -2
- package/src/util/__tests__/text-spacing.test.ts +53 -0
- package/src/util/cgroup-memory.ts +142 -0
- package/src/util/image-conversion.ts +281 -0
- package/src/util/phone.ts +1 -8
- package/src/util/platform.ts +33 -0
- package/src/util/process-tree.ts +10 -2
- package/src/util/text-spacing.ts +53 -0
- package/src/util/worker-process.ts +271 -0
- package/src/workflows/journal-store.ts +16 -1
- package/src/workflows/library.test.ts +35 -20
- package/src/workflows/library.ts +105 -66
- package/src/workspace/git-service.ts +1 -0
- package/src/workspace/migrations/011-backfill-installation-id.ts +83 -31
- package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +41 -22
- package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +1 -1
- package/src/workspace/migrations/118-seed-now-md.ts +64 -0
- package/src/workspace/migrations/119-strip-persisted-memory-v3-tuning-defaults.ts +169 -0
- package/src/workspace/migrations/120-revise-onboarding-threads.ts +47 -0
- package/src/workspace/migrations/121-seed-default-user-guardrails.ts +82 -0
- package/src/workspace/migrations/122-relocate-default-user-boundary.ts +111 -0
- package/src/workspace/migrations/123-swap-quality-profile-to-fable.ts +85 -0
- package/src/workspace/migrations/__tests__/119-strip-persisted-memory-v3-tuning-defaults.test.ts +181 -0
- package/src/workspace/migrations/registry.ts +12 -0
- package/drizzle/0000_dizzy_maggott.sql +0 -301
- package/drizzle/meta/0000_snapshot.json +0 -1933
- package/drizzle/meta/_journal.json +0 -13
- package/node_modules/@vellumai/skill-host-contracts/__tests__/client.test.ts +0 -887
- package/node_modules/@vellumai/skill-host-contracts/bun.lock +0 -24
- package/node_modules/@vellumai/skill-host-contracts/package.json +0 -18
- package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -97
- package/node_modules/@vellumai/skill-host-contracts/src/client.ts +0 -1349
- package/node_modules/@vellumai/skill-host-contracts/src/index.ts +0 -6
- package/node_modules/@vellumai/skill-host-contracts/src/runtime-mode.ts +0 -11
- package/node_modules/@vellumai/skill-host-contracts/src/server-message.ts +0 -32
- package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +0 -333
- package/node_modules/@vellumai/skill-host-contracts/tsconfig.json +0 -20
- package/node_modules/@vellumai/skill-host-contracts/tsconfig.test.json +0 -12
- package/src/__tests__/invite-redemption-service.test.ts +0 -1180
- package/src/__tests__/invite-service-ipc.test.ts +0 -350
- package/src/__tests__/job-handler-registry-guard.test.ts +0 -125
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +0 -203
- package/src/__tests__/relay-server.test.ts +0 -6153
- package/src/__tests__/telephony-stt-routing.test.ts +0 -329
- package/src/__tests__/voice-invite-redemption.test.ts +0 -702
- package/src/calls/relay-server.ts +0 -2463
- package/src/calls/telephony-stt-routing.ts +0 -145
- package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +0 -128
- package/src/daemon/__tests__/daemon-skill-host.test.ts +0 -273
- package/src/daemon/daemon-skill-host.ts +0 -262
- package/src/daemon/message-types/meet.ts +0 -143
- package/src/daemon/shutdown-registry.ts +0 -40
- package/src/ipc/__tests__/skill-server-bidirectional.test.ts +0 -253
- package/src/ipc/__tests__/skill-server.test.ts +0 -182
- package/src/ipc/skill-ipc-types.ts +0 -54
- package/src/ipc/skill-routes/__tests__/config.test.ts +0 -146
- package/src/ipc/skill-routes/__tests__/events-ipc.test.ts +0 -448
- package/src/ipc/skill-routes/__tests__/identity.test.ts +0 -62
- package/src/ipc/skill-routes/__tests__/log.test.ts +0 -133
- package/src/ipc/skill-routes/__tests__/memory.test.ts +0 -203
- package/src/ipc/skill-routes/__tests__/platform.test.ts +0 -111
- package/src/ipc/skill-routes/__tests__/providers.test.ts +0 -265
- package/src/ipc/skill-routes/__tests__/registries.test.ts +0 -400
- package/src/ipc/skill-routes/config.ts +0 -47
- package/src/ipc/skill-routes/events.ts +0 -147
- package/src/ipc/skill-routes/identity.ts +0 -21
- package/src/ipc/skill-routes/index.ts +0 -37
- package/src/ipc/skill-routes/log.ts +0 -40
- package/src/ipc/skill-routes/memory.ts +0 -92
- package/src/ipc/skill-routes/platform.ts +0 -39
- package/src/ipc/skill-routes/providers.ts +0 -162
- package/src/ipc/skill-routes/registries.ts +0 -338
- package/src/ipc/skill-server.ts +0 -831
- package/src/ipc/skill-socket-path.ts +0 -20
- package/src/jobs/register-job-handlers.ts +0 -74
- package/src/persistence/invite-store.ts +0 -481
- package/src/plugins/job-handler-registry.ts +0 -94
- package/src/providers/cache-control.ts +0 -26
- package/src/runtime/__tests__/runtime-mode.test.ts +0 -62
- package/src/runtime/invite-redemption-service.ts +0 -828
- package/src/runtime/invite-redemption-templates.ts +0 -46
- package/src/runtime/runtime-mode.ts +0 -40
- package/src/runtime/slack-dm-text-delivery.ts +0 -177
- package/src/util/voice-code.ts +0 -31
|
@@ -1,828 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Typed invite redemption engine.
|
|
3
|
-
*
|
|
4
|
-
* Wraps the low-level invite store primitives with channel-scoped enforcement
|
|
5
|
-
* and a discriminated-union outcome type so callers can handle every case
|
|
6
|
-
* deterministically. The raw token is accepted as input but is never logged,
|
|
7
|
-
* persisted, or returned in the outcome.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
getInboundTrustVerdict,
|
|
12
|
-
getPhoneCallerVerdict,
|
|
13
|
-
} from "../calls/inbound-trust-reader.js";
|
|
14
|
-
import type { ChannelId } from "../channels/types.js";
|
|
15
|
-
import { findContactChannel, getContact } from "../contacts/contact-store.js";
|
|
16
|
-
import { gatewayContactChannelState } from "../contacts/gateway-channel-read.js";
|
|
17
|
-
import { activateMemberChannel } from "../contacts/member-write-relay.js";
|
|
18
|
-
import type { ChannelStatus, ContactChannel } from "../contacts/types.js";
|
|
19
|
-
import { ipcCallPersistent } from "../ipc/gateway-client.js";
|
|
20
|
-
import {
|
|
21
|
-
findActiveVoiceInvites,
|
|
22
|
-
findByInviteCodeHash,
|
|
23
|
-
findByTokenHash,
|
|
24
|
-
hashToken,
|
|
25
|
-
markInviteExpired,
|
|
26
|
-
recordInviteUse,
|
|
27
|
-
} from "../persistence/invite-store.js";
|
|
28
|
-
import { canonicalizeInboundIdentity } from "../util/canonicalize-identity.js";
|
|
29
|
-
import { getLogger } from "../util/logger.js";
|
|
30
|
-
import { hashVoiceCode } from "../util/voice-code.js";
|
|
31
|
-
import { verdictMemberFromVerdict } from "./trust-verdict-consumer.js";
|
|
32
|
-
|
|
33
|
-
const log = getLogger("invite-redemption-service");
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Resolve the sender's existing member status for the already_member/blocked
|
|
37
|
-
* gate from the gateway trust verdict. Falls back to the gateway-sourced channel
|
|
38
|
-
* status when the verdict is absent or carries no resolvable member status (e.g.
|
|
39
|
-
* an externalChatId-only match or a resolutionFailed verdict), so a blocked
|
|
40
|
-
* contact can't bypass the gate.
|
|
41
|
-
*/
|
|
42
|
-
export async function resolveMemberGateStatus(
|
|
43
|
-
verdict: Awaited<ReturnType<typeof getInboundTrustVerdict>>,
|
|
44
|
-
fallbackStatus: ChannelStatus | null,
|
|
45
|
-
): Promise<ChannelStatus | null> {
|
|
46
|
-
const memberStatus = verdict
|
|
47
|
-
? verdictMemberFromVerdict(verdict)?.status
|
|
48
|
-
: null;
|
|
49
|
-
return memberStatus ?? fallbackStatus;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Gateway-sourced status for an existing local channel, used as the gate-status
|
|
54
|
-
* fallback when the verdict resolves no member. The local row is only located by
|
|
55
|
-
* identity; its status is read from the gateway (ACL source of truth), never the
|
|
56
|
-
* local column.
|
|
57
|
-
*/
|
|
58
|
-
async function gatewayFallbackStatus(
|
|
59
|
-
channel: Pick<ContactChannel, "contactId" | "type" | "address"> | null,
|
|
60
|
-
): Promise<ChannelStatus | null> {
|
|
61
|
-
if (!channel) return null;
|
|
62
|
-
const state = await gatewayContactChannelState(channel);
|
|
63
|
-
return (state?.status as ChannelStatus | undefined) ?? null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
// Gateway lifecycle bridge (shared by all redemption paths)
|
|
68
|
-
// ---------------------------------------------------------------------------
|
|
69
|
-
//
|
|
70
|
-
// The assistant is the authority on token/code → invite resolution (it holds
|
|
71
|
-
// the hashes and caller scoping). Once it has resolved the EXACT invite and
|
|
72
|
-
// passed its own validation, it CLAIMS the gateway-canonical row — BY ID, which
|
|
73
|
-
// is unambiguous — as the AUTHORITATIVE lifecycle gate BEFORE mutating its own
|
|
74
|
-
// DB. `record_invite_redemption` atomically checks status="active" and consumes
|
|
75
|
-
// the row, so there is no check-then-act window: a guardian revoke or a racing
|
|
76
|
-
// redemption that consumes the gateway row first makes this claim fail, and the
|
|
77
|
-
// assistant never mutates. This keeps the gateway invite row the lifecycle
|
|
78
|
-
// source of truth across every redemption path (token + 6-digit channel
|
|
79
|
-
// intercepts, voice relay, HTTP).
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Atomically CLAIM the gateway-canonical invite row for `inviteId`.
|
|
83
|
-
*
|
|
84
|
-
* This is the single authoritative gateway gate, performed BEFORE the assistant
|
|
85
|
-
* mutation. Returns a decision the caller maps to behavior:
|
|
86
|
-
* - "claimed": the gateway row existed and was consumed (active → redeemed) →
|
|
87
|
-
* proceed to the assistant mutation.
|
|
88
|
-
* - "rejected": the gateway row EXISTS but was NOT consumable
|
|
89
|
-
* (revoked / exhausted / expired / already redeemed in a race) → REJECT the
|
|
90
|
-
* redemption WITHOUT mutating the assistant DB.
|
|
91
|
-
* - "legacy": no gateway row exists (pre-migration assistant-only invite) →
|
|
92
|
-
* proceed (the assistant stays authoritative). Logged at warn.
|
|
93
|
-
* - "unavailable": the gateway IPC threw (gateway unreachable). FAIL-OPEN —
|
|
94
|
-
* proceed; the assistant-side checks still apply. Logged loudly.
|
|
95
|
-
*/
|
|
96
|
-
async function claimGatewayRedemption(
|
|
97
|
-
inviteId: string,
|
|
98
|
-
redeemedBy: {
|
|
99
|
-
redeemedByExternalUserId?: string | null;
|
|
100
|
-
redeemedByExternalChatId?: string | null;
|
|
101
|
-
},
|
|
102
|
-
): Promise<"claimed" | "rejected" | "legacy" | "unavailable"> {
|
|
103
|
-
try {
|
|
104
|
-
const res = (await ipcCallPersistent("record_invite_redemption", {
|
|
105
|
-
inviteId,
|
|
106
|
-
redeemedByExternalUserId: redeemedBy.redeemedByExternalUserId ?? null,
|
|
107
|
-
redeemedByExternalChatId: redeemedBy.redeemedByExternalChatId ?? null,
|
|
108
|
-
})) as { ok?: boolean; updated?: boolean; mirrored?: boolean } | null;
|
|
109
|
-
|
|
110
|
-
if (!res || typeof res !== "object") {
|
|
111
|
-
log.warn(
|
|
112
|
-
{ inviteId, res },
|
|
113
|
-
"record_invite_redemption: gateway returned malformed response — failing open",
|
|
114
|
-
);
|
|
115
|
-
return "unavailable";
|
|
116
|
-
}
|
|
117
|
-
if (res.mirrored === false) {
|
|
118
|
-
log.warn(
|
|
119
|
-
{ inviteId },
|
|
120
|
-
"record_invite_redemption: no gateway invite row — treating as legacy assistant-only invite",
|
|
121
|
-
);
|
|
122
|
-
return "legacy";
|
|
123
|
-
}
|
|
124
|
-
// mirrored === true: a gateway row exists. `updated` reflects whether the
|
|
125
|
-
// atomic status="active" claim consumed it.
|
|
126
|
-
return res.updated ? "claimed" : "rejected";
|
|
127
|
-
} catch (err) {
|
|
128
|
-
// FAIL-OPEN: availability > strict gating. The assistant-side validation
|
|
129
|
-
// (status/expiry/use-count/channel) still applies; we just couldn't claim
|
|
130
|
-
// the gateway-canonical row.
|
|
131
|
-
log.error(
|
|
132
|
-
{ err, inviteId },
|
|
133
|
-
"record_invite_redemption: gateway claim unavailable — failing open (assistant-side checks still apply)",
|
|
134
|
-
);
|
|
135
|
-
return "unavailable";
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// ---------------------------------------------------------------------------
|
|
140
|
-
// Outcome type
|
|
141
|
-
// ---------------------------------------------------------------------------
|
|
142
|
-
|
|
143
|
-
export type InviteRedemptionOutcome =
|
|
144
|
-
| { ok: true; type: "redeemed"; memberId: string; inviteId: string }
|
|
145
|
-
| { ok: true; type: "already_member"; memberId: string }
|
|
146
|
-
| {
|
|
147
|
-
ok: false;
|
|
148
|
-
reason:
|
|
149
|
-
| "invalid_token"
|
|
150
|
-
| "expired"
|
|
151
|
-
| "revoked"
|
|
152
|
-
| "max_uses_reached"
|
|
153
|
-
| "channel_mismatch"
|
|
154
|
-
| "missing_identity";
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
// Generic failure reasons for voice redemption — intentionally vague to avoid
|
|
158
|
-
// leaking information about which invites exist or which identity is bound.
|
|
159
|
-
export type VoiceRedemptionOutcome =
|
|
160
|
-
| { ok: true; type: "redeemed"; memberId: string; inviteId: string }
|
|
161
|
-
| { ok: true; type: "already_member"; memberId: string }
|
|
162
|
-
| { ok: false; reason: "invalid_or_expired" };
|
|
163
|
-
|
|
164
|
-
// ---------------------------------------------------------------------------
|
|
165
|
-
// Error-string to typed-reason mapping
|
|
166
|
-
// ---------------------------------------------------------------------------
|
|
167
|
-
|
|
168
|
-
const STORE_ERROR_TO_REASON: Record<
|
|
169
|
-
string,
|
|
170
|
-
(InviteRedemptionOutcome & { ok: false }) | undefined
|
|
171
|
-
> = {
|
|
172
|
-
invite_not_found: { ok: false, reason: "invalid_token" },
|
|
173
|
-
invite_expired: { ok: false, reason: "expired" },
|
|
174
|
-
invite_revoked: { ok: false, reason: "revoked" },
|
|
175
|
-
invite_redeemed: { ok: false, reason: "max_uses_reached" },
|
|
176
|
-
invite_max_uses_reached: { ok: false, reason: "max_uses_reached" },
|
|
177
|
-
invite_channel_mismatch: { ok: false, reason: "channel_mismatch" },
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
// ---------------------------------------------------------------------------
|
|
181
|
-
// redeemInvite
|
|
182
|
-
// ---------------------------------------------------------------------------
|
|
183
|
-
|
|
184
|
-
export async function redeemInvite(params: {
|
|
185
|
-
rawToken: string;
|
|
186
|
-
sourceChannel: string;
|
|
187
|
-
externalUserId?: string;
|
|
188
|
-
externalChatId?: string;
|
|
189
|
-
displayName?: string;
|
|
190
|
-
username?: string;
|
|
191
|
-
assistantId?: string;
|
|
192
|
-
}): Promise<InviteRedemptionOutcome> {
|
|
193
|
-
const {
|
|
194
|
-
rawToken,
|
|
195
|
-
sourceChannel,
|
|
196
|
-
externalUserId,
|
|
197
|
-
externalChatId,
|
|
198
|
-
displayName,
|
|
199
|
-
username,
|
|
200
|
-
} = params;
|
|
201
|
-
|
|
202
|
-
if (!externalUserId && !externalChatId) {
|
|
203
|
-
return { ok: false, reason: "missing_identity" };
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Validate the invite token before any membership checks to prevent
|
|
207
|
-
// membership-status probing with arbitrary tokens.
|
|
208
|
-
const tokenHash = hashToken(rawToken);
|
|
209
|
-
const invite = findByTokenHash(tokenHash);
|
|
210
|
-
|
|
211
|
-
if (!invite) {
|
|
212
|
-
return { ok: false, reason: "invalid_token" };
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (invite.status !== "active") {
|
|
216
|
-
const mapped = STORE_ERROR_TO_REASON[`invite_${invite.status}`];
|
|
217
|
-
if (mapped) return mapped;
|
|
218
|
-
return { ok: false, reason: "invalid_token" };
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (invite.expiresAt <= Date.now()) {
|
|
222
|
-
markInviteExpired(invite.id);
|
|
223
|
-
return { ok: false, reason: "expired" };
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (invite.useCount >= invite.maxUses) {
|
|
227
|
-
return { ok: false, reason: "max_uses_reached" };
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Enforce channel match: the token must belong to the channel the caller
|
|
231
|
-
// is redeeming from.
|
|
232
|
-
if (sourceChannel !== invite.sourceChannel) {
|
|
233
|
-
return { ok: false, reason: "channel_mismatch" };
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Token is valid — now safe to check existing membership without leaking
|
|
237
|
-
// membership status to callers with bogus tokens.
|
|
238
|
-
const canonicalUserId = externalUserId
|
|
239
|
-
? (canonicalizeInboundIdentity(
|
|
240
|
-
sourceChannel as ChannelId,
|
|
241
|
-
externalUserId,
|
|
242
|
-
) ?? externalUserId)
|
|
243
|
-
: undefined;
|
|
244
|
-
const contactResult = findContactChannel({
|
|
245
|
-
channelType: sourceChannel,
|
|
246
|
-
address: canonicalUserId,
|
|
247
|
-
externalChatId: externalChatId,
|
|
248
|
-
});
|
|
249
|
-
const existingChannel = contactResult?.channel ?? null;
|
|
250
|
-
const existingContact = contactResult?.contact ?? null;
|
|
251
|
-
|
|
252
|
-
// If the invite targets a specific contact and the sender's existing channel
|
|
253
|
-
// belongs to a different contact, ignore the existing match — the invite
|
|
254
|
-
// should bind the sender's identity to the target contact, not the existing one.
|
|
255
|
-
const targetMismatch =
|
|
256
|
-
existingContact && existingContact.id !== invite.contactId;
|
|
257
|
-
|
|
258
|
-
const gateStatus = await resolveMemberGateStatus(
|
|
259
|
-
await getInboundTrustVerdict({
|
|
260
|
-
channelType: sourceChannel as ChannelId,
|
|
261
|
-
actorExternalId: canonicalUserId,
|
|
262
|
-
}),
|
|
263
|
-
await gatewayFallbackStatus(existingChannel),
|
|
264
|
-
);
|
|
265
|
-
|
|
266
|
-
if (existingChannel && gateStatus === "active" && !targetMismatch) {
|
|
267
|
-
return { ok: true, type: "already_member", memberId: existingChannel.id };
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Blocked members cannot bypass the guardian's explicit block via invite
|
|
271
|
-
// links. Return the same generic failure as an invalid token to avoid
|
|
272
|
-
// leaking membership status to the caller.
|
|
273
|
-
if (existingChannel && gateStatus === "blocked") {
|
|
274
|
-
return { ok: false, reason: "invalid_token" };
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Authoritative gateway claim (by id — caller-scoped resolution already
|
|
278
|
-
// picked the exact invite). Consume the gateway-canonical row BEFORE mutating
|
|
279
|
-
// the assistant so a concurrent revoke/redemption that consumes it first
|
|
280
|
-
// rejects here, with no assistant mutation to roll back. No use is consumed
|
|
281
|
-
// for the already_member/blocked early returns above.
|
|
282
|
-
if (
|
|
283
|
-
(await claimGatewayRedemption(invite.id, {
|
|
284
|
-
redeemedByExternalUserId: externalUserId,
|
|
285
|
-
redeemedByExternalChatId: externalChatId,
|
|
286
|
-
})) === "rejected"
|
|
287
|
-
) {
|
|
288
|
-
return { ok: false, reason: "invalid_token" };
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Inactive member reactivation: when the user already has a member record in
|
|
292
|
-
// a non-active state (revoked/pending), reactivate it gateway-first then
|
|
293
|
-
// consume an invite use. The fresh-member path below mirrors this.
|
|
294
|
-
if (existingChannel && !targetMismatch) {
|
|
295
|
-
const canonicalMemberId = existingChannel.address;
|
|
296
|
-
const canonicalCallerId = externalUserId
|
|
297
|
-
? canonicalizeInboundIdentity(sourceChannel as ChannelId, externalUserId)
|
|
298
|
-
: null;
|
|
299
|
-
const memberMatchesSender =
|
|
300
|
-
!!canonicalCallerId && canonicalMemberId === canonicalCallerId;
|
|
301
|
-
const preservedDisplayName =
|
|
302
|
-
memberMatchesSender && existingContact?.displayName?.trim().length
|
|
303
|
-
? existingContact.displayName
|
|
304
|
-
: displayName;
|
|
305
|
-
|
|
306
|
-
// Consume the assistant invite use BEFORE activating the member, so a
|
|
307
|
-
// concurrent revoke/exhaustion (recordInviteUse returns false) leaves no
|
|
308
|
-
// active member behind.
|
|
309
|
-
try {
|
|
310
|
-
if (
|
|
311
|
-
!recordInviteUse({
|
|
312
|
-
inviteId: invite.id,
|
|
313
|
-
externalUserId,
|
|
314
|
-
externalChatId,
|
|
315
|
-
})
|
|
316
|
-
) {
|
|
317
|
-
// Invite revoked/expired between pre-validation and write.
|
|
318
|
-
return { ok: false, reason: "invalid_token" };
|
|
319
|
-
}
|
|
320
|
-
} catch (err) {
|
|
321
|
-
// Rare: the gateway claim already consumed the row but the assistant
|
|
322
|
-
// mutation failed — a recoverable wasted gateway use; no cross-process
|
|
323
|
-
// rollback is attempted.
|
|
324
|
-
log.error(
|
|
325
|
-
{ err, inviteId: invite.id },
|
|
326
|
-
"redeemInvite: assistant mutation failed AFTER gateway claim consumed the row (wasted use)",
|
|
327
|
-
);
|
|
328
|
-
throw err;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// Gateway-first: activate the member channel on the authoritative gateway
|
|
332
|
-
// before the assistant DB; the local mirror is best-effort.
|
|
333
|
-
const reactivated = await activateMemberChannel({
|
|
334
|
-
sourceChannel,
|
|
335
|
-
externalUserId,
|
|
336
|
-
externalChatId,
|
|
337
|
-
// Reactivation should not overwrite a guardian-managed nickname.
|
|
338
|
-
displayName: preservedDisplayName,
|
|
339
|
-
username,
|
|
340
|
-
policy: "allow",
|
|
341
|
-
inviteId: invite.id,
|
|
342
|
-
verifiedAt: Date.now(),
|
|
343
|
-
verifiedVia: "invite",
|
|
344
|
-
contactId: invite.contactId,
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
if (reactivated.status === "refused") {
|
|
348
|
-
return { ok: false, reason: "invalid_token" };
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
return {
|
|
352
|
-
ok: true,
|
|
353
|
-
type: "redeemed",
|
|
354
|
-
memberId: reactivated.memberId,
|
|
355
|
-
inviteId: invite.id,
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Fresh member creation: upsert into contacts tables and consume an invite
|
|
360
|
-
// use atomically, mirroring the reactivation path above.
|
|
361
|
-
// When the invite targets a specific contact (targetMismatch path), preserve
|
|
362
|
-
// the target contact's guardian-assigned display name if it has one.
|
|
363
|
-
let freshDisplayName = displayName;
|
|
364
|
-
if (invite.contactId) {
|
|
365
|
-
const targetContact = getContact(invite.contactId);
|
|
366
|
-
if (targetContact?.displayName?.trim().length) {
|
|
367
|
-
freshDisplayName = targetContact.displayName;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Consume the assistant invite use BEFORE activating the member, so a
|
|
372
|
-
// concurrent revoke/exhaustion (recordInviteUse returns false) leaves no
|
|
373
|
-
// active member behind.
|
|
374
|
-
try {
|
|
375
|
-
if (
|
|
376
|
-
!recordInviteUse({ inviteId: invite.id, externalUserId, externalChatId })
|
|
377
|
-
) {
|
|
378
|
-
// Invite revoked/expired between pre-validation and write.
|
|
379
|
-
return { ok: false, reason: "invalid_token" };
|
|
380
|
-
}
|
|
381
|
-
} catch (err) {
|
|
382
|
-
// Rare: gateway claim succeeded but the assistant mutation failed — a
|
|
383
|
-
// recoverable wasted gateway use; no cross-process rollback attempted.
|
|
384
|
-
log.error(
|
|
385
|
-
{ err, inviteId: invite.id },
|
|
386
|
-
"redeemInvite: assistant mutation failed AFTER gateway claim consumed the row (wasted use)",
|
|
387
|
-
);
|
|
388
|
-
throw err;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Gateway-first: activate the member channel on the authoritative gateway
|
|
392
|
-
// before the assistant DB; the local mirror is best-effort.
|
|
393
|
-
const freshResult = await activateMemberChannel({
|
|
394
|
-
sourceChannel,
|
|
395
|
-
externalUserId,
|
|
396
|
-
externalChatId,
|
|
397
|
-
displayName: freshDisplayName,
|
|
398
|
-
username,
|
|
399
|
-
policy: "allow",
|
|
400
|
-
inviteId: invite.id,
|
|
401
|
-
verifiedAt: Date.now(),
|
|
402
|
-
verifiedVia: "invite",
|
|
403
|
-
contactId: invite.contactId,
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
if (freshResult.status === "refused") {
|
|
407
|
-
return { ok: false, reason: "invalid_token" };
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
return {
|
|
411
|
-
ok: true,
|
|
412
|
-
type: "redeemed",
|
|
413
|
-
memberId: freshResult.memberId,
|
|
414
|
-
inviteId: invite.id,
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// ---------------------------------------------------------------------------
|
|
419
|
-
// redeemVoiceInviteCode
|
|
420
|
-
// ---------------------------------------------------------------------------
|
|
421
|
-
|
|
422
|
-
/**
|
|
423
|
-
* Redeem a voice invite code for a caller identified by their E.164 phone number.
|
|
424
|
-
*
|
|
425
|
-
* Unlike token-based redemption, voice redemption:
|
|
426
|
-
* 1. Filters only active voice invites bound to the caller's identity
|
|
427
|
-
* (expectedExternalUserId must match callerExternalUserId).
|
|
428
|
-
* 2. Validates the short numeric code by hashing it and comparing to the
|
|
429
|
-
* stored voiceCodeHash.
|
|
430
|
-
* 3. Enforces expiry and use limits.
|
|
431
|
-
* 4. On success: upserts/reactivates a member with status 'active', policy 'allow'.
|
|
432
|
-
* 5. Consumes one invite use atomically (increment useCount).
|
|
433
|
-
*
|
|
434
|
-
* Failure responses are intentionally generic ("invalid_or_expired") to prevent
|
|
435
|
-
* oracle attacks that could reveal which invites exist or which phone numbers
|
|
436
|
-
* are bound.
|
|
437
|
-
*/
|
|
438
|
-
export async function redeemVoiceInviteCode(params: {
|
|
439
|
-
assistantId?: string;
|
|
440
|
-
callerExternalUserId: string;
|
|
441
|
-
sourceChannel: "phone";
|
|
442
|
-
code: string;
|
|
443
|
-
}): Promise<VoiceRedemptionOutcome> {
|
|
444
|
-
const { callerExternalUserId, code } = params;
|
|
445
|
-
|
|
446
|
-
if (!callerExternalUserId) {
|
|
447
|
-
return { ok: false, reason: "invalid_or_expired" };
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// Find all active voice invites bound to the caller's phone number
|
|
451
|
-
const candidates = findActiveVoiceInvites({
|
|
452
|
-
expectedExternalUserId: callerExternalUserId,
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
if (candidates.length === 0) {
|
|
456
|
-
return { ok: false, reason: "invalid_or_expired" };
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
const codeHash = hashVoiceCode(code);
|
|
460
|
-
const now = Date.now();
|
|
461
|
-
|
|
462
|
-
// Search for a matching invite: code hash match, not expired, uses remaining
|
|
463
|
-
const invite = candidates.find((inv) => {
|
|
464
|
-
if (inv.voiceCodeHash !== codeHash) return false;
|
|
465
|
-
if (inv.expiresAt <= now) return false;
|
|
466
|
-
if (inv.useCount >= inv.maxUses) return false;
|
|
467
|
-
return true;
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
if (!invite) {
|
|
471
|
-
// Mark any expired candidates while we're here
|
|
472
|
-
for (const inv of candidates) {
|
|
473
|
-
if (inv.expiresAt <= now && inv.status === "active") {
|
|
474
|
-
markInviteExpired(inv.id);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
return { ok: false, reason: "invalid_or_expired" };
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// Channel enforcement: voice invites can only be redeemed on the voice channel
|
|
481
|
-
if (invite.sourceChannel !== "phone") {
|
|
482
|
-
return { ok: false, reason: "invalid_or_expired" };
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// Check for existing membership
|
|
486
|
-
const canonicalCallerId =
|
|
487
|
-
canonicalizeInboundIdentity("phone" as ChannelId, callerExternalUserId) ??
|
|
488
|
-
callerExternalUserId;
|
|
489
|
-
const voiceContactResult = findContactChannel({
|
|
490
|
-
channelType: "phone",
|
|
491
|
-
address: canonicalCallerId,
|
|
492
|
-
});
|
|
493
|
-
const existingVoiceChannel = voiceContactResult?.channel ?? null;
|
|
494
|
-
const voiceContact = voiceContactResult?.contact ?? null;
|
|
495
|
-
|
|
496
|
-
// If the invite targets a specific contact and the sender's existing channel
|
|
497
|
-
// belongs to a different contact, ignore the existing match — the invite
|
|
498
|
-
// should bind the sender's identity to the target contact, not the existing one.
|
|
499
|
-
const targetMismatch = voiceContact && voiceContact.id !== invite.contactId;
|
|
500
|
-
|
|
501
|
-
const gateStatus = await resolveMemberGateStatus(
|
|
502
|
-
await getPhoneCallerVerdict(canonicalCallerId),
|
|
503
|
-
await gatewayFallbackStatus(existingVoiceChannel),
|
|
504
|
-
);
|
|
505
|
-
|
|
506
|
-
if (existingVoiceChannel && gateStatus === "active" && !targetMismatch) {
|
|
507
|
-
return {
|
|
508
|
-
ok: true,
|
|
509
|
-
type: "already_member",
|
|
510
|
-
memberId: existingVoiceChannel.id,
|
|
511
|
-
};
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// Blocked members cannot bypass the guardian's explicit block
|
|
515
|
-
if (existingVoiceChannel && gateStatus === "blocked") {
|
|
516
|
-
return { ok: false, reason: "invalid_or_expired" };
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// Authoritative gateway claim (by id). The candidate was already resolved
|
|
520
|
-
// caller-scoped (expectedExternalUserId == caller), so the id is unambiguous
|
|
521
|
-
// even when two phone invites share a 6-digit code — this closes the
|
|
522
|
-
// colliding-code gap that a code-hash-only gateway gate left open. Consuming
|
|
523
|
-
// the gateway row here, before the assistant mutation, closes the TOCTOU
|
|
524
|
-
// window: a concurrent revoke/redemption rejects with no assistant mutation.
|
|
525
|
-
if (
|
|
526
|
-
(await claimGatewayRedemption(invite.id, {
|
|
527
|
-
redeemedByExternalUserId: callerExternalUserId,
|
|
528
|
-
})) === "rejected"
|
|
529
|
-
) {
|
|
530
|
-
return { ok: false, reason: "invalid_or_expired" };
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// When the invite targets a specific contact, preserve the target contact's
|
|
534
|
-
// guardian-assigned display name if it has one.
|
|
535
|
-
let preservedDisplayName = voiceContact?.displayName?.trim().length
|
|
536
|
-
? voiceContact.displayName
|
|
537
|
-
: (invite.friendName ?? undefined);
|
|
538
|
-
if (invite.contactId) {
|
|
539
|
-
const targetContact = getContact(invite.contactId);
|
|
540
|
-
if (targetContact?.displayName?.trim().length) {
|
|
541
|
-
preservedDisplayName = targetContact.displayName;
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// Consume the assistant invite use BEFORE activating the member, so a
|
|
546
|
-
// concurrent revoke/exhaustion (recordInviteUse returns false) leaves no
|
|
547
|
-
// active member behind.
|
|
548
|
-
try {
|
|
549
|
-
if (
|
|
550
|
-
!recordInviteUse({
|
|
551
|
-
inviteId: invite.id,
|
|
552
|
-
externalUserId: callerExternalUserId,
|
|
553
|
-
})
|
|
554
|
-
) {
|
|
555
|
-
// Invite revoked/expired between pre-validation and write.
|
|
556
|
-
return { ok: false, reason: "invalid_or_expired" };
|
|
557
|
-
}
|
|
558
|
-
} catch (err) {
|
|
559
|
-
// Rare: gateway claim succeeded but the assistant mutation failed — a
|
|
560
|
-
// recoverable wasted gateway use; no cross-process rollback attempted.
|
|
561
|
-
log.error(
|
|
562
|
-
{ err, inviteId: invite.id },
|
|
563
|
-
"redeemVoiceInviteCode: assistant mutation failed AFTER gateway claim consumed the row (wasted use)",
|
|
564
|
-
);
|
|
565
|
-
throw err;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// Gateway-first: activate the member channel on the authoritative gateway
|
|
569
|
-
// before the assistant DB; the local mirror is best-effort.
|
|
570
|
-
const writeResult = await activateMemberChannel({
|
|
571
|
-
sourceChannel: "phone",
|
|
572
|
-
externalUserId: callerExternalUserId,
|
|
573
|
-
externalChatId: callerExternalUserId,
|
|
574
|
-
displayName: preservedDisplayName,
|
|
575
|
-
policy: "allow",
|
|
576
|
-
inviteId: invite.id,
|
|
577
|
-
verifiedAt: Date.now(),
|
|
578
|
-
verifiedVia: "invite",
|
|
579
|
-
contactId: invite.contactId,
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
if (writeResult.status === "refused") {
|
|
583
|
-
return { ok: false, reason: "invalid_or_expired" };
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
return {
|
|
587
|
-
ok: true,
|
|
588
|
-
type: "redeemed",
|
|
589
|
-
memberId: writeResult.memberId,
|
|
590
|
-
inviteId: invite.id,
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
// ---------------------------------------------------------------------------
|
|
595
|
-
// redeemInviteByCode
|
|
596
|
-
// ---------------------------------------------------------------------------
|
|
597
|
-
|
|
598
|
-
/**
|
|
599
|
-
* Redeem an invite using a 6-digit invite code (channel-agnostic).
|
|
600
|
-
*
|
|
601
|
-
* Unlike token-based redemption which uses deep links, code redemption works
|
|
602
|
-
* by intercepting bare 6-digit messages on channels with codeRedemptionEnabled.
|
|
603
|
-
* The code is hashed and looked up via `findByInviteCodeHash`.
|
|
604
|
-
*
|
|
605
|
-
* Validation: active status, not expired, uses remaining, channel match.
|
|
606
|
-
* On success: upserts/reactivates a member with status 'active', policy 'allow'.
|
|
607
|
-
*/
|
|
608
|
-
export async function redeemInviteByCode(params: {
|
|
609
|
-
code: string;
|
|
610
|
-
sourceChannel: string;
|
|
611
|
-
externalUserId?: string;
|
|
612
|
-
externalChatId?: string;
|
|
613
|
-
displayName?: string;
|
|
614
|
-
username?: string;
|
|
615
|
-
assistantId?: string;
|
|
616
|
-
}): Promise<InviteRedemptionOutcome> {
|
|
617
|
-
const {
|
|
618
|
-
code,
|
|
619
|
-
sourceChannel,
|
|
620
|
-
externalUserId,
|
|
621
|
-
externalChatId,
|
|
622
|
-
displayName,
|
|
623
|
-
username,
|
|
624
|
-
} = params;
|
|
625
|
-
|
|
626
|
-
if (!externalUserId && !externalChatId) {
|
|
627
|
-
return { ok: false, reason: "missing_identity" };
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
const codeHash = hashVoiceCode(code);
|
|
631
|
-
const invite = findByInviteCodeHash(codeHash, sourceChannel);
|
|
632
|
-
|
|
633
|
-
if (!invite) {
|
|
634
|
-
return { ok: false, reason: "invalid_token" };
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
if (invite.status !== "active") {
|
|
638
|
-
const mapped = STORE_ERROR_TO_REASON[`invite_${invite.status}`];
|
|
639
|
-
if (mapped) return mapped;
|
|
640
|
-
return { ok: false, reason: "invalid_token" };
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
if (invite.expiresAt <= Date.now()) {
|
|
644
|
-
markInviteExpired(invite.id);
|
|
645
|
-
return { ok: false, reason: "expired" };
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
if (invite.useCount >= invite.maxUses) {
|
|
649
|
-
return { ok: false, reason: "max_uses_reached" };
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
// Code is valid — now safe to check existing membership without leaking
|
|
653
|
-
// membership status to callers with bogus codes.
|
|
654
|
-
const canonicalUserId = externalUserId
|
|
655
|
-
? (canonicalizeInboundIdentity(
|
|
656
|
-
sourceChannel as ChannelId,
|
|
657
|
-
externalUserId,
|
|
658
|
-
) ?? externalUserId)
|
|
659
|
-
: undefined;
|
|
660
|
-
const contactResult = findContactChannel({
|
|
661
|
-
channelType: sourceChannel,
|
|
662
|
-
address: canonicalUserId,
|
|
663
|
-
externalChatId: externalChatId,
|
|
664
|
-
});
|
|
665
|
-
const existingChannel = contactResult?.channel ?? null;
|
|
666
|
-
const existingContact = contactResult?.contact ?? null;
|
|
667
|
-
|
|
668
|
-
// If the invite targets a specific contact and the sender's existing channel
|
|
669
|
-
// belongs to a different contact, ignore the existing match — the invite
|
|
670
|
-
// should bind the sender's identity to the target contact, not the existing one.
|
|
671
|
-
const targetMismatch =
|
|
672
|
-
existingContact && existingContact.id !== invite.contactId;
|
|
673
|
-
|
|
674
|
-
const gateStatus = await resolveMemberGateStatus(
|
|
675
|
-
await getInboundTrustVerdict({
|
|
676
|
-
channelType: sourceChannel as ChannelId,
|
|
677
|
-
actorExternalId: canonicalUserId,
|
|
678
|
-
}),
|
|
679
|
-
await gatewayFallbackStatus(existingChannel),
|
|
680
|
-
);
|
|
681
|
-
|
|
682
|
-
if (existingChannel && gateStatus === "active" && !targetMismatch) {
|
|
683
|
-
return { ok: true, type: "already_member", memberId: existingChannel.id };
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
// Blocked members cannot bypass the guardian's explicit block via invite
|
|
687
|
-
// codes. Return the same generic failure as an invalid token to avoid
|
|
688
|
-
// leaking membership status to the caller.
|
|
689
|
-
if (existingChannel && gateStatus === "blocked") {
|
|
690
|
-
return { ok: false, reason: "invalid_token" };
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
// Authoritative gateway claim (by id). Consume the gateway-canonical row
|
|
694
|
-
// before mutating the assistant, so a concurrent revoke/redemption that
|
|
695
|
-
// consumes it first rejects here with no assistant mutation to roll back. No
|
|
696
|
-
// use is consumed for the already_member/blocked early returns above.
|
|
697
|
-
if (
|
|
698
|
-
(await claimGatewayRedemption(invite.id, {
|
|
699
|
-
redeemedByExternalUserId: externalUserId,
|
|
700
|
-
redeemedByExternalChatId: externalChatId,
|
|
701
|
-
})) === "rejected"
|
|
702
|
-
) {
|
|
703
|
-
return { ok: false, reason: "invalid_token" };
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
// Inactive member reactivation: reactivate gateway-first, then consume an
|
|
707
|
-
// invite use.
|
|
708
|
-
if (existingChannel && !targetMismatch) {
|
|
709
|
-
const canonicalMemberId = existingChannel.address;
|
|
710
|
-
const canonicalCallerId = externalUserId
|
|
711
|
-
? canonicalizeInboundIdentity(sourceChannel as ChannelId, externalUserId)
|
|
712
|
-
: null;
|
|
713
|
-
const memberMatchesSender =
|
|
714
|
-
!!canonicalCallerId && canonicalMemberId === canonicalCallerId;
|
|
715
|
-
const preservedDisplayName =
|
|
716
|
-
memberMatchesSender && existingContact?.displayName?.trim().length
|
|
717
|
-
? existingContact.displayName
|
|
718
|
-
: displayName;
|
|
719
|
-
|
|
720
|
-
// Consume the assistant invite use BEFORE activating the member, so a
|
|
721
|
-
// concurrent revoke/exhaustion (recordInviteUse returns false) leaves no
|
|
722
|
-
// active member behind.
|
|
723
|
-
try {
|
|
724
|
-
if (
|
|
725
|
-
!recordInviteUse({
|
|
726
|
-
inviteId: invite.id,
|
|
727
|
-
externalUserId,
|
|
728
|
-
externalChatId,
|
|
729
|
-
})
|
|
730
|
-
) {
|
|
731
|
-
// Invite revoked/expired between pre-validation and write.
|
|
732
|
-
return { ok: false, reason: "invalid_token" };
|
|
733
|
-
}
|
|
734
|
-
} catch (err) {
|
|
735
|
-
// Rare: gateway claim succeeded but the assistant mutation failed — a
|
|
736
|
-
// recoverable wasted gateway use; no cross-process rollback attempted.
|
|
737
|
-
log.error(
|
|
738
|
-
{ err, inviteId: invite.id },
|
|
739
|
-
"redeemInviteByCode: assistant mutation failed AFTER gateway claim consumed the row (wasted use)",
|
|
740
|
-
);
|
|
741
|
-
throw err;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
// Gateway-first: activate the member channel on the authoritative gateway
|
|
745
|
-
// before the assistant DB; the local mirror is best-effort.
|
|
746
|
-
const reactivated = await activateMemberChannel({
|
|
747
|
-
sourceChannel,
|
|
748
|
-
externalUserId,
|
|
749
|
-
externalChatId,
|
|
750
|
-
displayName: preservedDisplayName,
|
|
751
|
-
username,
|
|
752
|
-
policy: "allow",
|
|
753
|
-
inviteId: invite.id,
|
|
754
|
-
verifiedAt: Date.now(),
|
|
755
|
-
verifiedVia: "invite",
|
|
756
|
-
contactId: invite.contactId,
|
|
757
|
-
});
|
|
758
|
-
|
|
759
|
-
if (reactivated.status === "refused") {
|
|
760
|
-
return { ok: false, reason: "invalid_token" };
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
return {
|
|
764
|
-
ok: true,
|
|
765
|
-
type: "redeemed",
|
|
766
|
-
memberId: reactivated.memberId,
|
|
767
|
-
inviteId: invite.id,
|
|
768
|
-
};
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
// Fresh member creation: upsert into contacts tables and consume an invite
|
|
772
|
-
// use atomically.
|
|
773
|
-
// When the invite targets a specific contact (targetMismatch path), preserve
|
|
774
|
-
// the target contact's guardian-assigned display name if it has one.
|
|
775
|
-
let freshDisplayName = displayName;
|
|
776
|
-
if (invite.contactId) {
|
|
777
|
-
const targetContact = getContact(invite.contactId);
|
|
778
|
-
if (targetContact?.displayName?.trim().length) {
|
|
779
|
-
freshDisplayName = targetContact.displayName;
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
// Consume the assistant invite use BEFORE activating the member, so a
|
|
784
|
-
// concurrent revoke/exhaustion (recordInviteUse returns false) leaves no
|
|
785
|
-
// active member behind.
|
|
786
|
-
try {
|
|
787
|
-
if (
|
|
788
|
-
!recordInviteUse({ inviteId: invite.id, externalUserId, externalChatId })
|
|
789
|
-
) {
|
|
790
|
-
// Invite revoked/expired between pre-validation and write.
|
|
791
|
-
return { ok: false, reason: "invalid_token" };
|
|
792
|
-
}
|
|
793
|
-
} catch (err) {
|
|
794
|
-
// Rare: gateway claim succeeded but the assistant mutation failed — a
|
|
795
|
-
// recoverable wasted gateway use; no cross-process rollback attempted.
|
|
796
|
-
log.error(
|
|
797
|
-
{ err, inviteId: invite.id },
|
|
798
|
-
"redeemInviteByCode: assistant mutation failed AFTER gateway claim consumed the row (wasted use)",
|
|
799
|
-
);
|
|
800
|
-
throw err;
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
// Gateway-first: activate the member channel on the authoritative gateway
|
|
804
|
-
// before the assistant DB; the local mirror is best-effort.
|
|
805
|
-
const freshResult = await activateMemberChannel({
|
|
806
|
-
sourceChannel,
|
|
807
|
-
externalUserId,
|
|
808
|
-
externalChatId,
|
|
809
|
-
displayName: freshDisplayName,
|
|
810
|
-
username,
|
|
811
|
-
policy: "allow",
|
|
812
|
-
inviteId: invite.id,
|
|
813
|
-
verifiedAt: Date.now(),
|
|
814
|
-
verifiedVia: "invite",
|
|
815
|
-
contactId: invite.contactId,
|
|
816
|
-
});
|
|
817
|
-
|
|
818
|
-
if (freshResult.status === "refused") {
|
|
819
|
-
return { ok: false, reason: "invalid_token" };
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
return {
|
|
823
|
-
ok: true,
|
|
824
|
-
type: "redeemed",
|
|
825
|
-
memberId: freshResult.memberId,
|
|
826
|
-
inviteId: invite.id,
|
|
827
|
-
};
|
|
828
|
-
}
|