@vellumai/assistant 0.3.5 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -0
- package/eslint.config.mjs +31 -0
- package/package.json +1 -1
- package/scripts/ipc/check-swift-decoder-drift.ts +4 -1
- package/scripts/ipc/generate-swift.ts +18 -2
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +338 -1
- package/src/__tests__/approval-conversation-turn.test.ts +214 -0
- package/src/__tests__/browser-manager.test.ts +1 -0
- package/src/__tests__/call-conversation-messages.test.ts +130 -0
- package/src/__tests__/call-orchestrator.test.ts +752 -271
- package/src/__tests__/call-pointer-messages.test.ts +148 -0
- package/src/__tests__/call-recovery.test.ts +3 -0
- package/src/__tests__/call-routes-http.test.ts +5 -0
- package/src/__tests__/call-store.test.ts +3 -0
- package/src/__tests__/channel-approval-routes.test.ts +1260 -85
- package/src/__tests__/channel-approval.test.ts +37 -0
- package/src/__tests__/channel-approvals.test.ts +4 -65
- package/src/__tests__/channel-guardian.test.ts +556 -0
- package/src/__tests__/channel-readiness-service.test.ts +74 -7
- package/src/__tests__/checker.test.ts +14 -7
- package/src/__tests__/clarification-resolver.test.ts +44 -24
- package/src/__tests__/commit-message-enrichment-service.test.ts +9 -4
- package/src/__tests__/computer-use-session-working-dir.test.ts +8 -0
- package/src/__tests__/config-schema.test.ts +12 -7
- package/src/__tests__/context-window-manager.test.ts +30 -2
- package/src/__tests__/contradiction-checker.test.ts +20 -5
- package/src/__tests__/credential-security-invariants.test.ts +6 -2
- package/src/__tests__/db-migration-rollback.test.ts +752 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +2 -0
- package/src/__tests__/fuzzy-match-property.test.ts +5 -5
- package/src/__tests__/guardian-action-store.test.ts +123 -0
- package/src/__tests__/guardian-action-sweep.test.ts +277 -0
- package/src/__tests__/guardian-dispatch.test.ts +389 -0
- package/src/__tests__/guardian-question-copy.test.ts +47 -0
- package/src/__tests__/handlers-telegram-config.test.ts +4 -2
- package/src/__tests__/handlers-twilio-config.test.ts +126 -0
- package/src/__tests__/intent-routing.test.ts +2 -0
- package/src/__tests__/ipc-snapshot.test.ts +228 -1
- package/src/__tests__/memory-upsert-concurrency.test.ts +828 -0
- package/src/__tests__/model-intents.test.ts +96 -0
- package/src/__tests__/no-direct-anthropic-sdk-imports.test.ts +42 -0
- package/src/__tests__/oauth2-gateway-transport.test.ts +130 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +2 -0
- package/src/__tests__/provider-commit-message-generator.test.ts +89 -13
- package/src/__tests__/provider-error-scenarios.test.ts +621 -0
- package/src/__tests__/provider-fail-open-selection.test.ts +119 -0
- package/src/__tests__/qdrant-manager.test.ts +27 -20
- package/src/__tests__/relay-server.test.ts +779 -40
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +2 -0
- package/src/__tests__/run-orchestrator.test.ts +20 -4
- package/src/__tests__/runtime-runs-http.test.ts +17 -1
- package/src/__tests__/runtime-runs.test.ts +16 -0
- package/src/__tests__/schedule-store.test.ts +18 -4
- package/src/__tests__/scheduler-recurrence.test.ts +13 -4
- package/src/__tests__/session-abort-tool-results.test.ts +6 -0
- package/src/__tests__/session-agent-loop.test.ts +857 -0
- package/src/__tests__/session-conflict-gate.test.ts +6 -0
- package/src/__tests__/session-pre-run-repair.test.ts +6 -0
- package/src/__tests__/session-profile-injection.test.ts +6 -0
- package/src/__tests__/session-provider-retry-repair.test.ts +6 -0
- package/src/__tests__/session-queue.test.ts +6 -0
- package/src/__tests__/session-runtime-assembly.test.ts +237 -13
- package/src/__tests__/session-slash-known.test.ts +6 -0
- package/src/__tests__/session-slash-queue.test.ts +6 -0
- package/src/__tests__/session-slash-unknown.test.ts +6 -0
- package/src/__tests__/session-surfaces-task-progress.test.ts +2 -0
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
- package/src/__tests__/session-workspace-injection.test.ts +6 -0
- package/src/__tests__/session-workspace-tool-tracking.test.ts +6 -0
- package/src/__tests__/skills.test.ts +2 -0
- package/src/__tests__/sms-messaging-provider.test.ts +2 -1
- package/src/__tests__/starter-task-flow.test.ts +2 -0
- package/src/__tests__/swarm-dag-pathological.test.ts +535 -0
- package/src/__tests__/system-prompt.test.ts +2 -0
- package/src/__tests__/task-management-tools.test.ts +2 -2
- package/src/__tests__/task-runner.test.ts +14 -4
- package/src/__tests__/terminal-tools.test.ts +25 -19
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +545 -0
- package/src/__tests__/tool-executor-shell-integration.test.ts +11 -11
- package/src/__tests__/tool-executor.test.ts +23 -24
- package/src/__tests__/trust-store.test.ts +3 -3
- package/src/__tests__/twilio-rest.test.ts +29 -0
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +3 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +11 -0
- package/src/__tests__/twilio-routes.test.ts +141 -21
- package/src/__tests__/user-reference.test.ts +2 -0
- package/src/__tests__/voice-quality.test.ts +222 -0
- package/src/__tests__/web-search.test.ts +45 -29
- package/src/agent/loop.ts +1 -1
- package/src/agent-heartbeat/agent-heartbeat-service.ts +2 -10
- package/src/amazon/client.ts +1418 -0
- package/src/amazon/request-extractor.ts +135 -0
- package/src/amazon/session.ts +109 -0
- package/src/autonomy/autonomy-store.ts +5 -5
- package/src/browser-extension-relay/client.ts +124 -0
- package/src/browser-extension-relay/protocol.ts +63 -0
- package/src/browser-extension-relay/server.ts +177 -0
- package/src/bundler/app-bundler.ts +3 -3
- package/src/bundler/bundle-signer.ts +1 -1
- package/src/bundler/signature-verifier.ts +1 -1
- package/src/calls/call-conversation-messages.ts +33 -0
- package/src/calls/call-domain.ts +106 -5
- package/src/calls/call-orchestrator.ts +252 -54
- package/src/calls/call-pointer-messages.ts +53 -0
- package/src/calls/call-recovery.ts +3 -8
- package/src/calls/call-store.ts +69 -87
- package/src/calls/elevenlabs-config.ts +3 -2
- package/src/calls/guardian-action-sweep.ts +105 -0
- package/src/calls/guardian-dispatch.ts +203 -0
- package/src/calls/guardian-question-copy.ts +133 -0
- package/src/calls/relay-server.ts +466 -8
- package/src/calls/speaker-identification.ts +1 -1
- package/src/calls/twilio-config.ts +7 -5
- package/src/calls/twilio-provider.ts +6 -4
- package/src/calls/twilio-rest.ts +40 -15
- package/src/calls/twilio-routes.ts +60 -45
- package/src/calls/types.ts +3 -1
- package/src/channels/types.ts +25 -0
- package/src/cli/amazon.ts +815 -0
- package/src/cli/config-commands.ts +2 -2
- package/src/cli/core-commands.ts +4 -3
- package/src/cli/influencer.ts +244 -0
- package/src/cli/map.ts +89 -6
- package/src/cli.ts +1 -1
- package/src/config/agent-schema.ts +171 -0
- package/src/config/bundled-skills/amazon/SKILL.md +127 -0
- package/src/config/bundled-skills/amazon/icon.svg +13 -0
- package/src/config/bundled-skills/api-mapping/SKILL.md +78 -0
- package/src/config/bundled-skills/browser/SKILL.md +1 -0
- package/src/config/bundled-skills/browser/TOOLS.json +17 -0
- package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +25 -0
- package/src/config/bundled-skills/doordash/SKILL.md +51 -51
- package/src/config/bundled-skills/email-setup/SKILL.md +14 -5
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +183 -0
- package/src/config/bundled-skills/influencer/SKILL.md +144 -0
- package/src/config/bundled-skills/macos-automation/icon.svg +12 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +72 -95
- package/src/config/bundled-skills/media-processing/TOOLS.json +57 -147
- package/src/config/bundled-skills/media-processing/__tests__/concurrency-pool.test.ts +77 -0
- package/src/config/bundled-skills/media-processing/__tests__/cost-tracker.test.ts +69 -0
- package/src/config/bundled-skills/media-processing/__tests__/preprocess.test.ts +303 -0
- package/src/config/bundled-skills/media-processing/services/concurrency-pool.ts +55 -0
- package/src/config/bundled-skills/media-processing/services/cost-tracker.ts +86 -0
- package/src/config/bundled-skills/media-processing/services/gemini-map.ts +339 -0
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +551 -0
- package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +7 -9
- package/src/config/bundled-skills/media-processing/services/reduce.ts +197 -0
- package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +88 -253
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +22 -153
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -2
- package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +28 -51
- package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +35 -270
- package/src/config/bundled-skills/messaging/SKILL.md +12 -2
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -7
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +2 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +86 -21
- package/src/config/bundled-skills/twitter/icon.svg +14 -0
- package/src/config/bundled-tool-registry.ts +310 -0
- package/src/config/calls-schema.ts +181 -0
- package/src/config/core-schema.ts +309 -0
- package/src/config/defaults.ts +26 -2
- package/src/config/env-registry.ts +162 -0
- package/src/config/env.ts +175 -0
- package/src/config/loader.ts +6 -6
- package/src/config/memory-schema.ts +528 -0
- package/src/config/sandbox-schema.ts +55 -0
- package/src/config/schema.ts +156 -1137
- package/src/config/skill-state.ts +1 -1
- package/src/config/skills-schema.ts +32 -0
- package/src/config/skills.ts +35 -24
- package/src/config/system-prompt.ts +107 -56
- package/src/config/templates/SOUL.md +1 -1
- package/src/config/types.ts +1 -0
- package/src/config/user-reference.ts +4 -9
- package/src/config/vellum-skills/catalog.json +0 -7
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +5 -1
- package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +1 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +112 -14
- package/src/context/window-manager.ts +27 -7
- package/src/daemon/approval-generators.ts +186 -0
- package/src/daemon/approved-devices-store.ts +140 -0
- package/src/daemon/assistant-attachments.ts +1 -1
- package/src/daemon/classifier.ts +35 -32
- package/src/daemon/config-watcher.ts +1 -1
- package/src/daemon/daemon-control.ts +217 -0
- package/src/daemon/handlers/apps.ts +2 -3
- package/src/daemon/handlers/config-channels.ts +158 -0
- package/src/daemon/handlers/config-inbox.ts +540 -0
- package/src/daemon/handlers/config-ingress.ts +231 -0
- package/src/daemon/handlers/config-integrations.ts +258 -0
- package/src/daemon/handlers/config-model.ts +143 -0
- package/src/daemon/handlers/config-parental.ts +163 -0
- package/src/daemon/handlers/config-scheduling.ts +172 -0
- package/src/daemon/handlers/config-slack.ts +92 -0
- package/src/daemon/handlers/config-telegram.ts +301 -0
- package/src/daemon/handlers/config-tools.ts +177 -0
- package/src/daemon/handlers/config-trust.ts +104 -0
- package/src/daemon/handlers/config-twilio.ts +1080 -0
- package/src/daemon/handlers/config.ts +53 -2463
- package/src/daemon/handlers/diagnostics.ts +1 -1
- package/src/daemon/handlers/dictation.ts +4 -6
- package/src/daemon/handlers/documents.ts +18 -32
- package/src/daemon/handlers/index.ts +9 -0
- package/src/daemon/handlers/misc.ts +3 -5
- package/src/daemon/handlers/pairing.ts +98 -0
- package/src/daemon/handlers/sessions.ts +54 -5
- package/src/daemon/handlers/shared.ts +3 -1
- package/src/daemon/handlers/skills.ts +1 -1
- package/src/daemon/handlers/twitter-auth.ts +2 -0
- package/src/daemon/handlers/work-items.ts +2 -2
- package/src/daemon/handlers/workspace-files.ts +4 -3
- package/src/daemon/install-cli-launchers.ts +113 -0
- package/src/daemon/ipc-contract/apps.ts +356 -0
- package/src/daemon/ipc-contract/browser.ts +74 -0
- package/src/daemon/ipc-contract/computer-use.ts +151 -0
- package/src/daemon/ipc-contract/diagnostics.ts +56 -0
- package/src/daemon/ipc-contract/documents.ts +74 -0
- package/src/daemon/ipc-contract/inbox.ts +209 -0
- package/src/daemon/ipc-contract/integrations.ts +284 -0
- package/src/daemon/ipc-contract/memory.ts +48 -0
- package/src/daemon/ipc-contract/messages.ts +211 -0
- package/src/daemon/ipc-contract/pairing.ts +45 -0
- package/src/daemon/ipc-contract/parental-control.ts +95 -0
- package/src/daemon/ipc-contract/schedules.ts +97 -0
- package/src/daemon/ipc-contract/sessions.ts +315 -0
- package/src/daemon/ipc-contract/shared.ts +42 -0
- package/src/daemon/ipc-contract/skills.ts +120 -0
- package/src/daemon/ipc-contract/subagents.ts +58 -0
- package/src/daemon/ipc-contract/surfaces.ts +250 -0
- package/src/daemon/ipc-contract/trust.ts +60 -0
- package/src/daemon/ipc-contract/work-items.ts +225 -0
- package/src/daemon/ipc-contract/workspace.ts +113 -0
- package/src/daemon/ipc-contract-inventory.json +60 -0
- package/src/daemon/ipc-contract-inventory.ts +55 -29
- package/src/daemon/ipc-contract.ts +226 -2527
- package/src/daemon/ipc-protocol.ts +1 -1
- package/src/daemon/ipc-validate.ts +7 -0
- package/src/daemon/lifecycle.ts +97 -379
- package/src/daemon/pairing-store.ts +177 -0
- package/src/daemon/providers-setup.ts +43 -0
- package/src/daemon/ride-shotgun-handler.ts +67 -2
- package/src/daemon/server.ts +60 -44
- package/src/daemon/session-agent-loop-handlers.ts +421 -0
- package/src/daemon/session-agent-loop.ts +113 -275
- package/src/daemon/session-dynamic-profile.ts +1 -1
- package/src/daemon/session-history.ts +1 -1
- package/src/daemon/session-media-retry.ts +1 -1
- package/src/daemon/session-messaging.ts +37 -2
- package/src/daemon/session-notifiers.ts +5 -25
- package/src/daemon/session-process.ts +99 -59
- package/src/daemon/session-queue-manager.ts +96 -4
- package/src/daemon/session-runtime-assembly.ts +149 -15
- package/src/daemon/session-surfaces.ts +19 -4
- package/src/daemon/session-tool-setup.ts +28 -30
- package/src/daemon/session-workspace.ts +1 -1
- package/src/daemon/session.ts +24 -1
- package/src/daemon/shutdown-handlers.ts +122 -0
- package/src/daemon/trace-emitter.ts +1 -1
- package/src/daemon/watch-handler.ts +36 -33
- package/src/doordash/cart-queries.ts +787 -0
- package/src/doordash/client.ts +144 -127
- package/src/doordash/order-queries.ts +85 -0
- package/src/doordash/queries.ts +10 -1308
- package/src/doordash/search-queries.ts +203 -0
- package/src/doordash/session.ts +3 -2
- package/src/doordash/store-queries.ts +246 -0
- package/src/doordash/types.ts +367 -0
- package/src/email/providers/agentmail.ts +2 -1
- package/src/email/providers/index.ts +3 -2
- package/src/email/service.ts +3 -2
- package/src/errors.ts +43 -0
- package/src/home-base/prebuilt/seed.ts +1 -1
- package/src/hooks/cli.ts +6 -5
- package/src/hooks/config.ts +6 -8
- package/src/hooks/discovery.ts +6 -5
- package/src/hooks/manager.ts +4 -3
- package/src/hooks/runner.ts +2 -2
- package/src/hooks/templates.ts +5 -5
- package/src/inbound/public-ingress-urls.ts +3 -1
- package/src/index.ts +4 -2
- package/src/influencer/client.ts +1104 -0
- package/src/instrument.ts +4 -3
- package/src/logfire.ts +4 -3
- package/src/memory/admin.ts +25 -35
- package/src/memory/attachments-store.ts +4 -7
- package/src/memory/channel-delivery-store.ts +30 -1
- package/src/memory/channel-guardian-store.ts +200 -1
- package/src/memory/clarification-resolver.ts +37 -33
- package/src/memory/conflict-store.ts +67 -61
- package/src/memory/contradiction-checker.ts +141 -117
- package/src/memory/conversation-store.ts +335 -51
- package/src/memory/db-connection.ts +27 -4
- package/src/memory/db-init.ts +121 -4
- package/src/memory/db.ts +14 -1
- package/src/memory/embedding-backend.ts +27 -5
- package/src/memory/embedding-ollama.ts +2 -1
- package/src/memory/entity-extractor.ts +38 -35
- package/src/memory/guardian-action-store.ts +430 -0
- package/src/memory/inbox-escalation-projection.ts +59 -0
- package/src/memory/inbox-thread-store.ts +218 -0
- package/src/memory/ingress-invite-store.ts +338 -0
- package/src/memory/ingress-member-store.ts +350 -0
- package/src/memory/items-extractor.ts +91 -97
- package/src/memory/job-handlers/index-maintenance.ts +3 -3
- package/src/memory/job-handlers/media-processing.ts +11 -42
- package/src/memory/job-handlers/summarization.ts +32 -26
- package/src/memory/job-utils.ts +3 -10
- package/src/memory/jobs-store.ts +6 -9
- package/src/memory/jobs-worker.ts +51 -36
- package/src/memory/migrations/001-job-deferrals.ts +45 -0
- package/src/memory/migrations/002-tool-invocations-fk.ts +43 -0
- package/src/memory/migrations/003-memory-fts-backfill.ts +24 -0
- package/src/memory/migrations/004-entity-relation-dedup.ts +87 -0
- package/src/memory/migrations/005-fingerprint-scope-unique.ts +80 -0
- package/src/memory/migrations/006-scope-salted-fingerprints.ts +62 -0
- package/src/memory/migrations/007-assistant-id-to-self.ts +254 -0
- package/src/memory/migrations/008-remove-assistant-id-columns.ts +208 -0
- package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +83 -0
- package/src/memory/migrations/010-ext-conv-bindings-channel-chat-unique.ts +56 -0
- package/src/memory/migrations/011-call-sessions-provider-sid-dedup.ts +63 -0
- package/src/memory/migrations/012-call-sessions-add-initiated-from.ts +19 -0
- package/src/memory/migrations/013-guardian-action-tables.ts +68 -0
- package/src/memory/migrations/014-backfill-inbox-thread-state.ts +76 -0
- package/src/memory/migrations/015-drop-active-search-index.ts +27 -0
- package/src/memory/migrations/016-memory-segments-indexes.ts +11 -0
- package/src/memory/migrations/017-memory-items-indexes.ts +10 -0
- package/src/memory/migrations/018-remaining-table-indexes.ts +13 -0
- package/src/memory/migrations/index.ts +24 -0
- package/src/memory/migrations/registry.ts +79 -0
- package/src/memory/migrations/validate-migration-state.ts +69 -0
- package/src/memory/qdrant-manager.ts +49 -8
- package/src/memory/query-builder.ts +1 -1
- package/src/memory/raw-query.ts +119 -0
- package/src/memory/recall-cache.ts +4 -1
- package/src/memory/retriever.ts +160 -47
- package/src/memory/schema-migration.ts +25 -984
- package/src/memory/schema.ts +130 -7
- package/src/memory/search/entity.ts +10 -19
- package/src/memory/search/lexical.ts +81 -52
- package/src/memory/search/ranking.ts +21 -22
- package/src/memory/search/semantic.ts +157 -19
- package/src/memory/shared-app-links-store.ts +4 -5
- package/src/memory/validation.ts +19 -0
- package/src/messaging/draft-store.ts +5 -6
- package/src/messaging/providers/sms/adapter.ts +3 -6
- package/src/messaging/providers/telegram-bot/adapter.ts +2 -5
- package/src/messaging/providers/whatsapp/adapter.ts +136 -0
- package/src/messaging/providers/whatsapp/client.ts +67 -0
- package/src/messaging/style-analyzer.ts +5 -4
- package/src/messaging/thread-summarizer.ts +61 -69
- package/src/messaging/triage-engine.ts +62 -71
- package/src/migrations/config-merge.ts +53 -0
- package/src/migrations/data-layout.ts +68 -0
- package/src/migrations/data-merge.ts +33 -0
- package/src/migrations/hooks-merge.ts +90 -0
- package/src/migrations/index.ts +6 -0
- package/src/migrations/log.ts +23 -0
- package/src/migrations/skills-merge.ts +33 -0
- package/src/migrations/workspace-layout.ts +79 -0
- package/src/permissions/checker.ts +119 -11
- package/src/permissions/prompter.ts +14 -0
- package/src/permissions/shell-identity.ts +31 -1
- package/src/permissions/trust-store.ts +21 -1
- package/src/providers/anthropic/client.ts +4 -4
- package/src/providers/failover.ts +2 -2
- package/src/providers/model-intents.ts +70 -0
- package/src/providers/ollama/client.ts +2 -1
- package/src/providers/provider-send-message.ts +176 -0
- package/src/providers/registry.ts +71 -30
- package/src/providers/retry.ts +35 -1
- package/src/providers/types.ts +12 -1
- package/src/runtime/approval-conversation-turn.ts +97 -0
- package/src/runtime/approval-message-composer.ts +115 -5
- package/src/runtime/channel-approval-parser.ts +36 -2
- package/src/runtime/channel-approvals.ts +0 -21
- package/src/runtime/channel-guardian-service.ts +48 -7
- package/src/runtime/channel-readiness-service.ts +160 -34
- package/src/runtime/channel-readiness-types.ts +10 -4
- package/src/runtime/channel-retry-sweep.ts +184 -0
- package/src/runtime/guardian-context-resolver.ts +108 -0
- package/src/runtime/http-server.ts +275 -743
- package/src/runtime/http-types.ts +56 -3
- package/src/runtime/middleware/auth.ts +116 -0
- package/src/runtime/middleware/error-handler.ts +33 -0
- package/src/runtime/middleware/twilio-validation.ts +127 -0
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/call-routes.ts +49 -6
- package/src/runtime/routes/channel-delivery-routes.ts +170 -0
- package/src/runtime/routes/channel-guardian-routes.ts +1191 -0
- package/src/runtime/routes/channel-inbound-routes.ts +1152 -0
- package/src/runtime/routes/channel-route-shared.ts +144 -0
- package/src/runtime/routes/channel-routes.ts +32 -1634
- package/src/runtime/routes/conversation-routes.ts +50 -7
- package/src/runtime/routes/events-routes.ts +2 -2
- package/src/runtime/routes/identity-routes.ts +126 -0
- package/src/runtime/routes/pairing-routes.ts +143 -0
- package/src/runtime/routes/run-routes.ts +15 -1
- package/src/runtime/run-orchestrator.ts +52 -34
- package/src/schedule/schedule-store.ts +36 -32
- package/src/schedule/scheduler.ts +3 -3
- package/src/security/encrypted-store.ts +5 -7
- package/src/security/oauth2.ts +45 -15
- package/src/security/parental-control-store.ts +183 -0
- package/src/security/secret-allowlist.ts +4 -3
- package/src/security/secret-scanner.ts +5 -5
- package/src/security/secure-keys.ts +1 -1
- package/src/security/token-manager.ts +3 -2
- package/src/services/vercel-deploy.ts +6 -2
- package/src/skills/tool-manifest.ts +3 -3
- package/src/skills/vellum-catalog-remote.ts +75 -16
- package/src/slack/slack-webhook.ts +2 -1
- package/src/swarm/orchestrator.ts +92 -1
- package/src/swarm/router-planner.ts +6 -9
- package/src/swarm/worker-prompts.ts +9 -12
- package/src/tasks/task-compiler.ts +19 -28
- package/src/tasks/task-runner.ts +1 -1
- package/src/tools/assets/search.ts +15 -14
- package/src/tools/browser/__tests__/auth-detector.test.ts +1 -0
- package/src/tools/browser/auto-navigate.ts +1 -0
- package/src/tools/browser/browser-execution.ts +10 -1
- package/src/tools/browser/browser-manager.ts +119 -4
- package/src/tools/browser/network-recorder.ts +5 -0
- package/src/tools/credentials/broker.ts +11 -2
- package/src/tools/credentials/metadata-store.ts +18 -14
- package/src/tools/credentials/post-connect-hooks.ts +61 -0
- package/src/tools/credentials/vault.ts +49 -23
- package/src/tools/executor.ts +68 -9
- package/src/tools/host-terminal/cli-discover.ts +1 -1
- package/src/tools/network/script-proxy/http-forwarder.ts +1 -1
- package/src/tools/network/script-proxy/mitm-handler.ts +1 -1
- package/src/tools/network/script-proxy/server.ts +1 -1
- package/src/tools/network/script-proxy/session-manager.ts +6 -5
- package/src/tools/network/web-fetch.ts +18 -2
- package/src/tools/network/web-search.ts +7 -3
- package/src/tools/reminder/reminder-store.ts +14 -15
- package/src/tools/schedule/create.ts +1 -0
- package/src/tools/schedule/list.ts +2 -1
- package/src/tools/shared/filesystem/file-ops-service.ts +5 -7
- package/src/tools/skills/skill-script-runner.ts +24 -9
- package/src/tools/skills/skill-tool-factory.ts +1 -0
- package/src/tools/tasks/work-item-enqueue.ts +2 -2
- package/src/tools/terminal/evaluate-typescript.ts +21 -12
- package/src/tools/terminal/parser.ts +50 -0
- package/src/tools/watcher/delete.ts +6 -0
- package/src/tools/weather/service.ts +1 -1
- package/src/twitter/client.ts +190 -24
- package/src/twitter/session.ts +4 -3
- package/src/util/clipboard.ts +1 -1
- package/src/util/errors.ts +65 -8
- package/src/util/fs.ts +40 -0
- package/src/util/json.ts +10 -0
- package/src/util/log-redact.ts +189 -0
- package/src/util/logger.ts +19 -17
- package/src/util/object.ts +3 -0
- package/src/util/platform.ts +72 -365
- package/src/util/pricing.ts +1 -1
- package/src/util/promise-guard.ts +1 -1
- package/src/util/retry.ts +19 -0
- package/src/util/row-mapper.ts +79 -0
- package/src/util/silently.ts +21 -0
- package/src/watcher/engine.ts +5 -1
- package/src/watcher/provider-types.ts +20 -0
- package/src/watcher/providers/github.ts +156 -0
- package/src/watcher/providers/gmail.ts +1 -0
- package/src/watcher/providers/google-calendar.ts +1 -0
- package/src/watcher/providers/linear.ts +460 -0
- package/src/watcher/providers/slack.ts +1 -0
- package/src/work-items/work-item-runner.ts +1 -1
- package/src/workspace/git-service.ts +1 -1
- package/src/workspace/provider-commit-message-generator.ts +51 -22
- package/src/__tests__/call-bridge.test.ts +0 -517
- package/src/__tests__/session-process-bridge.test.ts +0 -244
- package/src/calls/call-bridge.ts +0 -168
- package/src/config/bundled-skills/media-processing/services/capability-registry.ts +0 -137
- package/src/config/bundled-skills/media-processing/services/event-detection-service.ts +0 -280
- package/src/config/bundled-skills/media-processing/services/feedback-aggregation.ts +0 -144
- package/src/config/bundled-skills/media-processing/services/feedback-store.ts +0 -136
- package/src/config/bundled-skills/media-processing/services/retrieval-service.ts +0 -95
- package/src/config/bundled-skills/media-processing/services/timeline-service.ts +0 -267
- package/src/config/bundled-skills/media-processing/tools/detect-events.ts +0 -110
- package/src/config/bundled-skills/media-processing/tools/recalibrate.ts +0 -235
- package/src/config/bundled-skills/media-processing/tools/select-tracking-profile.ts +0 -142
- package/src/config/bundled-skills/media-processing/tools/submit-feedback.ts +0 -150
- package/src/config/vellum-skills/google-oauth-setup/SKILL.md +0 -199
|
@@ -0,0 +1,1152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel inbound routes: message ingress, conversation deletion,
|
|
3
|
+
* and background message processing.
|
|
4
|
+
*/
|
|
5
|
+
import { isChannelId, CHANNEL_IDS } from '../../channels/types.js';
|
|
6
|
+
import type { ChannelId, TurnChannelContext } from '../../channels/types.js';
|
|
7
|
+
import { deleteConversationKey } from '../../memory/conversation-key-store.js';
|
|
8
|
+
import * as conversationStore from '../../memory/conversation-store.js';
|
|
9
|
+
import * as attachmentsStore from '../../memory/attachments-store.js';
|
|
10
|
+
import * as channelDeliveryStore from '../../memory/channel-delivery-store.js';
|
|
11
|
+
import * as externalConversationStore from '../../memory/external-conversation-store.js';
|
|
12
|
+
import { getPendingConfirmationsByConversation } from '../../memory/runs-store.js';
|
|
13
|
+
import { checkIngressForSecrets } from '../../security/secret-ingress.js';
|
|
14
|
+
import { IngressBlockedError } from '../../util/errors.js';
|
|
15
|
+
import { getLogger } from '../../util/logger.js';
|
|
16
|
+
import { findMember, updateLastSeen } from '../../memory/ingress-member-store.js';
|
|
17
|
+
import {
|
|
18
|
+
getGuardianBinding,
|
|
19
|
+
validateAndConsumeChallenge,
|
|
20
|
+
} from '../channel-guardian-service.js';
|
|
21
|
+
import { resolveGuardianContext } from '../guardian-context-resolver.js';
|
|
22
|
+
import {
|
|
23
|
+
getPendingDeliveriesByDestination,
|
|
24
|
+
getGuardianActionRequest,
|
|
25
|
+
resolveGuardianActionRequest,
|
|
26
|
+
} from '../../memory/guardian-action-store.js';
|
|
27
|
+
import { answerCall } from '../../calls/call-domain.js';
|
|
28
|
+
import {
|
|
29
|
+
createApprovalRequest,
|
|
30
|
+
updateApprovalDecision,
|
|
31
|
+
getPendingApprovalForRun,
|
|
32
|
+
} from '../../memory/channel-guardian-store.js';
|
|
33
|
+
import { deliverChannelReply } from '../gateway-client.js';
|
|
34
|
+
import {
|
|
35
|
+
getChannelApprovalPrompt,
|
|
36
|
+
buildApprovalUIMetadata,
|
|
37
|
+
buildGuardianApprovalPrompt,
|
|
38
|
+
handleChannelDecision,
|
|
39
|
+
} from '../channel-approvals.js';
|
|
40
|
+
import type { RunOrchestrator } from '../run-orchestrator.js';
|
|
41
|
+
import type {
|
|
42
|
+
MessageProcessor,
|
|
43
|
+
ApprovalCopyGenerator,
|
|
44
|
+
ApprovalConversationGenerator,
|
|
45
|
+
} from '../http-types.js';
|
|
46
|
+
import { composeApprovalMessageGenerative } from '../approval-message-composer.js';
|
|
47
|
+
import { refreshThreadEscalation } from '../../memory/inbox-escalation-projection.js';
|
|
48
|
+
import {
|
|
49
|
+
type GuardianContext,
|
|
50
|
+
verifyGatewayOrigin,
|
|
51
|
+
toGuardianRuntimeContext,
|
|
52
|
+
GUARDIAN_APPROVAL_TTL_MS,
|
|
53
|
+
stripVerificationFailurePrefix,
|
|
54
|
+
buildGuardianDenyContext,
|
|
55
|
+
buildPromptDeliveryFailureContext,
|
|
56
|
+
RUN_POLL_INTERVAL_MS,
|
|
57
|
+
getEffectivePollMaxWait,
|
|
58
|
+
} from './channel-route-shared.js';
|
|
59
|
+
import { deliverReplyViaCallback } from './channel-delivery-routes.js';
|
|
60
|
+
import { handleApprovalInterception, deliverGeneratedApprovalPrompt } from './channel-guardian-routes.js';
|
|
61
|
+
|
|
62
|
+
const log = getLogger('runtime-http');
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Conversation deletion
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
export async function handleDeleteConversation(req: Request, assistantId: string = 'self'): Promise<Response> {
|
|
69
|
+
const body = await req.json() as {
|
|
70
|
+
sourceChannel?: string;
|
|
71
|
+
externalChatId?: string;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const { sourceChannel, externalChatId } = body;
|
|
75
|
+
|
|
76
|
+
if (!sourceChannel || typeof sourceChannel !== 'string') {
|
|
77
|
+
return Response.json({ error: 'sourceChannel is required' }, { status: 400 });
|
|
78
|
+
}
|
|
79
|
+
if (!externalChatId || typeof externalChatId !== 'string') {
|
|
80
|
+
return Response.json({ error: 'externalChatId is required' }, { status: 400 });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Delete the assistant-scoped key unconditionally. The legacy key is
|
|
84
|
+
// canonical for the self assistant and must not be deleted from non-self
|
|
85
|
+
// routes, otherwise a non-self reset can accidentally reset self state.
|
|
86
|
+
const legacyKey = `${sourceChannel}:${externalChatId}`;
|
|
87
|
+
const scopedKey = `asst:${assistantId}:${sourceChannel}:${externalChatId}`;
|
|
88
|
+
deleteConversationKey(scopedKey);
|
|
89
|
+
if (assistantId === 'self') {
|
|
90
|
+
deleteConversationKey(legacyKey);
|
|
91
|
+
}
|
|
92
|
+
// external_conversation_bindings is currently assistant-agnostic
|
|
93
|
+
// (unique by sourceChannel + externalChatId). Restrict mutations to the
|
|
94
|
+
// canonical self-assistant route so multi-assistant legacy routes do not
|
|
95
|
+
// clobber each other's bindings.
|
|
96
|
+
if (assistantId === 'self') {
|
|
97
|
+
externalConversationStore.deleteBindingByChannelChat(sourceChannel, externalChatId);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return Response.json({ ok: true });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Channel inbound message handler
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
export async function handleChannelInbound(
|
|
108
|
+
req: Request,
|
|
109
|
+
processMessage?: MessageProcessor,
|
|
110
|
+
bearerToken?: string,
|
|
111
|
+
runOrchestrator?: RunOrchestrator,
|
|
112
|
+
assistantId: string = 'self',
|
|
113
|
+
gatewayOriginSecret?: string,
|
|
114
|
+
approvalCopyGenerator?: ApprovalCopyGenerator,
|
|
115
|
+
approvalConversationGenerator?: ApprovalConversationGenerator,
|
|
116
|
+
): Promise<Response> {
|
|
117
|
+
// Reject requests that lack valid gateway-origin proof. This ensures
|
|
118
|
+
// channel inbound messages can only arrive via the gateway (which
|
|
119
|
+
// performs webhook-level verification) and not via direct HTTP calls.
|
|
120
|
+
if (!verifyGatewayOrigin(req, bearerToken, gatewayOriginSecret)) {
|
|
121
|
+
log.warn('Rejected channel inbound request: missing or invalid gateway-origin proof');
|
|
122
|
+
return Response.json(
|
|
123
|
+
{ error: 'Forbidden: missing gateway-origin proof', code: 'GATEWAY_ORIGIN_REQUIRED' },
|
|
124
|
+
{ status: 403 },
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const body = await req.json() as {
|
|
129
|
+
sourceChannel?: string;
|
|
130
|
+
externalChatId?: string;
|
|
131
|
+
externalMessageId?: string;
|
|
132
|
+
content?: string;
|
|
133
|
+
isEdit?: boolean;
|
|
134
|
+
senderName?: string;
|
|
135
|
+
attachmentIds?: string[];
|
|
136
|
+
senderExternalUserId?: string;
|
|
137
|
+
senderUsername?: string;
|
|
138
|
+
sourceMetadata?: Record<string, unknown>;
|
|
139
|
+
replyCallbackUrl?: string;
|
|
140
|
+
callbackQueryId?: string;
|
|
141
|
+
callbackData?: string;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const {
|
|
145
|
+
externalChatId,
|
|
146
|
+
externalMessageId,
|
|
147
|
+
content,
|
|
148
|
+
isEdit,
|
|
149
|
+
attachmentIds,
|
|
150
|
+
sourceMetadata,
|
|
151
|
+
} = body;
|
|
152
|
+
|
|
153
|
+
if (!body.sourceChannel || typeof body.sourceChannel !== 'string') {
|
|
154
|
+
return Response.json({ error: 'sourceChannel is required' }, { status: 400 });
|
|
155
|
+
}
|
|
156
|
+
// Validate and narrow to canonical ChannelId at the boundary — the gateway
|
|
157
|
+
// only sends well-known channel strings, so an unknown value is rejected.
|
|
158
|
+
if (!isChannelId(body.sourceChannel)) {
|
|
159
|
+
return Response.json(
|
|
160
|
+
{ error: `Invalid sourceChannel: ${body.sourceChannel}. Valid values: ${CHANNEL_IDS.join(', ')}` },
|
|
161
|
+
{ status: 400 },
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const sourceChannel = body.sourceChannel;
|
|
166
|
+
if (!externalChatId || typeof externalChatId !== 'string') {
|
|
167
|
+
return Response.json({ error: 'externalChatId is required' }, { status: 400 });
|
|
168
|
+
}
|
|
169
|
+
if (!externalMessageId || typeof externalMessageId !== 'string') {
|
|
170
|
+
return Response.json({ error: 'externalMessageId is required' }, { status: 400 });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Reject non-string content regardless of whether attachments are present.
|
|
174
|
+
if (content != null && typeof content !== 'string') {
|
|
175
|
+
return Response.json({ error: 'content must be a string' }, { status: 400 });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const trimmedContent = typeof content === 'string' ? content.trim() : '';
|
|
179
|
+
const hasAttachments = Array.isArray(attachmentIds) && attachmentIds.length > 0;
|
|
180
|
+
|
|
181
|
+
const hasCallbackData = typeof body.callbackData === 'string' && body.callbackData.length > 0;
|
|
182
|
+
|
|
183
|
+
if (trimmedContent.length === 0 && !hasAttachments && !isEdit && !hasCallbackData) {
|
|
184
|
+
return Response.json({ error: 'content or attachmentIds is required' }, { status: 400 });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Ingress ACL enforcement ──
|
|
188
|
+
// Track the resolved member so the escalate branch can reference it after
|
|
189
|
+
// recordInbound (where we have a conversationId).
|
|
190
|
+
let resolvedMember: ReturnType<typeof findMember> = null;
|
|
191
|
+
|
|
192
|
+
if (body.senderExternalUserId) {
|
|
193
|
+
resolvedMember = findMember({
|
|
194
|
+
sourceChannel,
|
|
195
|
+
externalUserId: body.senderExternalUserId,
|
|
196
|
+
externalChatId,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
if (!resolvedMember) {
|
|
200
|
+
log.info({ sourceChannel, externalUserId: body.senderExternalUserId }, 'Ingress ACL: no member record, denying');
|
|
201
|
+
return Response.json({ accepted: true, denied: true, reason: 'not_a_member' });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (resolvedMember.status !== 'active') {
|
|
205
|
+
log.info({ sourceChannel, memberId: resolvedMember.id, status: resolvedMember.status }, 'Ingress ACL: member not active, denying');
|
|
206
|
+
return Response.json({ accepted: true, denied: true, reason: `member_${resolvedMember.status}` });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (resolvedMember.policy === 'deny') {
|
|
210
|
+
log.info({ sourceChannel, memberId: resolvedMember.id }, 'Ingress ACL: member policy deny');
|
|
211
|
+
return Response.json({ accepted: true, denied: true, reason: 'policy_deny' });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 'allow' or 'escalate' — update last seen and continue
|
|
215
|
+
updateLastSeen(resolvedMember.id);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (hasAttachments) {
|
|
219
|
+
const resolved = attachmentsStore.getAttachmentsByIds(attachmentIds);
|
|
220
|
+
if (resolved.length !== attachmentIds.length) {
|
|
221
|
+
const resolvedIds = new Set(resolved.map((a) => a.id));
|
|
222
|
+
const missing = attachmentIds.filter((id) => !resolvedIds.has(id));
|
|
223
|
+
return Response.json(
|
|
224
|
+
{ error: `Attachment IDs not found: ${missing.join(', ')}` },
|
|
225
|
+
{ status: 400 },
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const sourceMessageId = typeof sourceMetadata?.messageId === 'string'
|
|
231
|
+
? sourceMetadata.messageId
|
|
232
|
+
: undefined;
|
|
233
|
+
|
|
234
|
+
if (isEdit && !sourceMessageId) {
|
|
235
|
+
return Response.json({ error: 'sourceMetadata.messageId is required for edits' }, { status: 400 });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── Edit path: update existing message content, no new agent loop ──
|
|
239
|
+
if (isEdit && sourceMessageId) {
|
|
240
|
+
// Dedup the edit event itself (retried edited_message webhooks)
|
|
241
|
+
const editResult = channelDeliveryStore.recordInbound(
|
|
242
|
+
sourceChannel,
|
|
243
|
+
externalChatId,
|
|
244
|
+
externalMessageId,
|
|
245
|
+
{ sourceMessageId, assistantId },
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
if (editResult.duplicate) {
|
|
249
|
+
return Response.json({
|
|
250
|
+
accepted: true,
|
|
251
|
+
duplicate: true,
|
|
252
|
+
eventId: editResult.eventId,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Retry lookup a few times — the original message may still be processing
|
|
257
|
+
// (linkMessage hasn't been called yet). Short backoff avoids losing edits
|
|
258
|
+
// that arrive while the original agent loop is in progress.
|
|
259
|
+
const EDIT_LOOKUP_RETRIES = 5;
|
|
260
|
+
const EDIT_LOOKUP_DELAY_MS = 2000;
|
|
261
|
+
|
|
262
|
+
let original: { messageId: string; conversationId: string } | null = null;
|
|
263
|
+
for (let attempt = 0; attempt <= EDIT_LOOKUP_RETRIES; attempt++) {
|
|
264
|
+
original = channelDeliveryStore.findMessageBySourceId(
|
|
265
|
+
sourceChannel,
|
|
266
|
+
externalChatId,
|
|
267
|
+
sourceMessageId,
|
|
268
|
+
);
|
|
269
|
+
if (original) break;
|
|
270
|
+
if (attempt < EDIT_LOOKUP_RETRIES) {
|
|
271
|
+
log.info(
|
|
272
|
+
{ assistantId, sourceMessageId, attempt: attempt + 1, maxAttempts: EDIT_LOOKUP_RETRIES },
|
|
273
|
+
'Original message not linked yet, retrying edit lookup',
|
|
274
|
+
);
|
|
275
|
+
await new Promise((resolve) => setTimeout(resolve, EDIT_LOOKUP_DELAY_MS));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (original) {
|
|
280
|
+
conversationStore.updateMessageContent(original.messageId, content ?? '');
|
|
281
|
+
log.info(
|
|
282
|
+
{ assistantId, sourceMessageId, messageId: original.messageId },
|
|
283
|
+
'Updated message content from edited_message',
|
|
284
|
+
);
|
|
285
|
+
} else {
|
|
286
|
+
log.warn(
|
|
287
|
+
{ assistantId, sourceChannel, externalChatId, sourceMessageId },
|
|
288
|
+
'Could not find original message for edit after retries, ignoring',
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return Response.json({
|
|
293
|
+
accepted: true,
|
|
294
|
+
duplicate: false,
|
|
295
|
+
eventId: editResult.eventId,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ── New message path ──
|
|
300
|
+
const result = channelDeliveryStore.recordInbound(
|
|
301
|
+
sourceChannel,
|
|
302
|
+
externalChatId,
|
|
303
|
+
externalMessageId,
|
|
304
|
+
{ sourceMessageId, assistantId },
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
// external_conversation_bindings is assistant-agnostic. Restrict writes to
|
|
308
|
+
// self so assistant-scoped legacy routes do not overwrite each other's
|
|
309
|
+
// channel binding metadata for the same chat.
|
|
310
|
+
if (assistantId === 'self') {
|
|
311
|
+
externalConversationStore.upsertBinding({
|
|
312
|
+
conversationId: result.conversationId,
|
|
313
|
+
sourceChannel,
|
|
314
|
+
externalChatId,
|
|
315
|
+
externalUserId: body.senderExternalUserId ?? null,
|
|
316
|
+
displayName: body.senderName ?? null,
|
|
317
|
+
username: body.senderUsername ?? null,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ── Ingress escalation ──
|
|
322
|
+
// When the member's policy is 'escalate', create a pending guardian
|
|
323
|
+
// approval request and halt the run. This check runs after recordInbound
|
|
324
|
+
// so we have a conversationId for the approval record.
|
|
325
|
+
if (resolvedMember?.policy === 'escalate') {
|
|
326
|
+
const binding = getGuardianBinding(assistantId, sourceChannel);
|
|
327
|
+
if (!binding) {
|
|
328
|
+
// Fail-closed: can't escalate without a guardian to route to
|
|
329
|
+
log.info({ sourceChannel, memberId: resolvedMember.id }, 'Ingress ACL: escalate policy but no guardian binding, denying');
|
|
330
|
+
return Response.json({ accepted: true, denied: true, reason: 'escalate_no_guardian' });
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Persist the raw payload so the decide handler can recover the original
|
|
334
|
+
// message content when the escalation is approved.
|
|
335
|
+
channelDeliveryStore.storePayload(result.eventId, {
|
|
336
|
+
sourceChannel, externalChatId, externalMessageId, content,
|
|
337
|
+
attachmentIds, sourceMetadata: body.sourceMetadata,
|
|
338
|
+
senderName: body.senderName,
|
|
339
|
+
senderExternalUserId: body.senderExternalUserId,
|
|
340
|
+
senderUsername: body.senderUsername,
|
|
341
|
+
replyCallbackUrl: body.replyCallbackUrl,
|
|
342
|
+
assistantId,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
createApprovalRequest({
|
|
346
|
+
runId: `ingress-escalation-${Date.now()}`,
|
|
347
|
+
conversationId: result.conversationId,
|
|
348
|
+
assistantId,
|
|
349
|
+
channel: sourceChannel,
|
|
350
|
+
requesterExternalUserId: body.senderExternalUserId ?? '',
|
|
351
|
+
requesterChatId: externalChatId,
|
|
352
|
+
guardianExternalUserId: binding.guardianExternalUserId,
|
|
353
|
+
guardianChatId: binding.guardianDeliveryChatId,
|
|
354
|
+
toolName: 'ingress_message',
|
|
355
|
+
riskLevel: 'escalated_ingress',
|
|
356
|
+
reason: 'Ingress policy requires guardian approval',
|
|
357
|
+
expiresAt: Date.now() + GUARDIAN_APPROVAL_TTL_MS,
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// Update inbox thread escalation state so the desktop UI badge is accurate
|
|
361
|
+
refreshThreadEscalation(result.conversationId, assistantId);
|
|
362
|
+
|
|
363
|
+
// Notify the guardian about the pending escalation via channel delivery
|
|
364
|
+
const senderIdentifier = body.senderName || body.senderUsername || body.senderExternalUserId || 'Unknown sender';
|
|
365
|
+
if (body.replyCallbackUrl) {
|
|
366
|
+
try {
|
|
367
|
+
const notificationText = await composeApprovalMessageGenerative(
|
|
368
|
+
{
|
|
369
|
+
scenario: 'guardian_prompt',
|
|
370
|
+
channel: sourceChannel,
|
|
371
|
+
toolName: 'ingress_message',
|
|
372
|
+
requesterIdentifier: senderIdentifier,
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
fallbackText: `New message from ${senderIdentifier} requires your review. Reply with "approve" or "deny".`,
|
|
376
|
+
},
|
|
377
|
+
approvalCopyGenerator,
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
await deliverChannelReply(
|
|
381
|
+
body.replyCallbackUrl,
|
|
382
|
+
{ chatId: binding.guardianDeliveryChatId, text: notificationText, assistantId },
|
|
383
|
+
bearerToken,
|
|
384
|
+
);
|
|
385
|
+
} catch (err) {
|
|
386
|
+
// Fail-closed: approval request is already created in the DB and
|
|
387
|
+
// thread escalation state is updated — the desktop UI will show
|
|
388
|
+
// the pending escalation even if channel notification failed.
|
|
389
|
+
log.error({ err, conversationId: result.conversationId, guardianChatId: binding.guardianDeliveryChatId }, 'Failed to notify guardian of ingress escalation');
|
|
390
|
+
}
|
|
391
|
+
} else {
|
|
392
|
+
log.warn({ conversationId: result.conversationId }, 'Ingress escalation created but no replyCallbackUrl to notify guardian');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return Response.json({ accepted: true, escalated: true, reason: 'policy_escalate' });
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const metadataHintsRaw = sourceMetadata?.hints;
|
|
399
|
+
const metadataHints = Array.isArray(metadataHintsRaw)
|
|
400
|
+
? metadataHintsRaw.filter((hint): hint is string => typeof hint === 'string' && hint.trim().length > 0)
|
|
401
|
+
: [];
|
|
402
|
+
const metadataUxBrief = typeof sourceMetadata?.uxBrief === 'string' && sourceMetadata.uxBrief.trim().length > 0
|
|
403
|
+
? sourceMetadata.uxBrief.trim()
|
|
404
|
+
: undefined;
|
|
405
|
+
|
|
406
|
+
// Extract channel command intent (e.g. /start from Telegram)
|
|
407
|
+
const rawCommandIntent = sourceMetadata?.commandIntent;
|
|
408
|
+
const commandIntent = rawCommandIntent && typeof rawCommandIntent === 'object' && !Array.isArray(rawCommandIntent)
|
|
409
|
+
? rawCommandIntent as Record<string, unknown>
|
|
410
|
+
: undefined;
|
|
411
|
+
|
|
412
|
+
// Preserve locale from sourceMetadata so the model can greet in the user's language
|
|
413
|
+
const sourceLanguageCode = typeof sourceMetadata?.languageCode === 'string' && sourceMetadata.languageCode.trim().length > 0
|
|
414
|
+
? sourceMetadata.languageCode.trim()
|
|
415
|
+
: undefined;
|
|
416
|
+
|
|
417
|
+
const replyCallbackUrl = body.replyCallbackUrl;
|
|
418
|
+
|
|
419
|
+
// ── Guardian verification command intercept ──
|
|
420
|
+
// Handled before normal message processing so it never enters the agent loop.
|
|
421
|
+
if (
|
|
422
|
+
!result.duplicate &&
|
|
423
|
+
trimmedContent.startsWith('/guardian_verify ') &&
|
|
424
|
+
replyCallbackUrl &&
|
|
425
|
+
body.senderExternalUserId
|
|
426
|
+
) {
|
|
427
|
+
const token = trimmedContent.slice('/guardian_verify '.length).trim();
|
|
428
|
+
if (token.length > 0) {
|
|
429
|
+
const verifyResult = validateAndConsumeChallenge(
|
|
430
|
+
assistantId,
|
|
431
|
+
sourceChannel,
|
|
432
|
+
token,
|
|
433
|
+
body.senderExternalUserId,
|
|
434
|
+
externalChatId,
|
|
435
|
+
body.senderUsername,
|
|
436
|
+
body.senderName,
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
const replyText = verifyResult.success
|
|
440
|
+
? await composeApprovalMessageGenerative({
|
|
441
|
+
scenario: 'guardian_verify_success',
|
|
442
|
+
channel: sourceChannel,
|
|
443
|
+
}, {}, approvalCopyGenerator)
|
|
444
|
+
: await composeApprovalMessageGenerative({
|
|
445
|
+
scenario: 'guardian_verify_failed',
|
|
446
|
+
channel: sourceChannel,
|
|
447
|
+
failureReason: stripVerificationFailurePrefix(verifyResult.reason),
|
|
448
|
+
}, {}, approvalCopyGenerator);
|
|
449
|
+
|
|
450
|
+
try {
|
|
451
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
452
|
+
chatId: externalChatId,
|
|
453
|
+
text: replyText,
|
|
454
|
+
assistantId,
|
|
455
|
+
}, bearerToken);
|
|
456
|
+
} catch (err) {
|
|
457
|
+
log.error({ err, externalChatId }, 'Failed to deliver guardian verification reply');
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return Response.json({
|
|
461
|
+
accepted: true,
|
|
462
|
+
duplicate: false,
|
|
463
|
+
eventId: result.eventId,
|
|
464
|
+
guardianVerification: verifyResult.success ? 'verified' : 'failed',
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ── Guardian action answer interception ──
|
|
470
|
+
// Check if this inbound message is a reply to a cross-channel guardian
|
|
471
|
+
// action request (from a voice call). Must run before approval interception
|
|
472
|
+
// so guardian answers are not mistakenly routed into the approval flow.
|
|
473
|
+
if (
|
|
474
|
+
!result.duplicate &&
|
|
475
|
+
trimmedContent.length > 0 &&
|
|
476
|
+
body.senderExternalUserId &&
|
|
477
|
+
replyCallbackUrl
|
|
478
|
+
) {
|
|
479
|
+
const pendingDeliveries = getPendingDeliveriesByDestination(assistantId, sourceChannel, externalChatId);
|
|
480
|
+
if (pendingDeliveries.length > 0) {
|
|
481
|
+
// Identity check: only the designated guardian can answer
|
|
482
|
+
const validDeliveries = pendingDeliveries.filter(
|
|
483
|
+
(d) => d.destinationExternalUserId === body.senderExternalUserId,
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
if (validDeliveries.length > 0) {
|
|
487
|
+
let matchedDelivery = validDeliveries.length === 1 ? validDeliveries[0] : null;
|
|
488
|
+
let answerText = trimmedContent;
|
|
489
|
+
|
|
490
|
+
// Multiple pending deliveries: require request code prefix for disambiguation
|
|
491
|
+
if (validDeliveries.length > 1) {
|
|
492
|
+
for (const d of validDeliveries) {
|
|
493
|
+
const req = getGuardianActionRequest(d.requestId);
|
|
494
|
+
if (req && trimmedContent.toUpperCase().startsWith(req.requestCode)) {
|
|
495
|
+
matchedDelivery = d;
|
|
496
|
+
answerText = trimmedContent.slice(req.requestCode.length).trim();
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (!matchedDelivery) {
|
|
502
|
+
// Send disambiguation message listing the request codes
|
|
503
|
+
const codes = validDeliveries
|
|
504
|
+
.map((d) => {
|
|
505
|
+
const req = getGuardianActionRequest(d.requestId);
|
|
506
|
+
return req ? req.requestCode : null;
|
|
507
|
+
})
|
|
508
|
+
.filter(Boolean);
|
|
509
|
+
try {
|
|
510
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
511
|
+
chatId: externalChatId,
|
|
512
|
+
text: `You have multiple pending guardian questions. Please prefix your reply with the reference code (${codes.join(', ')}) to indicate which question you are answering.`,
|
|
513
|
+
assistantId,
|
|
514
|
+
}, bearerToken);
|
|
515
|
+
} catch (err) {
|
|
516
|
+
log.error({ err, externalChatId }, 'Failed to deliver guardian action disambiguation message');
|
|
517
|
+
}
|
|
518
|
+
return Response.json({
|
|
519
|
+
accepted: true,
|
|
520
|
+
duplicate: false,
|
|
521
|
+
eventId: result.eventId,
|
|
522
|
+
guardianAnswer: 'disambiguation_sent',
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (matchedDelivery) {
|
|
528
|
+
const request = getGuardianActionRequest(matchedDelivery.requestId);
|
|
529
|
+
if (request) {
|
|
530
|
+
// Attempt to deliver the answer to the call first. Only resolve
|
|
531
|
+
// the guardian action request if answerCall succeeds, so that a
|
|
532
|
+
// failed delivery (e.g. pending question timed out) leaves the
|
|
533
|
+
// request pending for retry from another channel.
|
|
534
|
+
const answerResult = await answerCall({ callSessionId: request.callSessionId, answer: answerText });
|
|
535
|
+
|
|
536
|
+
if (!('ok' in answerResult) || !answerResult.ok) {
|
|
537
|
+
const errorMsg = 'error' in answerResult ? answerResult.error : 'Unknown error';
|
|
538
|
+
log.warn({ callSessionId: request.callSessionId, error: errorMsg }, 'answerCall failed for guardian answer');
|
|
539
|
+
try {
|
|
540
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
541
|
+
chatId: externalChatId,
|
|
542
|
+
text: 'Failed to deliver your answer to the call. Please try again.',
|
|
543
|
+
assistantId,
|
|
544
|
+
}, bearerToken);
|
|
545
|
+
} catch (deliverErr) {
|
|
546
|
+
log.error({ err: deliverErr, externalChatId }, 'Failed to deliver guardian answer failure notice');
|
|
547
|
+
}
|
|
548
|
+
return Response.json({
|
|
549
|
+
accepted: true,
|
|
550
|
+
duplicate: false,
|
|
551
|
+
eventId: result.eventId,
|
|
552
|
+
guardianAnswer: 'answer_failed',
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const resolved = resolveGuardianActionRequest(
|
|
557
|
+
request.id,
|
|
558
|
+
answerText,
|
|
559
|
+
sourceChannel,
|
|
560
|
+
body.senderExternalUserId,
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
if (resolved) {
|
|
564
|
+
return Response.json({
|
|
565
|
+
accepted: true,
|
|
566
|
+
duplicate: false,
|
|
567
|
+
eventId: result.eventId,
|
|
568
|
+
guardianAnswer: 'resolved',
|
|
569
|
+
});
|
|
570
|
+
} else {
|
|
571
|
+
// Already answered from another channel
|
|
572
|
+
try {
|
|
573
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
574
|
+
chatId: externalChatId,
|
|
575
|
+
text: 'This question has already been answered from another channel.',
|
|
576
|
+
assistantId,
|
|
577
|
+
}, bearerToken);
|
|
578
|
+
} catch (err) {
|
|
579
|
+
log.error({ err, externalChatId }, 'Failed to deliver guardian action stale notice');
|
|
580
|
+
}
|
|
581
|
+
return Response.json({
|
|
582
|
+
accepted: true,
|
|
583
|
+
duplicate: false,
|
|
584
|
+
eventId: result.eventId,
|
|
585
|
+
guardianAnswer: 'stale',
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ── Actor role resolution ──
|
|
595
|
+
// Uses shared channel-agnostic resolution so all ingress paths classify
|
|
596
|
+
// guardian vs non-guardian actors the same way.
|
|
597
|
+
const guardianCtx: GuardianContext = resolveGuardianContext({
|
|
598
|
+
assistantId,
|
|
599
|
+
sourceChannel,
|
|
600
|
+
externalChatId,
|
|
601
|
+
senderExternalUserId: body.senderExternalUserId,
|
|
602
|
+
senderUsername: body.senderUsername,
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
// ── Approval interception ──
|
|
606
|
+
// Keep this active whenever orchestrator + callback context are available.
|
|
607
|
+
if (
|
|
608
|
+
runOrchestrator &&
|
|
609
|
+
replyCallbackUrl &&
|
|
610
|
+
!result.duplicate
|
|
611
|
+
) {
|
|
612
|
+
const approvalResult = await handleApprovalInterception({
|
|
613
|
+
conversationId: result.conversationId,
|
|
614
|
+
callbackData: body.callbackData,
|
|
615
|
+
content: trimmedContent,
|
|
616
|
+
externalChatId,
|
|
617
|
+
sourceChannel,
|
|
618
|
+
senderExternalUserId: body.senderExternalUserId,
|
|
619
|
+
replyCallbackUrl,
|
|
620
|
+
bearerToken,
|
|
621
|
+
orchestrator: runOrchestrator,
|
|
622
|
+
guardianCtx,
|
|
623
|
+
assistantId,
|
|
624
|
+
approvalCopyGenerator,
|
|
625
|
+
approvalConversationGenerator,
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
if (approvalResult.handled) {
|
|
629
|
+
return Response.json({
|
|
630
|
+
accepted: true,
|
|
631
|
+
duplicate: false,
|
|
632
|
+
eventId: result.eventId,
|
|
633
|
+
approval: approvalResult.type,
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// When a callback payload was not handled by approval interception, it's
|
|
638
|
+
// a stale button press with no pending approval. Return early regardless
|
|
639
|
+
// of whether content/attachments are present — callback payloads always
|
|
640
|
+
// have non-empty content (normalize.ts sets message.content to cbq.data),
|
|
641
|
+
// so checking for empty content alone would miss stale callbacks.
|
|
642
|
+
if (hasCallbackData) {
|
|
643
|
+
return Response.json({
|
|
644
|
+
accepted: true,
|
|
645
|
+
duplicate: false,
|
|
646
|
+
eventId: result.eventId,
|
|
647
|
+
approval: 'stale_ignored',
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// For new (non-duplicate) messages, run the secret ingress check
|
|
653
|
+
// synchronously, then fire off the agent loop in the background.
|
|
654
|
+
if (!result.duplicate && processMessage) {
|
|
655
|
+
// Persist the raw payload first so dead-lettered events can always be
|
|
656
|
+
// replayed. If the ingress check later detects secrets we clear it
|
|
657
|
+
// before throwing, so secret-bearing content is never left on disk.
|
|
658
|
+
channelDeliveryStore.storePayload(result.eventId, {
|
|
659
|
+
sourceChannel, externalChatId, externalMessageId, content,
|
|
660
|
+
attachmentIds, sourceMetadata: body.sourceMetadata,
|
|
661
|
+
senderName: body.senderName,
|
|
662
|
+
senderExternalUserId: body.senderExternalUserId,
|
|
663
|
+
senderUsername: body.senderUsername,
|
|
664
|
+
guardianCtx: toGuardianRuntimeContext(sourceChannel, guardianCtx),
|
|
665
|
+
replyCallbackUrl,
|
|
666
|
+
assistantId,
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
const contentToCheck = content ?? '';
|
|
670
|
+
let ingressCheck: ReturnType<typeof checkIngressForSecrets>;
|
|
671
|
+
try {
|
|
672
|
+
ingressCheck = checkIngressForSecrets(contentToCheck);
|
|
673
|
+
} catch (checkErr) {
|
|
674
|
+
channelDeliveryStore.clearPayload(result.eventId);
|
|
675
|
+
throw checkErr;
|
|
676
|
+
}
|
|
677
|
+
if (ingressCheck.blocked) {
|
|
678
|
+
channelDeliveryStore.clearPayload(result.eventId);
|
|
679
|
+
throw new IngressBlockedError(ingressCheck.userNotice!, ingressCheck.detectedTypes);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Use the approval-aware orchestrator path whenever orchestration and a
|
|
683
|
+
// callback delivery target are available. This keeps approval handling
|
|
684
|
+
// consistent across all channels and avoids silent prompt timeouts.
|
|
685
|
+
const useApprovalPath = Boolean(
|
|
686
|
+
runOrchestrator &&
|
|
687
|
+
replyCallbackUrl,
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
if (useApprovalPath && runOrchestrator && replyCallbackUrl) {
|
|
691
|
+
processChannelMessageWithApprovals({
|
|
692
|
+
orchestrator: runOrchestrator,
|
|
693
|
+
conversationId: result.conversationId,
|
|
694
|
+
eventId: result.eventId,
|
|
695
|
+
content: content ?? '',
|
|
696
|
+
attachmentIds: hasAttachments ? attachmentIds : undefined,
|
|
697
|
+
externalChatId,
|
|
698
|
+
sourceChannel,
|
|
699
|
+
replyCallbackUrl,
|
|
700
|
+
bearerToken,
|
|
701
|
+
guardianCtx,
|
|
702
|
+
assistantId,
|
|
703
|
+
metadataHints,
|
|
704
|
+
metadataUxBrief,
|
|
705
|
+
commandIntent,
|
|
706
|
+
sourceLanguageCode,
|
|
707
|
+
approvalCopyGenerator,
|
|
708
|
+
});
|
|
709
|
+
} else {
|
|
710
|
+
// Fire-and-forget: process the message and deliver the reply in the background.
|
|
711
|
+
// The HTTP response returns immediately so the gateway webhook is not blocked.
|
|
712
|
+
processChannelMessageInBackground({
|
|
713
|
+
processMessage,
|
|
714
|
+
conversationId: result.conversationId,
|
|
715
|
+
eventId: result.eventId,
|
|
716
|
+
content: content ?? '',
|
|
717
|
+
attachmentIds: hasAttachments ? attachmentIds : undefined,
|
|
718
|
+
sourceChannel,
|
|
719
|
+
externalChatId,
|
|
720
|
+
guardianCtx,
|
|
721
|
+
metadataHints,
|
|
722
|
+
metadataUxBrief,
|
|
723
|
+
commandIntent,
|
|
724
|
+
sourceLanguageCode,
|
|
725
|
+
replyCallbackUrl,
|
|
726
|
+
bearerToken,
|
|
727
|
+
assistantId,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return Response.json({
|
|
733
|
+
accepted: result.accepted,
|
|
734
|
+
duplicate: result.duplicate,
|
|
735
|
+
eventId: result.eventId,
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// ---------------------------------------------------------------------------
|
|
740
|
+
// Background message processing
|
|
741
|
+
// ---------------------------------------------------------------------------
|
|
742
|
+
|
|
743
|
+
interface BackgroundProcessingParams {
|
|
744
|
+
processMessage: MessageProcessor;
|
|
745
|
+
conversationId: string;
|
|
746
|
+
eventId: string;
|
|
747
|
+
content: string;
|
|
748
|
+
attachmentIds?: string[];
|
|
749
|
+
sourceChannel: ChannelId;
|
|
750
|
+
externalChatId: string;
|
|
751
|
+
guardianCtx: GuardianContext;
|
|
752
|
+
metadataHints: string[];
|
|
753
|
+
metadataUxBrief?: string;
|
|
754
|
+
replyCallbackUrl?: string;
|
|
755
|
+
bearerToken?: string;
|
|
756
|
+
assistantId?: string;
|
|
757
|
+
commandIntent?: Record<string, unknown>;
|
|
758
|
+
sourceLanguageCode?: string;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function processChannelMessageInBackground(params: BackgroundProcessingParams): void {
|
|
762
|
+
const {
|
|
763
|
+
processMessage,
|
|
764
|
+
conversationId,
|
|
765
|
+
eventId,
|
|
766
|
+
content,
|
|
767
|
+
attachmentIds,
|
|
768
|
+
sourceChannel,
|
|
769
|
+
externalChatId,
|
|
770
|
+
guardianCtx,
|
|
771
|
+
metadataHints,
|
|
772
|
+
metadataUxBrief,
|
|
773
|
+
replyCallbackUrl,
|
|
774
|
+
bearerToken,
|
|
775
|
+
assistantId,
|
|
776
|
+
commandIntent,
|
|
777
|
+
sourceLanguageCode,
|
|
778
|
+
} = params;
|
|
779
|
+
|
|
780
|
+
(async () => {
|
|
781
|
+
try {
|
|
782
|
+
const cmdIntent = commandIntent && typeof commandIntent.type === 'string'
|
|
783
|
+
? { type: commandIntent.type as string, ...(typeof commandIntent.payload === 'string' ? { payload: commandIntent.payload } : {}), ...(sourceLanguageCode ? { languageCode: sourceLanguageCode } : {}) }
|
|
784
|
+
: undefined;
|
|
785
|
+
const { messageId: userMessageId } = await processMessage(
|
|
786
|
+
conversationId,
|
|
787
|
+
content,
|
|
788
|
+
attachmentIds,
|
|
789
|
+
{
|
|
790
|
+
transport: {
|
|
791
|
+
channelId: sourceChannel,
|
|
792
|
+
hints: metadataHints.length > 0 ? metadataHints : undefined,
|
|
793
|
+
uxBrief: metadataUxBrief,
|
|
794
|
+
},
|
|
795
|
+
assistantId,
|
|
796
|
+
guardianContext: toGuardianRuntimeContext(sourceChannel, guardianCtx),
|
|
797
|
+
...(cmdIntent ? { commandIntent: cmdIntent } : {}),
|
|
798
|
+
},
|
|
799
|
+
sourceChannel,
|
|
800
|
+
);
|
|
801
|
+
channelDeliveryStore.linkMessage(eventId, userMessageId);
|
|
802
|
+
channelDeliveryStore.markProcessed(eventId);
|
|
803
|
+
|
|
804
|
+
if (replyCallbackUrl) {
|
|
805
|
+
await deliverReplyViaCallback(
|
|
806
|
+
conversationId,
|
|
807
|
+
externalChatId,
|
|
808
|
+
replyCallbackUrl,
|
|
809
|
+
bearerToken,
|
|
810
|
+
assistantId,
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
} catch (err) {
|
|
814
|
+
log.error({ err, conversationId }, 'Background channel message processing failed');
|
|
815
|
+
channelDeliveryStore.recordProcessingFailure(eventId, err);
|
|
816
|
+
}
|
|
817
|
+
})();
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// ---------------------------------------------------------------------------
|
|
821
|
+
// Orchestrator-backed channel processing with approval prompt delivery
|
|
822
|
+
// ---------------------------------------------------------------------------
|
|
823
|
+
|
|
824
|
+
interface ApprovalProcessingParams {
|
|
825
|
+
orchestrator: RunOrchestrator;
|
|
826
|
+
conversationId: string;
|
|
827
|
+
eventId: string;
|
|
828
|
+
content: string;
|
|
829
|
+
attachmentIds?: string[];
|
|
830
|
+
externalChatId: string;
|
|
831
|
+
sourceChannel: ChannelId;
|
|
832
|
+
replyCallbackUrl: string;
|
|
833
|
+
bearerToken?: string;
|
|
834
|
+
guardianCtx: GuardianContext;
|
|
835
|
+
assistantId: string;
|
|
836
|
+
metadataHints: string[];
|
|
837
|
+
metadataUxBrief?: string;
|
|
838
|
+
commandIntent?: Record<string, unknown>;
|
|
839
|
+
sourceLanguageCode?: string;
|
|
840
|
+
approvalCopyGenerator?: ApprovalCopyGenerator;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Process a channel message using the run orchestrator so that
|
|
845
|
+
* `confirmation_request` events are intercepted and written to the
|
|
846
|
+
* runs store. Polls the run until it reaches a terminal state,
|
|
847
|
+
* sending approval prompts when `needs_confirmation` is detected.
|
|
848
|
+
*
|
|
849
|
+
* When the actor is a non-guardian:
|
|
850
|
+
* - `forceStrictSideEffects` is set on the run so all side-effect tools
|
|
851
|
+
* trigger the confirmation flow
|
|
852
|
+
* - Approval prompts are routed to the guardian's chat
|
|
853
|
+
* - A `channelGuardianApprovalRequest` record is created
|
|
854
|
+
*/
|
|
855
|
+
function processChannelMessageWithApprovals(params: ApprovalProcessingParams): void {
|
|
856
|
+
const {
|
|
857
|
+
orchestrator,
|
|
858
|
+
conversationId,
|
|
859
|
+
eventId,
|
|
860
|
+
content,
|
|
861
|
+
attachmentIds,
|
|
862
|
+
externalChatId,
|
|
863
|
+
sourceChannel,
|
|
864
|
+
replyCallbackUrl,
|
|
865
|
+
bearerToken,
|
|
866
|
+
guardianCtx,
|
|
867
|
+
assistantId,
|
|
868
|
+
metadataHints,
|
|
869
|
+
metadataUxBrief,
|
|
870
|
+
commandIntent,
|
|
871
|
+
sourceLanguageCode,
|
|
872
|
+
approvalCopyGenerator,
|
|
873
|
+
} = params;
|
|
874
|
+
|
|
875
|
+
const isNonGuardian = guardianCtx.actorRole === 'non-guardian';
|
|
876
|
+
const isUnverifiedChannel = guardianCtx.actorRole === 'unverified_channel';
|
|
877
|
+
|
|
878
|
+
const cmdIntent = commandIntent && typeof commandIntent.type === 'string'
|
|
879
|
+
? { type: commandIntent.type as string, ...(typeof commandIntent.payload === 'string' ? { payload: commandIntent.payload } : {}), ...(sourceLanguageCode ? { languageCode: sourceLanguageCode } : {}) }
|
|
880
|
+
: undefined;
|
|
881
|
+
|
|
882
|
+
(async () => {
|
|
883
|
+
try {
|
|
884
|
+
const turnChannelContext: TurnChannelContext = {
|
|
885
|
+
userMessageChannel: sourceChannel,
|
|
886
|
+
assistantMessageChannel: sourceChannel,
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
const run = await orchestrator.startRun(
|
|
890
|
+
conversationId,
|
|
891
|
+
content,
|
|
892
|
+
attachmentIds,
|
|
893
|
+
{
|
|
894
|
+
...((isNonGuardian || isUnverifiedChannel) ? { forceStrictSideEffects: true } : {}),
|
|
895
|
+
sourceChannel,
|
|
896
|
+
hints: metadataHints.length > 0 ? metadataHints : undefined,
|
|
897
|
+
uxBrief: metadataUxBrief,
|
|
898
|
+
assistantId,
|
|
899
|
+
guardianContext: toGuardianRuntimeContext(sourceChannel, guardianCtx),
|
|
900
|
+
...(cmdIntent ? { commandIntent: cmdIntent } : {}),
|
|
901
|
+
turnChannelContext,
|
|
902
|
+
},
|
|
903
|
+
);
|
|
904
|
+
|
|
905
|
+
// Poll the run until it reaches a terminal state, delivering approval
|
|
906
|
+
// prompts when it transitions to needs_confirmation.
|
|
907
|
+
const startTime = Date.now();
|
|
908
|
+
const pollMaxWait = getEffectivePollMaxWait();
|
|
909
|
+
let lastStatus = run.status;
|
|
910
|
+
// Track whether a post-decision delivery path is guaranteed for this
|
|
911
|
+
// run. Set to true only when the approval prompt is successfully
|
|
912
|
+
// delivered (guardian or standard path), meaning
|
|
913
|
+
// handleApprovalInterception will schedule schedulePostDecisionDelivery
|
|
914
|
+
// when a decision arrives. Auto-deny paths (unverified channel, prompt
|
|
915
|
+
// delivery failures) do NOT set this flag because no post-decision
|
|
916
|
+
// delivery is scheduled in those cases.
|
|
917
|
+
let hasPostDecisionDelivery = false;
|
|
918
|
+
|
|
919
|
+
while (Date.now() - startTime < pollMaxWait) {
|
|
920
|
+
await new Promise((resolve) => setTimeout(resolve, RUN_POLL_INTERVAL_MS));
|
|
921
|
+
|
|
922
|
+
const current = orchestrator.getRun(run.id);
|
|
923
|
+
if (!current) break;
|
|
924
|
+
|
|
925
|
+
if (current.status === 'needs_confirmation' && lastStatus !== 'needs_confirmation') {
|
|
926
|
+
const pending = getPendingConfirmationsByConversation(conversationId);
|
|
927
|
+
|
|
928
|
+
if (isUnverifiedChannel && pending.length > 0) {
|
|
929
|
+
// Unverified channel — auto-deny the sensitive action (fail-closed).
|
|
930
|
+
handleChannelDecision(
|
|
931
|
+
conversationId,
|
|
932
|
+
{ action: 'reject', source: 'plain_text' },
|
|
933
|
+
orchestrator,
|
|
934
|
+
buildGuardianDenyContext(
|
|
935
|
+
pending[0].toolName,
|
|
936
|
+
guardianCtx.denialReason ?? 'no_binding',
|
|
937
|
+
sourceChannel,
|
|
938
|
+
),
|
|
939
|
+
);
|
|
940
|
+
} else if (isNonGuardian && guardianCtx.guardianChatId && pending.length > 0) {
|
|
941
|
+
// Non-guardian actor: route the approval prompt to the guardian's chat
|
|
942
|
+
const guardianPrompt = buildGuardianApprovalPrompt(
|
|
943
|
+
pending[0],
|
|
944
|
+
guardianCtx.requesterIdentifier ?? 'Unknown user',
|
|
945
|
+
);
|
|
946
|
+
const uiMetadata = buildApprovalUIMetadata(guardianPrompt, pending[0]);
|
|
947
|
+
|
|
948
|
+
// Persist the guardian approval request so we can look it up when
|
|
949
|
+
// the guardian responds from their chat.
|
|
950
|
+
const approvalReqRecord = createApprovalRequest({
|
|
951
|
+
runId: run.id,
|
|
952
|
+
conversationId,
|
|
953
|
+
assistantId,
|
|
954
|
+
channel: sourceChannel,
|
|
955
|
+
requesterExternalUserId: guardianCtx.requesterExternalUserId ?? '',
|
|
956
|
+
requesterChatId: guardianCtx.requesterChatId ?? externalChatId,
|
|
957
|
+
guardianExternalUserId: guardianCtx.guardianExternalUserId ?? '',
|
|
958
|
+
guardianChatId: guardianCtx.guardianChatId,
|
|
959
|
+
toolName: pending[0].toolName,
|
|
960
|
+
riskLevel: pending[0].riskLevel,
|
|
961
|
+
expiresAt: Date.now() + GUARDIAN_APPROVAL_TTL_MS,
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
const guardianNotified = await deliverGeneratedApprovalPrompt({
|
|
965
|
+
replyCallbackUrl,
|
|
966
|
+
chatId: guardianCtx.guardianChatId,
|
|
967
|
+
sourceChannel,
|
|
968
|
+
assistantId,
|
|
969
|
+
bearerToken,
|
|
970
|
+
prompt: guardianPrompt,
|
|
971
|
+
uiMetadata,
|
|
972
|
+
messageContext: {
|
|
973
|
+
scenario: 'guardian_prompt',
|
|
974
|
+
toolName: pending[0].toolName,
|
|
975
|
+
requesterIdentifier: guardianCtx.requesterIdentifier ?? 'Unknown user',
|
|
976
|
+
},
|
|
977
|
+
approvalCopyGenerator,
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
if (guardianNotified) {
|
|
981
|
+
hasPostDecisionDelivery = true;
|
|
982
|
+
} else {
|
|
983
|
+
// Deny the approval and the underlying run — fail-closed. If
|
|
984
|
+
// the prompt could not be delivered, the guardian will never see
|
|
985
|
+
// it, so the safest action is to deny rather than cancel (which
|
|
986
|
+
// would allow requester fallback).
|
|
987
|
+
updateApprovalDecision(approvalReqRecord.id, { status: 'denied' });
|
|
988
|
+
handleChannelDecision(
|
|
989
|
+
conversationId,
|
|
990
|
+
{ action: 'reject', source: 'plain_text' },
|
|
991
|
+
orchestrator,
|
|
992
|
+
buildPromptDeliveryFailureContext(pending[0].toolName),
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// Only notify the requester if the guardian prompt was actually delivered
|
|
997
|
+
if (guardianNotified) {
|
|
998
|
+
try {
|
|
999
|
+
const forwardedText = await composeApprovalMessageGenerative({
|
|
1000
|
+
scenario: 'guardian_request_forwarded',
|
|
1001
|
+
toolName: pending[0].toolName,
|
|
1002
|
+
channel: sourceChannel,
|
|
1003
|
+
}, {}, approvalCopyGenerator);
|
|
1004
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
1005
|
+
chatId: guardianCtx.requesterChatId ?? externalChatId,
|
|
1006
|
+
text: forwardedText,
|
|
1007
|
+
assistantId,
|
|
1008
|
+
}, bearerToken);
|
|
1009
|
+
} catch (err) {
|
|
1010
|
+
log.error({ err, runId: run.id }, 'Failed to notify requester of pending guardian approval');
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
} else {
|
|
1014
|
+
// Guardian actor or no guardian binding: standard approval prompt
|
|
1015
|
+
// sent to the requester's own chat.
|
|
1016
|
+
const prompt = getChannelApprovalPrompt(conversationId);
|
|
1017
|
+
if (prompt && pending.length > 0) {
|
|
1018
|
+
const uiMetadata = buildApprovalUIMetadata(prompt, pending[0]);
|
|
1019
|
+
const delivered = await deliverGeneratedApprovalPrompt({
|
|
1020
|
+
replyCallbackUrl,
|
|
1021
|
+
chatId: externalChatId,
|
|
1022
|
+
sourceChannel,
|
|
1023
|
+
assistantId,
|
|
1024
|
+
bearerToken,
|
|
1025
|
+
prompt,
|
|
1026
|
+
uiMetadata,
|
|
1027
|
+
messageContext: {
|
|
1028
|
+
scenario: 'standard_prompt',
|
|
1029
|
+
toolName: pending[0].toolName,
|
|
1030
|
+
},
|
|
1031
|
+
approvalCopyGenerator,
|
|
1032
|
+
});
|
|
1033
|
+
if (delivered) {
|
|
1034
|
+
hasPostDecisionDelivery = true;
|
|
1035
|
+
} else {
|
|
1036
|
+
// Fail-closed: if we cannot deliver the approval prompt, the
|
|
1037
|
+
// user will never see it and the run would hang indefinitely
|
|
1038
|
+
// in needs_confirmation. Auto-deny to avoid silent wait states.
|
|
1039
|
+
log.error(
|
|
1040
|
+
{ runId: run.id, conversationId },
|
|
1041
|
+
'Failed to deliver standard approval prompt; auto-denying (fail-closed)',
|
|
1042
|
+
);
|
|
1043
|
+
handleChannelDecision(
|
|
1044
|
+
conversationId,
|
|
1045
|
+
{ action: 'reject', source: 'plain_text' },
|
|
1046
|
+
orchestrator,
|
|
1047
|
+
buildPromptDeliveryFailureContext(pending[0].toolName),
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
lastStatus = current.status;
|
|
1055
|
+
|
|
1056
|
+
if (current.status === 'completed' || current.status === 'failed') {
|
|
1057
|
+
break;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// Only mark processed and deliver the final reply when the run has
|
|
1062
|
+
// actually reached a terminal state.
|
|
1063
|
+
const finalRun = orchestrator.getRun(run.id);
|
|
1064
|
+
const isTerminal = finalRun?.status === 'completed' || finalRun?.status === 'failed';
|
|
1065
|
+
|
|
1066
|
+
if (isTerminal) {
|
|
1067
|
+
// Link the inbound event to the user message created by the run so
|
|
1068
|
+
// that edit lookups and dead letter replay work correctly.
|
|
1069
|
+
if (run.messageId) {
|
|
1070
|
+
channelDeliveryStore.linkMessage(eventId, run.messageId);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
channelDeliveryStore.markProcessed(eventId);
|
|
1074
|
+
|
|
1075
|
+
// Deliver the final assistant reply exactly once. The post-decision
|
|
1076
|
+
// poll in schedulePostDecisionDelivery races with this path; the
|
|
1077
|
+
// claimRunDelivery guard ensures only the winner sends the reply.
|
|
1078
|
+
// If delivery fails, release the claim so the other poller can retry
|
|
1079
|
+
// rather than permanently losing the reply.
|
|
1080
|
+
if (channelDeliveryStore.claimRunDelivery(run.id)) {
|
|
1081
|
+
try {
|
|
1082
|
+
await deliverReplyViaCallback(
|
|
1083
|
+
conversationId,
|
|
1084
|
+
externalChatId,
|
|
1085
|
+
replyCallbackUrl,
|
|
1086
|
+
bearerToken,
|
|
1087
|
+
assistantId,
|
|
1088
|
+
);
|
|
1089
|
+
} catch (deliveryErr) {
|
|
1090
|
+
channelDeliveryStore.resetRunDeliveryClaim(run.id);
|
|
1091
|
+
throw deliveryErr;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// If this was a non-guardian run that went through guardian approval,
|
|
1096
|
+
// also notify the guardian's chat about the outcome.
|
|
1097
|
+
if (isNonGuardian && guardianCtx.guardianChatId) {
|
|
1098
|
+
const approvalReq = getPendingApprovalForRun(run.id);
|
|
1099
|
+
if (approvalReq) {
|
|
1100
|
+
// The approval was resolved (run completed or failed) — mark it
|
|
1101
|
+
const outcomeStatus = finalRun?.status === 'completed' ? 'approved' as const : 'denied' as const;
|
|
1102
|
+
updateApprovalDecision(approvalReq.id, { status: outcomeStatus });
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
} else if (
|
|
1106
|
+
finalRun?.status === 'needs_confirmation' ||
|
|
1107
|
+
(hasPostDecisionDelivery && finalRun?.status === 'running')
|
|
1108
|
+
) {
|
|
1109
|
+
// The run is either still waiting for an approval decision or was
|
|
1110
|
+
// recently approved and has resumed execution. In both cases, mark
|
|
1111
|
+
// the event as processed rather than failed:
|
|
1112
|
+
//
|
|
1113
|
+
// - needs_confirmation: the run will resume when the user clicks
|
|
1114
|
+
// approve/reject, and `handleApprovalInterception` will deliver
|
|
1115
|
+
// the reply via `schedulePostDecisionDelivery`.
|
|
1116
|
+
//
|
|
1117
|
+
// - running (after successful prompt delivery): an approval was
|
|
1118
|
+
// applied near the poll deadline and the run resumed but hasn't
|
|
1119
|
+
// reached terminal state yet. `handleApprovalInterception` has
|
|
1120
|
+
// already scheduled post-decision delivery, so the final reply
|
|
1121
|
+
// will be delivered. This condition is only true when the approval
|
|
1122
|
+
// prompt was actually delivered (not in auto-deny paths), ensuring
|
|
1123
|
+
// we don't suppress retry/dead-letter for cases where no
|
|
1124
|
+
// post-decision delivery path exists.
|
|
1125
|
+
//
|
|
1126
|
+
// Marking either state as failed would cause the generic retry sweep
|
|
1127
|
+
// to replay through `processMessage`, which throws "Session is
|
|
1128
|
+
// already processing a message" and dead-letters a valid conversation.
|
|
1129
|
+
log.warn(
|
|
1130
|
+
{ runId: run.id, status: finalRun.status, conversationId, hasPostDecisionDelivery },
|
|
1131
|
+
'Approval-path poll loop timed out while run is in approval-related state; marking event as processed',
|
|
1132
|
+
);
|
|
1133
|
+
channelDeliveryStore.markProcessed(eventId);
|
|
1134
|
+
} else {
|
|
1135
|
+
// The run is in a non-terminal, non-approval state (e.g. running
|
|
1136
|
+
// without prior approval, needs_secret, or disappeared). Record a
|
|
1137
|
+
// processing failure so the retry/dead-letter machinery can handle it.
|
|
1138
|
+
const timeoutErr = new Error(
|
|
1139
|
+
`Approval poll timeout: run did not reach terminal state within ${pollMaxWait}ms (status: ${finalRun?.status ?? 'null'})`,
|
|
1140
|
+
);
|
|
1141
|
+
log.warn(
|
|
1142
|
+
{ runId: run.id, status: finalRun?.status, conversationId },
|
|
1143
|
+
'Approval-path poll loop timed out before run reached terminal state',
|
|
1144
|
+
);
|
|
1145
|
+
channelDeliveryStore.recordProcessingFailure(eventId, timeoutErr);
|
|
1146
|
+
}
|
|
1147
|
+
} catch (err) {
|
|
1148
|
+
log.error({ err, conversationId }, 'Approval-aware channel message processing failed');
|
|
1149
|
+
channelDeliveryStore.recordProcessingFailure(eventId, err);
|
|
1150
|
+
}
|
|
1151
|
+
})();
|
|
1152
|
+
}
|