@vellumai/assistant 0.11.2 → 0.11.3-staging.2
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 +58 -3
- package/docs/trusted-contact-access.md +9 -5
- package/docs/vellum-doctor.md +195 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +39 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +10 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +6 -0
- package/node_modules/@vellumai/ces-client/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/src/rpc-client.ts +19 -2
- package/node_modules/@vellumai/environments/package.json +2 -1
- package/node_modules/@vellumai/environments/src/__tests__/cloud-assistant-hub-url.test.ts +38 -0
- package/node_modules/@vellumai/environments/src/__tests__/install-layout.test.ts +98 -0
- package/node_modules/@vellumai/environments/src/__tests__/package-boundary.test.ts +5 -5
- package/node_modules/@vellumai/environments/src/index.ts +17 -6
- package/node_modules/@vellumai/environments/src/install-layout.ts +49 -0
- package/node_modules/@vellumai/environments/src/seeds.ts +29 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +39 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +10 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +6 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +4 -0
- package/node_modules/@vellumai/gateway-client/src/verification-session-contract.ts +91 -6
- package/node_modules/@vellumai/ipc-server-utils/src/endpoint.test.ts +36 -0
- package/node_modules/@vellumai/ipc-server-utils/src/endpoint.ts +142 -0
- package/node_modules/@vellumai/ipc-server-utils/src/index.ts +2 -0
- package/node_modules/@vellumai/ipc-server-utils/src/listen-options.ts +3 -0
- package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +15 -1
- package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +17 -2
- package/node_modules/@vellumai/service-contracts/src/channels.ts +39 -0
- package/node_modules/@vellumai/service-contracts/src/ingress.ts +10 -0
- package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +6 -0
- package/node_modules/@vellumai/slack-text/src/index.ts +16 -2
- package/openapi.yaml +589 -30
- package/package.json +1 -1
- package/src/__tests__/agent-image-optimize.test.ts +4 -4
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +113 -66
- package/src/__tests__/agent-loop-override-profile.test.ts +38 -48
- package/src/__tests__/agent-loop-subagent-billing-headers.test.ts +166 -0
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +1 -0
- package/src/__tests__/agent-wake-override-profile.test.ts +1 -0
- package/src/__tests__/answered-question-persistence.test.ts +240 -0
- package/src/__tests__/anthropic-provider.test.ts +165 -30
- package/src/__tests__/app-control-flow.test.ts +9 -8
- package/src/__tests__/assistant-event-hub.test.ts +48 -0
- package/src/__tests__/attachments-store-heic-normalize.test.ts +16 -12
- package/src/__tests__/attachments-store.test.ts +130 -68
- package/src/__tests__/attachments.test.ts +36 -36
- package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
- package/src/__tests__/call-start-guardian-guard.test.ts +18 -4
- package/src/__tests__/canned-reply-release.test.ts +86 -0
- package/src/__tests__/channel-guardian.test.ts +59 -29
- package/src/__tests__/channel-readiness-telegram-remote.test.ts +154 -0
- package/src/__tests__/channel-setup-panel-ack.test.ts +6 -5
- package/src/__tests__/client-os-metadata-persistence.test.ts +103 -4
- package/src/__tests__/compactor-image-manifest-trust.test.ts +1 -1
- package/src/__tests__/consult-deadline.test.ts +65 -0
- package/src/__tests__/conversation-agent-loop.test.ts +438 -2
- package/src/__tests__/conversation-disk-view-integration.test.ts +5 -1
- package/src/__tests__/conversation-disk-view.test.ts +4 -4
- package/src/__tests__/conversation-error.test.ts +22 -3
- package/src/__tests__/conversation-fork-crud.test.ts +5 -1
- package/src/__tests__/conversation-fork-referential.test.ts +352 -0
- package/src/__tests__/conversation-fork-retrospective.test.ts +5 -1
- package/src/__tests__/conversation-group-tools.test.ts +32 -2
- package/src/__tests__/conversation-inference-profile-route.test.ts +27 -1
- package/src/__tests__/conversation-initial-prompt.test.ts +51 -22
- package/src/__tests__/conversation-key-store-bootstrap-cleanup.test.ts +119 -0
- package/src/__tests__/conversation-key-store-origin.test.ts +105 -0
- package/src/__tests__/conversation-list-recency-ordering.test.ts +104 -0
- package/src/__tests__/conversation-origin-at-creation.test.ts +153 -0
- package/src/__tests__/conversation-origin-channel-filter.test.ts +95 -0
- package/src/__tests__/conversation-placement-promotion.test.ts +172 -0
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +123 -0
- package/src/__tests__/conversation-queue.test.ts +433 -1
- package/src/__tests__/conversation-retry-route.test.ts +3 -1
- package/src/__tests__/conversation-routes-hidden-queue.test.ts +64 -1
- package/src/__tests__/conversation-store-ephemeral.test.ts +4 -0
- package/src/__tests__/conversation-store.test.ts +26 -10
- package/src/__tests__/conversation-summarize-route.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +15 -6
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -5
- package/src/__tests__/conversation-surfaces-app-control.test.ts +12 -11
- package/src/__tests__/conversation-surfaces-app-open.test.ts +11 -5
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +5 -4
- package/src/__tests__/conversation-surfaces-history-restored-completion.test.ts +967 -0
- package/src/__tests__/conversation-surfaces-persisted-info.test.ts +312 -0
- package/src/__tests__/conversation-surfaces-queued-emit.test.ts +6 -5
- package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +12 -8
- package/src/__tests__/conversation-surfaces-standalone.test.ts +21 -11
- package/src/__tests__/conversation-surfaces-state-update.test.ts +7 -11
- package/src/__tests__/conversation-surfaces-table-action.test.ts +7 -6
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +11 -6
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +9 -6
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +70 -7
- package/src/__tests__/conversation-wait-for-idle.test.ts +6 -11
- package/src/__tests__/credential-prompt-route.test.ts +57 -7
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/cu-unified-flow.test.ts +89 -28
- package/src/__tests__/custom-group-listing-visibility.test.ts +1 -1
- package/src/__tests__/db-schedule-syntax-migration.test.ts +4 -1
- package/src/__tests__/discord-access-request-privacy.test.ts +366 -0
- package/src/__tests__/discord-requester-notice-privacy.test.ts +87 -0
- package/src/__tests__/disk-pressure-guard.test.ts +55 -55
- package/src/__tests__/disk-pressure-routes.test.ts +10 -10
- package/src/__tests__/disk-pressure-tools.test.ts +2 -2
- package/src/__tests__/disk-usage.test.ts +35 -20
- package/src/__tests__/document-sync-tags.test.ts +379 -0
- package/src/__tests__/document-update-default-surface.test.ts +28 -0
- package/src/__tests__/dynamic-skill-background-guard.test.ts +2 -2
- package/src/__tests__/emit-signal-routing-intent.test.ts +3 -4
- package/src/__tests__/ensure-conversation-exists.test.ts +8 -5
- package/src/__tests__/events-dev-bypass-actor.test.ts +183 -1
- package/src/__tests__/evict-conversations-for-reload.test.ts +90 -0
- package/src/__tests__/file-list-tool.test.ts +12 -10
- package/src/__tests__/file-ops-service.test.ts +151 -44
- package/src/__tests__/file-read-container-boundary.test.ts +137 -0
- package/src/__tests__/file-read-tool.test.ts +21 -9
- package/src/__tests__/file-url-path-guard.test.ts +83 -0
- package/src/__tests__/filesystem-tools.test.ts +82 -66
- package/src/__tests__/guardian-outbound-http.test.ts +8 -8
- package/src/__tests__/helpers/gateway-verification-sessions-stub.ts +14 -1
- package/src/__tests__/helpers/hub-clients.ts +42 -0
- package/src/__tests__/helpers/mock-conversation.ts +46 -0
- package/src/__tests__/helpers/verification-sessions-ipc-sim.ts +61 -57
- package/src/__tests__/history-repair.test.ts +112 -12
- package/src/__tests__/host-bash-proxy.test.ts +10 -5
- package/src/__tests__/host-bash-routes.test.ts +67 -0
- package/src/__tests__/host-browser-proxy.test.ts +2 -2
- package/src/__tests__/host-cu-proxy.test.ts +3 -7
- package/src/__tests__/host-file-proxy.test.ts +3 -7
- package/src/__tests__/host-file-read-tool.test.ts +5 -1
- package/src/__tests__/host-transfer-proxy-targeted.test.ts +6 -4
- package/src/__tests__/http-conversation-lineage.test.ts +2 -5
- package/src/__tests__/http-user-message-parity.test.ts +44 -0
- package/src/__tests__/identity-routes.test.ts +13 -13
- package/src/__tests__/image-conversion.test.ts +29 -26
- package/src/__tests__/image-source-path-reinject.test.ts +2 -2
- package/src/__tests__/inference-profile-session-handler.test.ts +95 -0
- package/src/__tests__/inference-profile-session-ipc.test.ts +25 -0
- package/src/__tests__/introduction-card-resolver.test.ts +6 -1
- package/src/__tests__/list-messages-attachments.test.ts +56 -32
- package/src/__tests__/list-messages-background-tool-completion.test.ts +4 -4
- package/src/__tests__/list-messages-client-message-id.test.ts +2 -2
- package/src/__tests__/list-messages-hidden-metadata.test.ts +16 -16
- package/src/__tests__/list-messages-page-latest.test.ts +61 -61
- package/src/__tests__/list-messages-provider-error.test.ts +6 -6
- package/src/__tests__/list-messages-queued.test.ts +49 -6
- package/src/__tests__/list-messages-system-card.test.ts +4 -4
- package/src/__tests__/list-messages-tool-merge.test.ts +9 -9
- package/src/__tests__/llm-resolver-override-or-default.test.ts +141 -0
- package/src/__tests__/llm-usage-store.test.ts +55 -0
- package/src/__tests__/manual-token-reconciliation.test.ts +45 -0
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +133 -0
- package/src/__tests__/media-stream-server-integration.test.ts +36 -0
- package/src/__tests__/migration-export-http.test.ts +1 -1
- package/src/__tests__/migration-import-commit-http.test.ts +1 -1
- package/src/__tests__/migration-import-preflight-http.test.ts +1 -1
- package/src/__tests__/migration-validate-http.test.ts +1 -1
- package/src/__tests__/mtime-cache.test.ts +65 -3
- package/src/__tests__/notification-platform-adapter.test.ts +104 -0
- package/src/__tests__/notification-schedule-notify-dedup.test.ts +1 -1
- package/src/__tests__/oauth-provider-profiles.test.ts +4 -2
- package/src/__tests__/oauth-provider-seed-logos.test.ts +65 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +17 -0
- package/src/__tests__/oauth-providers-routes.test.ts +1 -0
- package/src/__tests__/openai-responses-prompt-cache.test.ts +5 -2
- package/src/__tests__/path-policy.test.ts +164 -1
- package/src/__tests__/persist-media-references.test.ts +2 -2
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +5 -5
- package/src/__tests__/persist-unsendable-image.test.ts +23 -23
- package/src/__tests__/persona-resolver.test.ts +154 -0
- package/src/__tests__/plugin-api-webhook-url.test.ts +170 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +6 -0
- package/src/__tests__/processing-flag-persist-failure.test.ts +15 -10
- package/src/__tests__/profile-availability-incomplete.test.ts +113 -0
- package/src/__tests__/profiler-routes.test.ts +13 -13
- package/src/__tests__/prompt-cache-cross-turn-stability.test.ts +593 -0
- package/src/__tests__/proxy-approval-callback.test.ts +3 -3
- package/src/__tests__/queued-message-cancel-and-steer.test.ts +189 -0
- package/src/__tests__/queued-message-steer-actor-scoping.test.ts +164 -0
- package/src/__tests__/resolve-trust-class.test.ts +20 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +1 -1
- package/src/__tests__/run-due-schedules.test.ts +174 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +15 -7
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +166 -0
- package/src/__tests__/schedule-routes.test.ts +730 -8
- package/src/__tests__/schedule-store.test.ts +540 -0
- package/src/__tests__/schedule-tools.test.ts +103 -0
- package/src/__tests__/scheduler-wake.test.ts +5 -0
- package/src/__tests__/secret-routes-platform-proxy.test.ts +29 -1
- package/src/__tests__/secure-keys.test.ts +91 -0
- package/src/__tests__/server-history-render.test.ts +97 -7
- package/src/__tests__/size-guard.test.ts +8 -8
- package/src/__tests__/skill-docs-sync-guard.test.ts +3 -2
- package/src/__tests__/skill-tool-factory.test.ts +247 -0
- package/src/__tests__/skills-install-staging.test.ts +15 -6
- package/src/__tests__/slack-edit-ordering-characterization.test.ts +103 -0
- package/src/__tests__/slack-inbound-verification.test.ts +4 -1
- package/src/__tests__/slack-mention-binding-invariance.test.ts +103 -0
- package/src/__tests__/slack-mention-provider-leak-audit.test.ts +135 -0
- package/src/__tests__/starter-task-flow.test.ts +5 -4
- package/src/__tests__/steer-on-enqueue-question.test.ts +3 -2
- package/src/__tests__/stt-language-catalog-parity.test.ts +38 -0
- package/src/__tests__/subagent-allowlist-validation.test.ts +26 -4
- package/src/__tests__/subagent-cron-run-attribution.test.ts +301 -0
- package/src/__tests__/subagent-disposal.test.ts +10 -0
- package/src/__tests__/subagent-fork-notifications.test.ts +10 -0
- package/src/__tests__/subagent-fork-prompt-role.test.ts +85 -11
- package/src/__tests__/subagent-fork-spawn.test.ts +15 -7
- package/src/__tests__/subagent-manager-notify.test.ts +148 -0
- package/src/__tests__/subagent-notify-parent.test.ts +1 -1
- package/src/__tests__/subagent-persistence.test.ts +141 -0
- package/src/__tests__/subagent-role-registry.test.ts +172 -63
- package/src/__tests__/subagent-role-resolution.test.ts +88 -0
- package/src/__tests__/subagent-spawn-and-await.test.ts +48 -0
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +111 -10
- package/src/__tests__/subagent-terminal-message.test.ts +88 -1
- package/src/__tests__/subagent-tool-gate-mode.test.ts +225 -10
- package/src/__tests__/subagent-tools.test.ts +1901 -108
- package/src/__tests__/surface-completion-compaction-boundary.test.ts +124 -0
- package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +298 -0
- package/src/__tests__/system-prompt.test.ts +82 -7
- package/src/__tests__/tool-grant-request-escalation.test.ts +40 -1
- package/src/__tests__/tool-result-spool.test.ts +74 -18
- package/src/__tests__/tool-side-effects-documents.test.ts +174 -0
- package/src/__tests__/tool-side-effects-hook-failures.test.ts +129 -0
- package/src/__tests__/tools-audio-read.test.ts +10 -10
- package/src/__tests__/tools-get-route.test.ts +85 -12
- package/src/__tests__/tools-list-cli.test.ts +171 -0
- package/src/__tests__/top-level-renderer.test.ts +54 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +68 -1
- package/src/__tests__/trusted-contact-multichannel.test.ts +4 -1
- package/src/__tests__/turn-tail-chain.test.ts +135 -0
- package/src/__tests__/ui-choice-copy-surfaces.test.ts +6 -4
- package/src/__tests__/ui-visual-surface.test.ts +5 -4
- package/src/__tests__/ui-work-result-surface.test.ts +5 -4
- package/src/__tests__/unified-turn-context-visible-app.test.ts +7 -8
- package/src/__tests__/usage-routes.test.ts +62 -0
- package/src/__tests__/verification-outbound-delivery.test.ts +176 -0
- package/src/__tests__/visible-app-context.test.ts +20 -4
- package/src/__tests__/web-search-history.test.ts +96 -0
- package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +6 -6
- package/src/__tests__/workspace-migration-140-repair-seed-pinned-memory-v3-live.test.ts +330 -0
- package/src/acp/failure-error.ts +3 -5
- package/src/agent/attachments.ts +26 -24
- package/src/agent/history-repair/history-repair.ts +27 -26
- package/src/agent/image-optimize.ts +6 -6
- package/src/agent/loop.ts +40 -22
- package/src/agent/message-types.ts +3 -3
- package/src/api/constants/document-tools.ts +55 -0
- package/src/api/events/question-answered.ts +53 -0
- package/src/api/events/tool-result.ts +7 -0
- package/src/api/index.ts +13 -0
- package/src/api/responses/conversation-message.ts +11 -0
- package/src/api/surfaces-oauth-connect.test.ts +96 -0
- package/src/api/surfaces.ts +36 -8
- package/src/approvals/guardian-channel-delivery.ts +83 -0
- package/src/approvals/guardian-expiry-notifier.ts +13 -11
- package/src/approvals/guardian-request-resolvers.ts +222 -135
- package/src/calls/__tests__/voice-session-bridge.test.ts +50 -0
- package/src/calls/media-stream-server.ts +9 -1
- package/src/calls/voice-session-bridge.ts +38 -0
- package/src/channels/__tests__/gateway-verification-sessions.test.ts +3 -3
- package/src/channels/config.ts +7 -2
- package/src/channels/gateway-verification-sessions.ts +7 -2
- package/src/channels/types.ts +7 -1
- package/src/cli/commands/__tests__/cache.test.ts +7 -4
- package/src/cli/commands/__tests__/cli-test-harness.ts +82 -0
- package/src/cli/commands/__tests__/conversations-wake.test.ts +51 -6
- package/src/cli/commands/__tests__/inference-models.test.ts +14 -0
- package/src/cli/commands/__tests__/inference-profiles.test.ts +44 -1
- package/src/cli/commands/__tests__/inference-providers.test.ts +7 -0
- package/src/cli/commands/__tests__/plugins.test.ts +692 -0
- package/src/cli/commands/__tests__/schedules.test.ts +136 -37
- package/src/cli/commands/conversations.ts +7 -3
- package/src/cli/commands/credentials.help.ts +7 -0
- package/src/cli/commands/inference-profiles.ts +9 -3
- package/src/cli/commands/inference.help.ts +16 -2
- package/src/cli/commands/mcp.help.ts +4 -2
- package/src/cli/commands/memory/memory-retrospective.ts +18 -0
- package/src/cli/commands/notifications.ts +3 -6
- package/src/cli/commands/oauth/connect-surface-guidance.test.ts +16 -0
- package/src/cli/commands/oauth/connect-surface-guidance.ts +25 -7
- package/src/cli/commands/oauth/connect.test.ts +26 -1
- package/src/cli/commands/oauth/connect.ts +4 -1
- package/src/cli/commands/oauth/index.help.ts +3 -1
- package/src/cli/commands/oauth/providers.ts +3 -0
- package/src/cli/commands/plugins.help.ts +25 -2
- package/src/cli/commands/plugins.ts +289 -10
- package/src/cli/commands/schedules.help.ts +14 -12
- package/src/cli/commands/schedules.ts +19 -5
- package/src/cli/commands/tools.help.ts +4 -2
- package/src/cli/commands/tools.ts +38 -0
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +1 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +79 -0
- package/src/cli/lib/__tests__/install-from-platform.test.ts +47 -1
- package/src/cli/lib/__tests__/plugin-surfaces.test.ts +86 -7
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +122 -0
- package/src/cli/lib/install-from-github.ts +61 -0
- package/src/cli/lib/install-from-platform.ts +13 -2
- package/src/cli/lib/plugin-surfaces.ts +124 -4
- package/src/cli/lib/upgrade-plugin.ts +18 -0
- package/src/cli.ts +1 -1
- package/src/config/__tests__/deployment-context-defaults.test.ts +73 -0
- package/src/config/__tests__/memory-retrospective-schema.test.ts +40 -0
- package/src/config/__tests__/plugin-updates-schema.test.ts +43 -0
- package/src/config/__tests__/profile-tool-support.test.ts +131 -0
- package/src/config/bundled-skills/AGENTS.md +1 -1
- package/src/config/bundled-skills/app-builder/SKILL.md +1 -1
- package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +3 -3
- package/src/config/bundled-skills/schedule/SKILL.md +5 -1
- package/src/config/bundled-skills/schedule/TOOLS.json +2 -2
- package/src/config/bundled-skills/settings/TOOLS.json +2 -2
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +1 -0
- package/src/config/bundled-skills/subagent/SKILL.md +53 -22
- package/src/config/bundled-skills/subagent/TOOLS.json +14 -13
- package/src/config/bundled-skills/visualize/SKILL.md +1 -1
- package/src/config/default-profile-catalog.ts +6 -2
- package/src/config/feature-flag-registry.json +25 -17
- package/src/config/llm-resolver.ts +45 -15
- package/src/config/loader.ts +70 -1
- package/src/config/profile-tool-support.ts +110 -0
- package/src/config/sanitize-for-transfer.ts +11 -0
- package/src/config/schema.ts +4 -0
- package/src/config/schemas/__tests__/stt.test.ts +20 -5
- package/src/config/schemas/channels.ts +13 -0
- package/src/config/schemas/memory-retrospective.ts +25 -0
- package/src/config/schemas/plugin-updates.ts +55 -0
- package/src/config/schemas/services.ts +8 -4
- package/src/config/schemas/stt.ts +17 -5
- package/src/config/schemas/timeouts.ts +1 -1
- package/src/contacts/__tests__/guardian-delivery-reader.test.ts +16 -0
- package/src/contacts/guardian-delivery-reader.ts +29 -2
- package/src/context/compactor.ts +4 -4
- package/src/context/post-turn-tool-result-truncation.ts +32 -16
- package/src/context/strip-injections.ts +42 -0
- package/src/context/tool-result-spool.ts +32 -18
- package/src/credential-execution/ces-runtime.ts +3 -0
- package/src/credential-execution/client.ts +22 -1
- package/src/credential-execution/process-manager.test.ts +99 -0
- package/src/credential-execution/process-manager.ts +62 -16
- package/src/daemon/__tests__/abort-null-controller.test.ts +141 -5
- package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +8 -12
- package/src/daemon/__tests__/install-assistant-command.test.ts +234 -0
- package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +2 -4
- package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +20 -29
- package/src/daemon/channel-ui-capability.ts +4 -4
- package/src/daemon/conversation-agent-loop-handlers.ts +117 -61
- package/src/daemon/conversation-agent-loop.ts +318 -148
- package/src/daemon/conversation-attachments.ts +1 -1
- package/src/daemon/conversation-error.ts +8 -1
- package/src/daemon/conversation-initial-prompt.ts +16 -38
- package/src/daemon/conversation-lifecycle.ts +154 -32
- package/src/daemon/conversation-messaging.ts +106 -43
- package/src/daemon/conversation-process.ts +131 -33
- package/src/daemon/conversation-queue-manager.ts +10 -0
- package/src/daemon/conversation-runtime-assembly.ts +9 -4
- package/src/daemon/conversation-store.ts +52 -3
- package/src/daemon/conversation-surface-state.ts +9 -0
- package/src/daemon/conversation-surfaces.ts +403 -210
- package/src/daemon/conversation-tool-setup.ts +56 -7
- package/src/daemon/conversation-turn-finalize.ts +134 -89
- package/src/daemon/conversation.ts +111 -27
- package/src/daemon/disk-pressure-guard-lifecycle.ts +2 -2
- package/src/daemon/disk-pressure-guard.ts +3 -3
- package/src/daemon/doordash-steps.ts +5 -5
- package/src/daemon/external-plugins-bootstrap.ts +7 -1
- package/src/daemon/handlers/config-channels.ts +48 -5
- package/src/daemon/handlers/conversation-history.ts +1 -1
- package/src/daemon/handlers/conversations.ts +62 -17
- package/src/daemon/handlers/shared.ts +12 -0
- package/src/daemon/handlers/skills.ts +11 -1
- package/src/daemon/identity-helpers.ts +38 -10
- package/src/daemon/install-assistant-command.ts +322 -0
- package/src/daemon/lifecycle.ts +14 -3
- package/src/daemon/message-types/conversations.ts +6 -7
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/persist-media-references.ts +23 -14
- package/src/daemon/process-message.ts +6 -6
- package/src/daemon/tool-setup-types.ts +19 -122
- package/src/daemon/tool-side-effects.ts +17 -94
- package/src/daemon/trust-context-types.ts +39 -0
- package/src/daemon/trust-context.ts +14 -6
- package/src/daemon/turn-tail-chain.ts +73 -0
- package/src/daemon/web-search-history.ts +47 -33
- package/src/heartbeat/heartbeat-service.ts +6 -6
- package/src/ipc/__tests__/socket-path.test.ts +21 -0
- package/src/ipc/assistant-server.ts +17 -10
- package/src/ipc/cli-client.ts +10 -1
- package/src/ipc/routes/__tests__/documents-sync-ipc-routes.test.ts +56 -0
- package/src/ipc/routes/documents-sync-ipc-routes.ts +42 -0
- package/src/ipc/socket-cleanup.ts +9 -7
- package/src/ipc/socket-path.ts +4 -96
- package/src/live-voice/__tests__/live-voice-archive.test.ts +16 -16
- package/src/live-voice/__tests__/live-voice-attach-image.test.ts +209 -0
- package/src/live-voice/__tests__/live-voice-connection.test.ts +20 -0
- package/src/live-voice/__tests__/live-voice-integration.test.ts +7 -5
- package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +265 -0
- package/src/live-voice/__tests__/live-voice-stt.test.ts +6 -1
- package/src/live-voice/__tests__/live-voice-vad.test.ts +35 -15
- package/src/live-voice/__tests__/protocol.test.ts +52 -0
- package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +3 -0
- package/src/live-voice/live-voice-archive.ts +13 -13
- package/src/live-voice/live-voice-connection.ts +7 -1
- package/src/live-voice/live-voice-photo.ts +169 -0
- package/src/live-voice/live-voice-session.ts +108 -4
- package/src/live-voice/protocol.ts +89 -1
- package/src/mcp/client.ts +10 -0
- package/src/messaging/providers/__tests__/callback-routing.test.ts +5 -1
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +66 -4
- package/src/messaging/providers/callback-routing.ts +1 -0
- package/src/messaging/providers/discord/api.ts +312 -0
- package/src/messaging/providers/discord/dm-delivery.test.ts +204 -0
- package/src/messaging/providers/discord/render.test.ts +204 -0
- package/src/messaging/providers/discord/render.ts +195 -0
- package/src/messaging/providers/discord/send.test.ts +179 -0
- package/src/messaging/providers/discord/send.ts +212 -0
- package/src/messaging/providers/discord/transport.ts +81 -0
- package/src/messaging/providers/index.ts +3 -1
- package/src/messaging/providers/retry-policy.test.ts +247 -0
- package/src/messaging/providers/retry-policy.ts +206 -0
- package/src/messaging/providers/slack/mention-source.test.ts +285 -0
- package/src/messaging/providers/slack/mention-source.ts +287 -0
- package/src/messaging/providers/telegram-bot/api.ts +47 -130
- package/src/messaging/providers/whatsapp/api.ts +47 -136
- package/src/monitoring/__tests__/db-integrity-sample.test.ts +4 -1
- package/src/monitoring/__tests__/plugin-auto-update.test.ts +332 -0
- package/src/monitoring/db-integrity-sample.ts +6 -1
- package/src/monitoring/plugin-auto-update.ts +359 -0
- package/src/monitoring/resource-sampler.ts +21 -5
- package/src/monitoring/worker.ts +13 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +375 -2
- package/src/notifications/__tests__/broadcaster.test.ts +37 -1
- package/src/notifications/__tests__/copy-composer.test.ts +165 -0
- package/src/notifications/__tests__/emit-signal-pipeline-failure.test.ts +205 -0
- package/src/notifications/__tests__/notification-utils.test.ts +249 -0
- package/src/notifications/adapters/platform.ts +31 -2
- package/src/notifications/assistant-reply-producer.ts +79 -32
- package/src/notifications/broadcaster.ts +30 -10
- package/src/notifications/copy-composer.ts +69 -0
- package/src/notifications/emit-signal.ts +82 -15
- package/src/notifications/events-store.ts +10 -2
- package/src/notifications/home-feed-side-effect.ts +6 -3
- package/src/notifications/notification-utils.ts +173 -5
- package/src/notifications/signal.ts +12 -0
- package/src/oauth/AGENTS.md +4 -2
- package/src/oauth/byo-connection.ts +6 -1
- package/src/oauth/credential-token-resolver.ts +3 -10
- package/src/oauth/manual-token-connection.ts +29 -64
- package/src/oauth/manual-token-providers.ts +62 -0
- package/src/oauth/provider-serializer.ts +13 -0
- package/src/oauth/seed-providers.ts +62 -11
- package/src/onboarding/onboarding-events-store.ts +91 -1
- package/src/permissions/question-prompter.test.ts +4 -0
- package/src/permissions/question-prompter.ts +21 -4
- package/src/persistence/__tests__/conversation-lineage.test.ts +248 -0
- package/src/persistence/attachments-store.ts +15 -15
- package/src/persistence/conversation-attention-store.ts +30 -10
- package/src/persistence/conversation-bootstrap.ts +17 -0
- package/src/persistence/conversation-crud.ts +363 -97
- package/src/persistence/conversation-group-migration.ts +1 -1
- package/src/persistence/conversation-key-store.ts +38 -10
- package/src/persistence/conversation-lineage.ts +226 -0
- package/src/persistence/conversation-plugin-facade.ts +28 -3
- package/src/persistence/conversation-queries.ts +207 -33
- package/src/persistence/conversation-types.ts +107 -1
- package/src/persistence/jobs-store.ts +32 -8
- package/src/persistence/llm-usage-store.ts +22 -0
- package/src/persistence/migrations/361-normalize-managed-connection-rows.test.ts +168 -0
- package/src/persistence/migrations/361-normalize-managed-connection-rows.ts +76 -0
- package/src/persistence/migrations/362-add-conversation-subagent-kind.test.ts +187 -0
- package/src/persistence/migrations/362-add-conversation-subagent-kind.ts +93 -0
- package/src/persistence/migrations/363-backfill-schedule-inference-profile.test.ts +229 -0
- package/src/persistence/migrations/363-backfill-schedule-inference-profile.ts +127 -0
- package/src/persistence/migrations/364-add-schedule-source-key.test.ts +120 -0
- package/src/persistence/migrations/364-add-schedule-source-key.ts +43 -0
- package/src/persistence/migrations/365-add-conversation-fork-strategy.ts +27 -0
- package/src/persistence/schema/conversations.ts +32 -0
- package/src/persistence/schema/infrastructure.ts +3 -0
- package/src/persistence/steps.ts +30 -0
- package/src/persistence/subagent-store.ts +148 -6
- package/src/platform/managed-speech.test.ts +20 -0
- package/src/platform/managed-speech.ts +9 -0
- package/src/plugin-api/index.ts +8 -0
- package/src/plugin-api/webhook-url.ts +151 -0
- package/src/plugins/__tests__/installed-plugin-dirs.test.ts +82 -0
- package/src/plugins/__tests__/source-fingerprint.test.ts +5 -1
- package/src/plugins/collect-source-versions.ts +4 -27
- package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +5 -5
- package/src/plugins/defaults/exploration-drift/package.json +1 -1
- package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +4 -2
- package/src/plugins/defaults/image-recovery/recover.ts +67 -57
- package/src/plugins/defaults/index.ts +1 -1
- package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +64 -18
- package/src/plugins/defaults/memory/__tests__/jobs-worker-outcome-contract.test.ts +242 -0
- package/src/plugins/defaults/memory/__tests__/jobs-worker-retrospective-sweep.test.ts +24 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +48 -3
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +511 -19
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +39 -3
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +493 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +5 -4
- package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +5 -1
- package/src/plugins/defaults/memory/job-handlers.ts +87 -5
- package/src/plugins/defaults/memory/jobs-worker.ts +60 -6
- package/src/plugins/defaults/memory/memory-retrospective-constants.ts +11 -0
- package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +29 -0
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +342 -60
- package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +23 -3
- package/src/plugins/defaults/memory/substrate/consolidation-lock.ts +9 -4
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +7 -0
- package/src/plugins/defaults/memory/v1/graph/injection.ts +1 -1
- package/src/plugins/defaults/memory/v2/router.ts +8 -0
- package/src/plugins/defaults/turn-context/unified-turn-context.ts +1 -2
- package/src/plugins/external-plugin-loader.ts +17 -8
- package/src/plugins/installed-plugin-dirs.ts +101 -0
- package/src/plugins/mtime-cache.ts +34 -22
- package/src/plugins/types.ts +69 -8
- package/src/prompts/persona-resolver.ts +6 -2
- package/src/prompts/system-prompt.ts +72 -17
- package/src/providers/__tests__/inference.test.ts +40 -0
- package/src/providers/__tests__/preflight-resolved-config.test.ts +58 -1
- package/src/providers/__tests__/retry-callsite.test.ts +67 -0
- package/src/providers/__tests__/vellum-connection-routing.test.ts +42 -11
- package/src/providers/anthropic/client.ts +112 -95
- package/src/providers/connection-resolution.ts +35 -1
- package/src/providers/gemini/client.ts +5 -5
- package/src/providers/inference/adapter-factory.ts +16 -10
- package/src/providers/inference/auth.ts +16 -0
- package/src/providers/inference/connection-availability.ts +164 -12
- package/src/providers/inference/connections.ts +25 -1
- package/src/providers/media-resolve.ts +21 -11
- package/src/providers/model-catalog.ts +15 -0
- package/src/providers/openai/chat-completions-provider.ts +4 -4
- package/src/providers/openai/responses-provider.ts +12 -25
- package/src/providers/registry.ts +9 -4
- package/src/providers/retry.ts +25 -0
- package/src/providers/server-tool-pairing.ts +134 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +187 -6
- package/src/providers/speech-to-text/deepgram.ts +1 -0
- package/src/providers/speech-to-text/resolve.ts +80 -10
- package/src/providers/speech-to-text/vellum-managed.ts +7 -1
- package/src/providers/speech-to-text/vellum-speech-relay-connection.ts +3 -1
- package/src/providers/types.ts +26 -6
- package/src/providers/vellum-model-routing.ts +6 -7
- package/src/runtime/AGENTS.md +2 -0
- package/src/runtime/__tests__/agent-wake.test.ts +16 -16
- package/src/runtime/__tests__/background-job-runner.test.ts +0 -144
- package/src/runtime/__tests__/desktop-presence.test.ts +162 -0
- package/src/runtime/agent-wake.ts +63 -24
- package/src/runtime/assistant-event-hub.ts +87 -12
- package/src/runtime/auth/__tests__/guard-tests.test.ts +2 -0
- package/src/runtime/auth/__tests__/scopes.test.ts +20 -0
- package/src/runtime/auth/same-actor.ts +56 -8
- package/src/runtime/auth/scopes.ts +12 -0
- package/src/runtime/auth/types.ts +2 -0
- package/src/runtime/background-job-runner.ts +5 -27
- package/src/runtime/channel-readiness-service.ts +59 -3
- package/src/runtime/channel-readiness-types.ts +18 -0
- package/src/runtime/desktop-presence.ts +69 -0
- package/src/runtime/http-server.ts +12 -0
- package/src/runtime/local-actor-identity.ts +11 -9
- package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -0
- package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/client-routes.test.ts +186 -14
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +547 -1
- package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +162 -7
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +14 -10
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +340 -3
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +28 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +173 -1
- package/src/runtime/routes/__tests__/question-routes.test.ts +64 -0
- package/src/runtime/routes/__tests__/retrospective-routes.test.ts +20 -0
- package/src/runtime/routes/__tests__/skill-history-route.test.ts +116 -0
- package/src/runtime/routes/__tests__/wake-conversation-routes.test.ts +60 -2
- package/src/runtime/routes/archive-utils.ts +16 -12
- package/src/runtime/routes/attachment-routes.ts +13 -6
- package/src/runtime/routes/canned-reply-release.ts +54 -0
- package/src/runtime/routes/channel-verification-routes.ts +2 -27
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +5 -0
- package/src/runtime/routes/client-routes.ts +114 -2
- package/src/runtime/routes/conversation-cli-routes.ts +10 -12
- package/src/runtime/routes/conversation-list-routes.ts +59 -10
- package/src/runtime/routes/conversation-management-routes.ts +47 -18
- package/src/runtime/routes/conversation-query-routes.ts +32 -53
- package/src/runtime/routes/conversation-routes.ts +360 -293
- package/src/runtime/routes/credential-prompt-routes.ts +23 -2
- package/src/runtime/routes/debug-routes.ts +4 -1
- package/src/runtime/routes/default-provider-routes.ts +7 -11
- package/src/runtime/routes/documents-routes.ts +44 -1
- package/src/runtime/routes/events-routes.ts +28 -1
- package/src/runtime/routes/host-app-control-routes.ts +2 -0
- package/src/runtime/routes/host-bash-routes.ts +2 -0
- package/src/runtime/routes/host-browser-routes.ts +2 -0
- package/src/runtime/routes/host-cu-routes.ts +2 -0
- package/src/runtime/routes/host-file-routes.ts +2 -0
- package/src/runtime/routes/host-transfer-routes.ts +4 -0
- package/src/runtime/routes/identity-routes.ts +6 -6
- package/src/runtime/routes/inbound-message-handler.ts +5 -5
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +7 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +14 -0
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -2
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +14 -11
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -17
- package/src/runtime/routes/inference-profile-availability-guard.ts +171 -0
- package/src/runtime/routes/inference-profile-session-handler.ts +26 -2
- package/src/runtime/routes/inference-profile-session-routes.ts +2 -1
- package/src/runtime/routes/inference-profiles-routes.ts +152 -73
- package/src/runtime/routes/inference-provider-connection-routes.ts +0 -16
- package/src/runtime/routes/llm-call-sites-routes.ts +13 -0
- package/src/runtime/routes/log-export-routes.ts +1 -1
- package/src/runtime/routes/notification-routes.ts +0 -14
- package/src/runtime/routes/oauth-providers.ts +1 -0
- package/src/runtime/routes/plugins-routes.ts +106 -1
- package/src/runtime/routes/profiler-routes.ts +4 -2
- package/src/runtime/routes/question-routes.ts +28 -10
- package/src/runtime/routes/retrospective-routes.ts +9 -3
- package/src/runtime/routes/schedule-routes.ts +210 -12
- package/src/runtime/routes/secret-routes.ts +40 -23
- package/src/runtime/routes/settings-routes.ts +41 -13
- package/src/runtime/routes/skills-routes.ts +79 -0
- package/src/runtime/routes/ui-snapshot-routes.ts +1 -0
- package/src/runtime/routes/wake-conversation-routes.ts +23 -0
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
- package/src/runtime/services/conversation-serializer.ts +24 -9
- package/src/runtime/sync/documents-sidecar-publish.test.ts +101 -0
- package/src/runtime/sync/resource-sync-events.ts +79 -1
- package/src/runtime/sync/worker-daemon-notify.test.ts +34 -0
- package/src/runtime/sync/worker-daemon-notify.ts +44 -6
- package/src/runtime/verification-outbound-actions.ts +250 -222
- package/src/runtime/verification-templates.ts +41 -30
- package/src/schedule/__tests__/plugin-schedule-declarations.test.ts +614 -0
- package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +1066 -0
- package/src/schedule/inference-profile.ts +64 -3
- package/src/schedule/plugin-schedule-declarations.ts +518 -0
- package/src/schedule/plugin-schedule-reconciler.ts +605 -0
- package/src/schedule/plugin-schedules-gate.ts +23 -0
- package/src/schedule/schedule-store.ts +520 -26
- package/src/schedule/scheduler.ts +38 -0
- package/src/security/credential-backend.ts +51 -4
- package/src/security/encrypted-store.ts +64 -42
- package/src/security/secure-keys.ts +55 -7
- package/src/skills/catalog-install.ts +20 -7
- package/src/skills/inline-command-runner.ts +2 -7
- package/src/skills/skill-history.test.ts +269 -0
- package/src/skills/skill-history.ts +250 -0
- package/src/skills/skillssh-registry.ts +1 -1
- package/src/stt/daemon-batch-transcriber.ts +8 -6
- package/src/subagent/__tests__/consult-prompt.test.ts +25 -0
- package/src/subagent/consult-prompt.ts +3 -1
- package/src/subagent/consult-transcript.ts +1 -1
- package/src/subagent/manager.ts +304 -44
- package/src/subagent/progress-events.ts +35 -0
- package/src/subagent/role-resolution.ts +88 -0
- package/src/subagent/types.ts +302 -67
- package/src/subagent/validate-allowlists.ts +3 -4
- package/src/telegram/__tests__/webhook-health.test.ts +73 -1
- package/src/telegram/webhook-health.ts +101 -3
- package/src/telemetry/__tests__/telemetry-event-fixtures.ts +2 -0
- package/src/telemetry/activation-funnel.ts +6 -1
- package/src/telemetry/live-voice-funnel.ts +75 -0
- package/src/telemetry/telemetry-event-sources.ts +6 -0
- package/src/telemetry/telemetry-wire-source.json +1 -1
- package/src/telemetry/telemetry-wire.generated.ts +2 -0
- package/src/telemetry/types.ts +23 -4
- package/src/telemetry/usage-telemetry-reporter.test.ts +4 -0
- package/src/tools/__tests__/subagent-workflow-tool-input-schemas.test.ts +1 -1
- package/src/tools/ask-question/ask-question-tool.test.ts +118 -3
- package/src/tools/ask-question/ask-question-tool.ts +56 -2
- package/src/tools/browser/browser-execution.ts +1 -1
- package/src/tools/calls/call-start.ts +8 -5
- package/src/tools/conversation-groups/move_to_group.ts +10 -12
- package/src/tools/document/document-tool.ts +11 -4
- package/src/tools/execution-timeout.ts +9 -2
- package/src/tools/executor.ts +7 -6
- package/src/tools/filesystem/edit.ts +1 -1
- package/src/tools/filesystem/list.ts +1 -1
- package/src/tools/filesystem/read.ts +7 -13
- package/src/tools/filesystem/search.ts +23 -21
- package/src/tools/filesystem/write.ts +4 -1
- package/src/tools/host-filesystem/edit.ts +7 -2
- package/src/tools/host-filesystem/read.ts +21 -8
- package/src/tools/host-filesystem/write.ts +10 -2
- package/src/tools/host-terminal/host-shell.ts +2 -2
- package/src/tools/mcp/mcp-tool-factory.ts +25 -1
- package/src/tools/permission-checker.ts +8 -3
- package/src/tools/schedule/delete.ts +13 -1
- package/src/tools/schedule/list.ts +14 -3
- package/src/tools/schedule/update.ts +56 -33
- package/src/tools/shared/filesystem/audio-read.ts +6 -4
- package/src/tools/shared/filesystem/file-ops-service.ts +176 -116
- package/src/tools/shared/filesystem/image-read.ts +12 -12
- package/src/tools/shared/filesystem/path-policy.ts +70 -5
- package/src/tools/shared/filesystem/size-guard.ts +6 -6
- package/src/tools/shared/input-misuse.ts +72 -0
- package/src/tools/skills/scaffold-managed.ts +82 -0
- package/src/tools/skills/skill-tool-factory.ts +14 -1
- package/src/tools/subagent/consult-deadline.ts +12 -7
- package/src/tools/subagent/message.ts +3 -1
- package/src/tools/subagent/read.ts +66 -4
- package/src/tools/subagent/spawn.ts +500 -78
- package/src/tools/tool-approval-handler.ts +45 -4
- package/src/tools/types.ts +22 -3
- package/src/tools/ui-surface/surface-shape-docs.ts +1 -1
- package/src/tools/ui-surface/visual-validation.ts +2 -0
- package/src/usage/__tests__/subagent-attribution.test.ts +109 -0
- package/src/usage/subagent-attribution.ts +108 -0
- package/src/util/__tests__/sqlite-retry.test.ts +112 -0
- package/src/util/abort-reasons.ts +16 -0
- package/src/util/ansi.ts +44 -0
- package/src/util/disk-usage.ts +17 -13
- package/src/util/image-conversion.ts +20 -13
- package/src/util/schedule-source-key.ts +23 -0
- package/src/util/worker-process.ts +7 -1
- package/src/workspace/git-service.ts +33 -9
- package/src/workspace/migrations/140-repair-seed-pinned-memory-v3-live.ts +260 -0
- package/src/workspace/migrations/141-stt-english-default-to-multilingual.ts +111 -0
- package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +126 -0
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/workspace/top-level-renderer.ts +15 -2
- package/src/__tests__/tool-side-effects-slack-dm.test.ts +0 -357
- package/src/daemon/install-symlink.ts +0 -207
- package/src/notifications/deferred-emit.ts +0 -147
|
@@ -36,11 +36,23 @@ const whatsapp = {
|
|
|
36
36
|
const a2a = {
|
|
37
37
|
deliverA2AReply: mock((..._args: unknown[]) => Promise.resolve({ ok: true })),
|
|
38
38
|
};
|
|
39
|
+
const discord = {
|
|
40
|
+
sendDiscordReply: mock((..._args: unknown[]) =>
|
|
41
|
+
Promise.resolve({ lastMessageId: "discord-id" }),
|
|
42
|
+
),
|
|
43
|
+
sendDiscordTypingIndicator: mock((..._args: unknown[]) =>
|
|
44
|
+
Promise.resolve(true),
|
|
45
|
+
),
|
|
46
|
+
sendDiscordAttachments: mock((..._args: unknown[]) =>
|
|
47
|
+
Promise.resolve({ allFailed: false, failureCount: 0, totalCount: 0 }),
|
|
48
|
+
),
|
|
49
|
+
};
|
|
39
50
|
|
|
40
51
|
mock.module("../slack/send.js", () => slack);
|
|
41
52
|
mock.module("../telegram-bot/send.js", () => telegram);
|
|
42
53
|
mock.module("../whatsapp/send.js", () => whatsapp);
|
|
43
54
|
mock.module("../a2a/deliver.js", () => a2a);
|
|
55
|
+
mock.module("../discord/send.js", () => discord);
|
|
44
56
|
mock.module("../../../util/logger.js", () => ({
|
|
45
57
|
getLogger: () => ({ debug() {}, info() {}, warn() {}, error() {} }),
|
|
46
58
|
}));
|
|
@@ -57,7 +69,7 @@ function payload(
|
|
|
57
69
|
}
|
|
58
70
|
|
|
59
71
|
beforeEach(() => {
|
|
60
|
-
for (const group of [slack, telegram, whatsapp, a2a]) {
|
|
72
|
+
for (const group of [slack, telegram, whatsapp, a2a, discord]) {
|
|
61
73
|
for (const spy of Object.values(group)) {
|
|
62
74
|
spy.mockClear();
|
|
63
75
|
}
|
|
@@ -78,19 +90,25 @@ describe("routing", () => {
|
|
|
78
90
|
expect(
|
|
79
91
|
getTransportForCallback(`${BASE}/deliver/a2a?taskId=t1`)?.channel,
|
|
80
92
|
).toBe("a2a");
|
|
93
|
+
expect(getTransportForCallback(`${BASE}/deliver/discord`)?.channel).toBe(
|
|
94
|
+
"discord",
|
|
95
|
+
);
|
|
81
96
|
});
|
|
82
97
|
|
|
83
98
|
test("isDirectDelivery is true for owned paths, false otherwise", () => {
|
|
84
99
|
expect(isDirectDelivery(`${BASE}/deliver/slack`)).toBe(true);
|
|
85
100
|
expect(isDirectDelivery(`${BASE}/deliver/a2a?taskId=t1`)).toBe(true);
|
|
86
|
-
expect(isDirectDelivery(`${BASE}/deliver/discord`)).toBe(
|
|
101
|
+
expect(isDirectDelivery(`${BASE}/deliver/discord`)).toBe(true);
|
|
87
102
|
expect(isDirectDelivery(`${BASE}/v1/messages`)).toBe(false);
|
|
88
103
|
expect(
|
|
89
104
|
isDirectDelivery(
|
|
90
105
|
`${BASE}/v1/internal/managed-gateway/outbound-send/?route_id=r1`,
|
|
91
106
|
),
|
|
92
107
|
).toBe(false);
|
|
93
|
-
|
|
108
|
+
// `phone` is a canonical channel with no direct-delivery transport, so it
|
|
109
|
+
// stands in for the never-registered case discord used to cover.
|
|
110
|
+
expect(isDirectDelivery(`${BASE}/deliver/phone`)).toBe(false);
|
|
111
|
+
expect(getTransportForCallback(`${BASE}/deliver/phone`)).toBeUndefined();
|
|
94
112
|
});
|
|
95
113
|
});
|
|
96
114
|
|
|
@@ -203,12 +221,56 @@ describe("capability gating across channels", () => {
|
|
|
203
221
|
expect(a2a.deliverA2AReply).toHaveBeenCalledTimes(1);
|
|
204
222
|
expect(a2a.deliverA2AReply.mock.calls[0][0]).toBe(url);
|
|
205
223
|
});
|
|
224
|
+
|
|
225
|
+
test("Discord text routes to sendDiscordReply, targeting the channel", async () => {
|
|
226
|
+
await deliverDirect(`${BASE}/deliver/discord`, payload({ text: "hi" }));
|
|
227
|
+
expect(discord.sendDiscordReply).toHaveBeenCalledTimes(1);
|
|
228
|
+
expect(discord.sendDiscordReply.mock.calls[0][0]).toEqual({
|
|
229
|
+
channelId: "C1",
|
|
230
|
+
});
|
|
231
|
+
expect(discord.sendDiscordReply.mock.calls[0][1]).toBe("hi");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("Discord threads a reply to the thread id, not the parent channel", async () => {
|
|
235
|
+
await deliverDirect(
|
|
236
|
+
`${BASE}/deliver/discord?threadId=T9`,
|
|
237
|
+
payload({ text: "hi" }),
|
|
238
|
+
);
|
|
239
|
+
expect(discord.sendDiscordReply).toHaveBeenCalledTimes(1);
|
|
240
|
+
// A Discord thread is itself a channel: the reply must post to the thread
|
|
241
|
+
// snowflake, never to the parent channel the payload's chatId carries.
|
|
242
|
+
expect(discord.sendDiscordReply.mock.calls[0][0]).toEqual({
|
|
243
|
+
channelId: "T9",
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("a typing payload to Discord falls through to deliver (no sendTyping)", async () => {
|
|
248
|
+
// Only the Telegram heartbeat produces `chatAction: "typing"`, and it is
|
|
249
|
+
// gated on sourceChannel === "telegram", so Discord can never receive one.
|
|
250
|
+
// The transport implements no sendTyping, so this falls through.
|
|
251
|
+
await deliverDirect(
|
|
252
|
+
`${BASE}/deliver/discord`,
|
|
253
|
+
payload({ chatAction: "typing", text: "hi" }),
|
|
254
|
+
);
|
|
255
|
+
expect(discord.sendDiscordReply).toHaveBeenCalledTimes(1);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("a Slack-only stream payload to Discord falls through to deliver", async () => {
|
|
259
|
+
await deliverDirect(
|
|
260
|
+
`${BASE}/deliver/discord`,
|
|
261
|
+
payload({
|
|
262
|
+
text: "hi",
|
|
263
|
+
slackStream: { action: "start", threadTs: "1700.5" },
|
|
264
|
+
}),
|
|
265
|
+
);
|
|
266
|
+
expect(discord.sendDiscordReply).toHaveBeenCalledTimes(1);
|
|
267
|
+
});
|
|
206
268
|
});
|
|
207
269
|
|
|
208
270
|
describe("unsupported callback", () => {
|
|
209
271
|
test("throws when no transport owns the callback", async () => {
|
|
210
272
|
await expect(
|
|
211
|
-
deliverDirect(`${BASE}/deliver/
|
|
273
|
+
deliverDirect(`${BASE}/deliver/phone`, payload({ text: "hi" })),
|
|
212
274
|
).rejects.toThrow(/unsupported callback/);
|
|
213
275
|
});
|
|
214
276
|
});
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord REST client for direct outbound messaging.
|
|
3
|
+
*
|
|
4
|
+
* Calls the Discord HTTP API directly using the `discord_channel:bot_token`
|
|
5
|
+
* credential, the same token the gateway's Discord Gateway client identifies
|
|
6
|
+
* with, read from the secure store rather than passed across the process
|
|
7
|
+
* boundary. Retry and error classification mirror the sibling Telegram client
|
|
8
|
+
* so delivery behaviour is consistent across channels.
|
|
9
|
+
*
|
|
10
|
+
* Wire shapes verified against the official docs:
|
|
11
|
+
* https://docs.discord.com/developers/resources/message
|
|
12
|
+
* https://docs.discord.com/developers/topics/rate-limits
|
|
13
|
+
* https://docs.discord.com/developers/reference
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { credentialKey } from "../../../security/credential-key.js";
|
|
17
|
+
import { getSecureKeyResultAsync } from "../../../security/secure-keys.js";
|
|
18
|
+
import { BackendUnavailableError, ConfigError } from "../../../util/errors.js";
|
|
19
|
+
import { getLogger } from "../../../util/logger.js";
|
|
20
|
+
import { retryableCall } from "../retry-policy.js";
|
|
21
|
+
|
|
22
|
+
const log = getLogger("discord-api");
|
|
23
|
+
|
|
24
|
+
const DISCORD_API_BASE_URL = "https://discord.com/api/v10";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Discord requires HTTP API clients to identify themselves with a User-Agent
|
|
28
|
+
* of this documented form (`https://docs.discord.com/developers/reference`).
|
|
29
|
+
*/
|
|
30
|
+
const DISCORD_USER_AGENT =
|
|
31
|
+
"DiscordBot (https://github.com/vellum-ai/vellum-assistant, 1.0)";
|
|
32
|
+
|
|
33
|
+
const DISCORD_DEFAULT_MAX_RETRIES = 3;
|
|
34
|
+
const DISCORD_DEFAULT_INITIAL_BACKOFF_MS = 1000;
|
|
35
|
+
const DISCORD_DEFAULT_TIMEOUT_MS = 15_000;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Cap on a server-advertised `retry_after`. Discord can hand back a multi-hour
|
|
39
|
+
* wait when a bot is globally limited; sleeping that long inside a delivery
|
|
40
|
+
* would hold the per-conversation single-flight open indefinitely. Past this
|
|
41
|
+
* bound the call fails and the channel retry sweep owns the redelivery.
|
|
42
|
+
*/
|
|
43
|
+
const DISCORD_MAX_RETRY_AFTER_MS = 60_000;
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Error types
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A Discord REST call that returned a non-retryable client error: a 4xx that
|
|
51
|
+
* is not a 429. The request itself is the problem (the channel, the
|
|
52
|
+
* permissions, or the payload), so retrying the same call is pointless.
|
|
53
|
+
*
|
|
54
|
+
* Callers branch on this type to degrade gracefully, e.g. an attachment that
|
|
55
|
+
* Discord refuses is reported to the channel instead of retried. Branch on the
|
|
56
|
+
* type rather than on Discord's numeric error code: the docs treat that code
|
|
57
|
+
* list as open, and new codes are added without notice.
|
|
58
|
+
*/
|
|
59
|
+
export class DiscordNonRetryableError extends Error {
|
|
60
|
+
readonly status: number;
|
|
61
|
+
|
|
62
|
+
constructor(status: number, message: string) {
|
|
63
|
+
super(message);
|
|
64
|
+
this.name = "DiscordNonRetryableError";
|
|
65
|
+
this.status = status;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Internal helpers
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
interface DiscordErrorBody {
|
|
74
|
+
message?: string;
|
|
75
|
+
retry_after?: number;
|
|
76
|
+
global?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseErrorBody(body: string): DiscordErrorBody {
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(body) as DiscordErrorBody;
|
|
82
|
+
} catch {
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The wait Discord asked for, as a `Retry-After` seconds value.
|
|
89
|
+
*
|
|
90
|
+
* Discord carries it in the 429 JSON body as well as the header, in seconds
|
|
91
|
+
* (fractional) either way, and the body is the more precise of the two.
|
|
92
|
+
*/
|
|
93
|
+
function retryAfterFrom(_response: Response, body: string): string | null {
|
|
94
|
+
const parsed = parseErrorBody(body);
|
|
95
|
+
return typeof parsed.retry_after === "number" && parsed.retry_after > 0
|
|
96
|
+
? String(parsed.retry_after)
|
|
97
|
+
: null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Run a Discord REST call with retries.
|
|
102
|
+
*
|
|
103
|
+
* `T | undefined` because an endpoint may answer 204 with no body; callers
|
|
104
|
+
* that need a response shape assert on it themselves.
|
|
105
|
+
*/
|
|
106
|
+
function discordCall<T>(
|
|
107
|
+
route: string,
|
|
108
|
+
doFetch: () => Promise<Response>,
|
|
109
|
+
): Promise<T | undefined> {
|
|
110
|
+
return retryableCall<T | undefined>({
|
|
111
|
+
provider: "Discord",
|
|
112
|
+
operation: route,
|
|
113
|
+
maxRetries: DISCORD_DEFAULT_MAX_RETRIES,
|
|
114
|
+
initialBackoffMs: DISCORD_DEFAULT_INITIAL_BACKOFF_MS,
|
|
115
|
+
maxDelayMs: DISCORD_MAX_RETRY_AFTER_MS,
|
|
116
|
+
log,
|
|
117
|
+
doFetch,
|
|
118
|
+
detailFrom: (body) => parseErrorBody(body).message,
|
|
119
|
+
retryAfterFrom,
|
|
120
|
+
nonRetryableError: ({ status, message }) =>
|
|
121
|
+
new DiscordNonRetryableError(status, message),
|
|
122
|
+
decode: (body) => {
|
|
123
|
+
if (!body) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
return JSON.parse(body) as T;
|
|
128
|
+
} catch {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Discord ${route} returned an unparseable response body`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Run a Discord REST call with retries.
|
|
139
|
+
*
|
|
140
|
+
* Retryable: 429 (rate limited) and 5xx, plus transport failures. Everything
|
|
141
|
+
* else surfaces as {@link DiscordNonRetryableError}.
|
|
142
|
+
*
|
|
143
|
+
* `T | undefined` because an endpoint may answer 204 with no body; callers
|
|
144
|
+
* that need a response shape assert on it themselves.
|
|
145
|
+
*/
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Credential resolution
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
async function resolveBotToken(): Promise<string> {
|
|
152
|
+
const result = await getSecureKeyResultAsync(
|
|
153
|
+
credentialKey("discord_channel", "bot_token"),
|
|
154
|
+
);
|
|
155
|
+
if (result.value) {
|
|
156
|
+
return result.value;
|
|
157
|
+
}
|
|
158
|
+
// Distinguish a transient credential-store outage from a token that was
|
|
159
|
+
// genuinely never set. Both surface as an absent value, but only the latter
|
|
160
|
+
// is an operator misconfiguration; the former is retryable infrastructure
|
|
161
|
+
// unavailability that callers should not treat as "not configured".
|
|
162
|
+
if (result.unreachable) {
|
|
163
|
+
throw new BackendUnavailableError("Discord credential store unreachable");
|
|
164
|
+
}
|
|
165
|
+
throw new ConfigError("Discord bot token not configured");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// Public API
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
/** The subset of Discord's message object that delivery reads back. */
|
|
173
|
+
export interface DiscordMessage {
|
|
174
|
+
id: string;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The subset of Discord's channel object that DM resolution reads back. */
|
|
178
|
+
interface DiscordChannel {
|
|
179
|
+
id: string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Resolved DM channels, keyed by recipient user snowflake.
|
|
184
|
+
*
|
|
185
|
+
* The mapping is stable, so this never goes stale: Discord's create-DM route
|
|
186
|
+
* returns the existing channel when one exists rather than opening a second
|
|
187
|
+
* (https://docs.discord.com/developers/resources/user). The cache is here
|
|
188
|
+
* because that same page warns that opening DMs in volume gets a bot rate
|
|
189
|
+
* limited or blocked from opening new ones, and a per-recipient DM lane would
|
|
190
|
+
* otherwise re-open on every notice.
|
|
191
|
+
*/
|
|
192
|
+
const dmChannelIds = new Map<string, string>();
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Bound on the cache. Entries are one small string pair each, and the eviction
|
|
196
|
+
* below is insertion-order rather than least-recently-used: the entries this
|
|
197
|
+
* cache exists to protect (the guardian, a handful of contacts) are the ones
|
|
198
|
+
* created first, and evicting one only costs a single extra API call.
|
|
199
|
+
*/
|
|
200
|
+
const MAX_CACHED_DM_CHANNELS = 512;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Opens already in progress, so concurrent callers for the same recipient
|
|
204
|
+
* share one request rather than racing to make the same one. A verification
|
|
205
|
+
* send and a guardian expiry notice can address the same person in the same
|
|
206
|
+
* tick, and the cache alone does not help there: neither has populated it yet.
|
|
207
|
+
*/
|
|
208
|
+
const inFlightDmOpens = new Map<string, Promise<string>>();
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Open the DM channel with a Discord user and return its snowflake.
|
|
212
|
+
*
|
|
213
|
+
* Discord has no "look up my DM with this person" route: a bot names a
|
|
214
|
+
* recipient and is handed the channel, creating it the first time and
|
|
215
|
+
* receiving the same one after that. So reaching one person privately is
|
|
216
|
+
* always this call followed by an ordinary channel send, and the DM channel is
|
|
217
|
+
* a resolved address rather than something the caller can be told up front.
|
|
218
|
+
*/
|
|
219
|
+
export function openDiscordDmChannel(recipientUserId: string): Promise<string> {
|
|
220
|
+
const cached = dmChannelIds.get(recipientUserId);
|
|
221
|
+
if (cached) {
|
|
222
|
+
return Promise.resolve(cached);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const existing = inFlightDmOpens.get(recipientUserId);
|
|
226
|
+
if (existing) {
|
|
227
|
+
return existing;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const open = (async (): Promise<string> => {
|
|
231
|
+
const channel = await callDiscordApi<DiscordChannel>(
|
|
232
|
+
"POST",
|
|
233
|
+
"/users/@me/channels",
|
|
234
|
+
{ recipient_id: recipientUserId },
|
|
235
|
+
);
|
|
236
|
+
if (typeof channel?.id !== "string" || channel.id.length === 0) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
"Discord create-DM returned no channel id for the recipient",
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (dmChannelIds.size >= MAX_CACHED_DM_CHANNELS) {
|
|
243
|
+
const oldest = dmChannelIds.keys().next();
|
|
244
|
+
if (!oldest.done) {
|
|
245
|
+
dmChannelIds.delete(oldest.value);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
dmChannelIds.set(recipientUserId, channel.id);
|
|
249
|
+
log.debug({ recipientUserId }, "Opened Discord DM channel");
|
|
250
|
+
return channel.id;
|
|
251
|
+
})().finally(() => {
|
|
252
|
+
// Cleared on failure too, so a transient error does not pin every later
|
|
253
|
+
// caller to the same rejected promise.
|
|
254
|
+
inFlightDmOpens.delete(recipientUserId);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
inFlightDmOpens.set(recipientUserId, open);
|
|
258
|
+
return open;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Drop the resolved and in-flight DM channel state. Test-only seam. */
|
|
262
|
+
export function resetDiscordDmChannelCache(): void {
|
|
263
|
+
dmChannelIds.clear();
|
|
264
|
+
inFlightDmOpens.clear();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Call a Discord REST route with a JSON body. */
|
|
268
|
+
export async function callDiscordApi<T>(
|
|
269
|
+
method: "POST" | "PATCH",
|
|
270
|
+
route: string,
|
|
271
|
+
body: Record<string, unknown>,
|
|
272
|
+
): Promise<T | undefined> {
|
|
273
|
+
const botToken = await resolveBotToken();
|
|
274
|
+
return discordCall<T>(route, () =>
|
|
275
|
+
fetch(`${DISCORD_API_BASE_URL}${route}`, {
|
|
276
|
+
method,
|
|
277
|
+
headers: {
|
|
278
|
+
Authorization: `Bot ${botToken}`,
|
|
279
|
+
"Content-Type": "application/json",
|
|
280
|
+
"User-Agent": DISCORD_USER_AGENT,
|
|
281
|
+
},
|
|
282
|
+
body: JSON.stringify(body),
|
|
283
|
+
signal: AbortSignal.timeout(DISCORD_DEFAULT_TIMEOUT_MS),
|
|
284
|
+
}),
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Call a Discord REST route with a `multipart/form-data` body.
|
|
290
|
+
*
|
|
291
|
+
* Discord's documented upload shape: files ride as `files[n]` parts and the
|
|
292
|
+
* JSON body moves to a `payload_json` part.
|
|
293
|
+
*/
|
|
294
|
+
export async function callDiscordApiMultipart<T>(
|
|
295
|
+
route: string,
|
|
296
|
+
form: FormData,
|
|
297
|
+
): Promise<T | undefined> {
|
|
298
|
+
const botToken = await resolveBotToken();
|
|
299
|
+
return discordCall<T>(route, () =>
|
|
300
|
+
fetch(`${DISCORD_API_BASE_URL}${route}`, {
|
|
301
|
+
method: "POST",
|
|
302
|
+
headers: {
|
|
303
|
+
Authorization: `Bot ${botToken}`,
|
|
304
|
+
// Content-Type is deliberately unset so fetch derives the multipart
|
|
305
|
+
// boundary from the FormData body.
|
|
306
|
+
"User-Agent": DISCORD_USER_AGENT,
|
|
307
|
+
},
|
|
308
|
+
body: form,
|
|
309
|
+
signal: AbortSignal.timeout(DISCORD_DEFAULT_TIMEOUT_MS),
|
|
310
|
+
}),
|
|
311
|
+
);
|
|
312
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Discord private-delivery lane, asserted against a stubbed fetch.
|
|
3
|
+
*
|
|
4
|
+
* Discord has no route that looks a DM up by recipient, so reaching one person
|
|
5
|
+
* is always "open the channel, then post to it". Two things about that are
|
|
6
|
+
* silent when wrong and are pinned here: the open call must use the documented
|
|
7
|
+
* create-DM route, and a delivery marked `dm` must post to the channel that
|
|
8
|
+
* call returned rather than to the id it was handed. The second is the one
|
|
9
|
+
* that matters: the id handed in is a *user* snowflake, and a Discord user id
|
|
10
|
+
* and channel id are both bare digits, so posting it unresolved does not fail
|
|
11
|
+
* loudly. It either 404s or, worse, addresses whatever channel happens to
|
|
12
|
+
* carry that id.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
16
|
+
|
|
17
|
+
const BOT_TOKEN = "discord-bot-token";
|
|
18
|
+
|
|
19
|
+
const realSecureKeys = await import("../../../security/secure-keys.js");
|
|
20
|
+
mock.module("../../../security/secure-keys.js", () => ({
|
|
21
|
+
...realSecureKeys,
|
|
22
|
+
getSecureKeyResultAsync: async () => ({
|
|
23
|
+
value: BOT_TOKEN,
|
|
24
|
+
unreachable: false,
|
|
25
|
+
}),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
// Pulled in by `send.js`; stubbed so importing the transport does not reach
|
|
29
|
+
// the attachment store. No test here sends an attachment.
|
|
30
|
+
mock.module("../../../persistence/attachments-store.js", () => ({
|
|
31
|
+
getAttachmentContent: () => null,
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
mock.module("../../../util/logger.js", () => ({
|
|
35
|
+
getLogger: () => ({ debug() {}, info() {}, warn() {}, error() {} }),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
const { openDiscordDmChannel, resetDiscordDmChannelCache } =
|
|
39
|
+
await import("./api.js");
|
|
40
|
+
const { discordTransport } = await import("./transport.js");
|
|
41
|
+
|
|
42
|
+
const originalFetch = globalThis.fetch;
|
|
43
|
+
|
|
44
|
+
interface Captured {
|
|
45
|
+
url: string;
|
|
46
|
+
method: string;
|
|
47
|
+
body: unknown;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let calls: Captured[] = [];
|
|
51
|
+
|
|
52
|
+
/** The user snowflake a notice is addressed to. */
|
|
53
|
+
const RECIPIENT = "900000000000000042";
|
|
54
|
+
/** The DM channel Discord hands back for that recipient. */
|
|
55
|
+
const DM_CHANNEL = "800000000000000099";
|
|
56
|
+
/** The public guild channel the requester was seen in. */
|
|
57
|
+
const GUILD_CHANNEL = "700000000000000001";
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Answer the create-DM route with a channel and every other route with a
|
|
61
|
+
* message. Keyed on the route so a mis-routed call is visible as a wrong
|
|
62
|
+
* payload rather than quietly succeeding.
|
|
63
|
+
*/
|
|
64
|
+
function stubFetch(): void {
|
|
65
|
+
globalThis.fetch = (async (url: string | URL, init?: RequestInit) => {
|
|
66
|
+
const href = String(url);
|
|
67
|
+
calls.push({
|
|
68
|
+
url: href,
|
|
69
|
+
method: init?.method ?? "GET",
|
|
70
|
+
body: init?.body,
|
|
71
|
+
});
|
|
72
|
+
const payload = href.endsWith("/users/@me/channels")
|
|
73
|
+
? { id: DM_CHANNEL }
|
|
74
|
+
: { id: "msg-1" };
|
|
75
|
+
return new Response(JSON.stringify(payload), {
|
|
76
|
+
status: 200,
|
|
77
|
+
headers: { "content-type": "application/json" },
|
|
78
|
+
});
|
|
79
|
+
}) as typeof fetch;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
calls = [];
|
|
84
|
+
resetDiscordDmChannelCache();
|
|
85
|
+
stubFetch();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
afterEach(() => {
|
|
89
|
+
globalThis.fetch = originalFetch;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("openDiscordDmChannel", () => {
|
|
93
|
+
test("POSTs the documented create-DM route with recipient_id", async () => {
|
|
94
|
+
const channelId = await openDiscordDmChannel(RECIPIENT);
|
|
95
|
+
|
|
96
|
+
expect(channelId).toBe(DM_CHANNEL);
|
|
97
|
+
expect(calls).toHaveLength(1);
|
|
98
|
+
expect(calls[0].url).toBe("https://discord.com/api/v10/users/@me/channels");
|
|
99
|
+
expect(calls[0].method).toBe("POST");
|
|
100
|
+
expect(JSON.parse(String(calls[0].body))).toEqual({
|
|
101
|
+
recipient_id: RECIPIENT,
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("resolves a repeat recipient without a second API call", async () => {
|
|
106
|
+
// Discord returns the existing channel rather than opening a second, so
|
|
107
|
+
// this is not about correctness. It is about the documented rate limit:
|
|
108
|
+
// a bot that opens DMs too quickly gets blocked from opening new ones,
|
|
109
|
+
// and a per-notice lane would re-open on every delivery.
|
|
110
|
+
await openDiscordDmChannel(RECIPIENT);
|
|
111
|
+
await openDiscordDmChannel(RECIPIENT);
|
|
112
|
+
await openDiscordDmChannel(RECIPIENT);
|
|
113
|
+
|
|
114
|
+
expect(calls).toHaveLength(1);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("concurrent opens for one recipient share a single request", async () => {
|
|
118
|
+
// The cache cannot help here: nothing has populated it yet. A verification
|
|
119
|
+
// send and a guardian notice can address the same person in one tick, and
|
|
120
|
+
// Discord blocks a bot that opens DMs too quickly.
|
|
121
|
+
const [a, b, c] = await Promise.all([
|
|
122
|
+
openDiscordDmChannel(RECIPIENT),
|
|
123
|
+
openDiscordDmChannel(RECIPIENT),
|
|
124
|
+
openDiscordDmChannel(RECIPIENT),
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
expect(calls).toHaveLength(1);
|
|
128
|
+
expect([a, b, c]).toEqual([DM_CHANNEL, DM_CHANNEL, DM_CHANNEL]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("a channel-less answer throws, and is not pinned for later callers", async () => {
|
|
132
|
+
globalThis.fetch = (async () =>
|
|
133
|
+
new Response(JSON.stringify({}), {
|
|
134
|
+
status: 200,
|
|
135
|
+
headers: { "content-type": "application/json" },
|
|
136
|
+
})) as unknown as typeof fetch;
|
|
137
|
+
await expect(openDiscordDmChannel(RECIPIENT)).rejects.toThrow(
|
|
138
|
+
/no channel id/i,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// A transient failure must not leave every later caller sharing the
|
|
142
|
+
// rejected promise.
|
|
143
|
+
stubFetch();
|
|
144
|
+
await expect(openDiscordDmChannel(RECIPIENT)).resolves.toBe(DM_CHANNEL);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("opens a separate channel per recipient", async () => {
|
|
148
|
+
await openDiscordDmChannel(RECIPIENT);
|
|
149
|
+
await openDiscordDmChannel("900000000000000043");
|
|
150
|
+
|
|
151
|
+
expect(calls).toHaveLength(2);
|
|
152
|
+
expect(JSON.parse(String(calls[1].body))).toEqual({
|
|
153
|
+
recipient_id: "900000000000000043",
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("discordTransport with dm=1", () => {
|
|
160
|
+
test("delivers to the opened DM channel, never to the id it was handed", async () => {
|
|
161
|
+
await discordTransport.deliver(
|
|
162
|
+
{ callbackUrl: "/deliver/discord?dm=1", params: { dm: "1" } },
|
|
163
|
+
{ chatId: RECIPIENT, text: "Your access request was declined." },
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const messageCalls = calls.filter((c) => c.url.includes("/messages"));
|
|
167
|
+
expect(messageCalls).toHaveLength(1);
|
|
168
|
+
expect(messageCalls[0].url).toBe(
|
|
169
|
+
`https://discord.com/api/v10/channels/${DM_CHANNEL}/messages`,
|
|
170
|
+
);
|
|
171
|
+
// The recipient's own snowflake must never appear as a channel in the
|
|
172
|
+
// route: that is the unresolved-user-id delivery this lane exists to stop.
|
|
173
|
+
expect(messageCalls[0].url).not.toContain(RECIPIENT);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("a dm delivery ignores threadId, which addresses a guild thread", async () => {
|
|
177
|
+
await discordTransport.deliver(
|
|
178
|
+
{
|
|
179
|
+
callbackUrl: "/deliver/discord?dm=1",
|
|
180
|
+
params: { dm: "1", threadId: "700000000000000009" },
|
|
181
|
+
},
|
|
182
|
+
{ chatId: RECIPIENT, text: "expired" },
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const messageCalls = calls.filter((c) => c.url.includes("/messages"));
|
|
186
|
+
expect(messageCalls[0].url).toContain(DM_CHANNEL);
|
|
187
|
+
expect(messageCalls[0].url).not.toContain("700000000000000009");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("without dm, chatId is still a channel and no DM is opened", async () => {
|
|
191
|
+
// The ordinary in-channel reply must not start routing through a DM.
|
|
192
|
+
await discordTransport.deliver(
|
|
193
|
+
{ callbackUrl: "/deliver/discord", params: {} },
|
|
194
|
+
{ chatId: GUILD_CHANNEL, text: "hello" },
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
expect(calls.some((c) => c.url.endsWith("/users/@me/channels"))).toBe(
|
|
198
|
+
false,
|
|
199
|
+
);
|
|
200
|
+
expect(calls[0].url).toBe(
|
|
201
|
+
`https://discord.com/api/v10/channels/${GUILD_CHANNEL}/messages`,
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
});
|