@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
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord outbound message orchestration.
|
|
3
|
+
*
|
|
4
|
+
* Splits replies into Discord-sized chunks (see ./render.ts) and uploads
|
|
5
|
+
* attachments, by calling the Discord REST API directly via ./api.ts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getAttachmentContent } from "../../../persistence/attachments-store.js";
|
|
9
|
+
import type { RuntimeAttachmentMetadata } from "../../../runtime/http-types.js";
|
|
10
|
+
import { getLogger } from "../../../util/logger.js";
|
|
11
|
+
import {
|
|
12
|
+
callDiscordApi,
|
|
13
|
+
callDiscordApiMultipart,
|
|
14
|
+
type DiscordMessage,
|
|
15
|
+
DiscordNonRetryableError,
|
|
16
|
+
} from "./api.js";
|
|
17
|
+
import { renderDiscordMessages } from "./render.js";
|
|
18
|
+
|
|
19
|
+
const log = getLogger("discord-send");
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Mention types Discord is allowed to resolve in an outbound message.
|
|
23
|
+
*
|
|
24
|
+
* The agent composes this text, and text it authored (or echoed from an
|
|
25
|
+
* untrusted inbound message) must never be able to ping a whole server or a
|
|
26
|
+
* whole role. `users` is kept so the assistant can address a person directly,
|
|
27
|
+
* which is ordinary Discord conversation; `everyone` and `roles` are withheld.
|
|
28
|
+
*
|
|
29
|
+
* This mirrors the invite scope the setup skill requests, which deliberately
|
|
30
|
+
* excludes Mention Everyone, and closes the role-mention gap that permission
|
|
31
|
+
* alone leaves open.
|
|
32
|
+
*/
|
|
33
|
+
const DISCORD_ALLOWED_MENTIONS = { parse: ["users"] } as const;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Upper bound on an outbound attachment. Discord's real limit is the guild's
|
|
37
|
+
* boost tier (10 MiB with no boosts, more above that), which the API does not
|
|
38
|
+
* expose here, so the client-side guard is only the ceiling no tier exceeds:
|
|
39
|
+
* past it the upload is provably futile and not worth the bandwidth. Anything
|
|
40
|
+
* under it is attempted and, if the guild's own tier rejects it, reported
|
|
41
|
+
* through the same failure notice as any other attachment error.
|
|
42
|
+
*/
|
|
43
|
+
const DISCORD_MAX_ATTACHMENT_BYTES = 100 * 1024 * 1024;
|
|
44
|
+
|
|
45
|
+
/** Send target for one Discord delivery. */
|
|
46
|
+
export interface DiscordSendTarget {
|
|
47
|
+
/**
|
|
48
|
+
* Snowflake of the channel the message is posted to. For a threaded reply
|
|
49
|
+
* this is the thread's own id, because a Discord thread *is* a channel and
|
|
50
|
+
* `POST /channels/{id}/messages` addresses it directly.
|
|
51
|
+
*/
|
|
52
|
+
channelId: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function messagesRoute(target: DiscordSendTarget): string {
|
|
56
|
+
return `/channels/${encodeURIComponent(target.channelId)}/messages`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Outcome of a Discord reply send. */
|
|
60
|
+
export interface DiscordSendResult {
|
|
61
|
+
/**
|
|
62
|
+
* Channel-native id of the last chunk sent, for callers that need to address
|
|
63
|
+
* the message later. Undefined when the API response carried no id.
|
|
64
|
+
*/
|
|
65
|
+
lastMessageId?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Send a text reply, split across as many messages as Discord's content cap
|
|
70
|
+
* requires. Chunks post in order so the reply reads top to bottom.
|
|
71
|
+
*/
|
|
72
|
+
export async function sendDiscordReply(
|
|
73
|
+
target: DiscordSendTarget,
|
|
74
|
+
text: string,
|
|
75
|
+
): Promise<DiscordSendResult> {
|
|
76
|
+
const chunks = renderDiscordMessages(text);
|
|
77
|
+
if (chunks.length === 0) {
|
|
78
|
+
return {};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let lastMessageId: string | undefined;
|
|
82
|
+
for (const chunk of chunks) {
|
|
83
|
+
const sent = await callDiscordApi<DiscordMessage>(
|
|
84
|
+
"POST",
|
|
85
|
+
messagesRoute(target),
|
|
86
|
+
{
|
|
87
|
+
content: chunk,
|
|
88
|
+
allowed_mentions: DISCORD_ALLOWED_MENTIONS,
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
lastMessageId = typeof sent?.id === "string" ? sent.id : undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
log.debug(
|
|
95
|
+
{ channelId: target.channelId, chunks: chunks.length },
|
|
96
|
+
"Discord reply sent",
|
|
97
|
+
);
|
|
98
|
+
return lastMessageId !== undefined ? { lastMessageId } : {};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface DiscordAttachmentResult {
|
|
102
|
+
allFailed: boolean;
|
|
103
|
+
failureCount: number;
|
|
104
|
+
totalCount: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Upload attachments to a Discord channel, one request per file so a single
|
|
109
|
+
* rejection does not take the rest of the batch with it.
|
|
110
|
+
*
|
|
111
|
+
* Discord's documented upload shape is a `multipart/form-data` body whose
|
|
112
|
+
* files ride as `files[n]` parts, with the JSON body moved to `payload_json`
|
|
113
|
+
* and each file declared in an `attachments` array keyed by the same `n`.
|
|
114
|
+
*/
|
|
115
|
+
export async function sendDiscordAttachments(
|
|
116
|
+
target: DiscordSendTarget,
|
|
117
|
+
attachments: RuntimeAttachmentMetadata[],
|
|
118
|
+
): Promise<DiscordAttachmentResult> {
|
|
119
|
+
const failures: string[] = [];
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Name a failed file for the channel notice. A refusal Discord explains
|
|
123
|
+
* (over the guild's upload limit, missing Attach Files) is worth passing on,
|
|
124
|
+
* since the reader can act on it; anything else stays bare, because the
|
|
125
|
+
* detail is a transport error the reader cannot do anything about.
|
|
126
|
+
*/
|
|
127
|
+
const describeFailure = (name: string, err: unknown): string =>
|
|
128
|
+
err instanceof DiscordNonRetryableError
|
|
129
|
+
? `${name} (Discord returned ${err.status})`
|
|
130
|
+
: name;
|
|
131
|
+
|
|
132
|
+
for (const meta of attachments) {
|
|
133
|
+
const displayName = meta.filename ?? meta.id;
|
|
134
|
+
|
|
135
|
+
if (
|
|
136
|
+
meta.sizeBytes !== undefined &&
|
|
137
|
+
meta.sizeBytes > DISCORD_MAX_ATTACHMENT_BYTES
|
|
138
|
+
) {
|
|
139
|
+
log.warn(
|
|
140
|
+
{ attachmentId: meta.id, sizeBytes: meta.sizeBytes },
|
|
141
|
+
"Skipping oversized outbound attachment",
|
|
142
|
+
);
|
|
143
|
+
failures.push(displayName);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const content = getAttachmentContent(meta.id);
|
|
149
|
+
if (!content) {
|
|
150
|
+
log.error(
|
|
151
|
+
{ attachmentId: meta.id },
|
|
152
|
+
"Attachment content not found in store",
|
|
153
|
+
);
|
|
154
|
+
failures.push(displayName);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (content.length > DISCORD_MAX_ATTACHMENT_BYTES) {
|
|
159
|
+
log.warn(
|
|
160
|
+
{ attachmentId: meta.id, sizeBytes: content.length },
|
|
161
|
+
"Skipping oversized outbound attachment (detected after read)",
|
|
162
|
+
);
|
|
163
|
+
failures.push(displayName);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const blob = new Blob([new Uint8Array(content)], {
|
|
168
|
+
type: meta.mimeType ?? "application/octet-stream",
|
|
169
|
+
});
|
|
170
|
+
const form = new FormData();
|
|
171
|
+
form.set(
|
|
172
|
+
"payload_json",
|
|
173
|
+
JSON.stringify({
|
|
174
|
+
attachments: [{ id: 0, filename: displayName }],
|
|
175
|
+
allowed_mentions: DISCORD_ALLOWED_MENTIONS,
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
form.set("files[0]", blob, displayName);
|
|
179
|
+
|
|
180
|
+
await callDiscordApiMultipart(messagesRoute(target), form);
|
|
181
|
+
|
|
182
|
+
log.debug(
|
|
183
|
+
{ channelId: target.channelId, attachmentId: meta.id },
|
|
184
|
+
"Attachment sent to Discord",
|
|
185
|
+
);
|
|
186
|
+
} catch (err) {
|
|
187
|
+
log.error(
|
|
188
|
+
{ err, attachmentId: meta.id, filename: displayName },
|
|
189
|
+
"Failed to send attachment to Discord",
|
|
190
|
+
);
|
|
191
|
+
failures.push(describeFailure(displayName, err));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (failures.length > 0) {
|
|
196
|
+
const notice = `⚠️ ${failures.length} attachment(s) could not be delivered: ${failures.join(", ")}`;
|
|
197
|
+
try {
|
|
198
|
+
await sendDiscordReply(target, notice);
|
|
199
|
+
} catch (err) {
|
|
200
|
+
log.error(
|
|
201
|
+
{ err, channelId: target.channelId },
|
|
202
|
+
"Failed to send attachment failure notice",
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
allFailed: failures.length === attachments.length,
|
|
209
|
+
failureCount: failures.length,
|
|
210
|
+
totalCount: attachments.length,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ChannelDeliveryError } from "@vellumai/gateway-client/http-delivery";
|
|
2
|
+
|
|
3
|
+
import { getLogger } from "../../../util/logger.js";
|
|
4
|
+
import type {
|
|
5
|
+
CallbackContext,
|
|
6
|
+
ChannelTransport,
|
|
7
|
+
} from "../channel-transport.js";
|
|
8
|
+
import { openDiscordDmChannel } from "./api.js";
|
|
9
|
+
import type { DiscordSendTarget } from "./send.js";
|
|
10
|
+
import { sendDiscordAttachments, sendDiscordReply } from "./send.js";
|
|
11
|
+
|
|
12
|
+
const log = getLogger("discord-transport");
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolve where a reply is posted.
|
|
16
|
+
*
|
|
17
|
+
* The gateway normalizer sets `conversationExternalId` (which arrives as
|
|
18
|
+
* `payload.chatId`) to the *parent* channel for a thread message, mirroring
|
|
19
|
+
* Slack's channel + thread_ts split, and carries the thread's own snowflake on
|
|
20
|
+
* the callback URL's `threadId` param. A Discord thread is itself a channel,
|
|
21
|
+
* so a threaded reply posts to the thread id and everything else to the
|
|
22
|
+
* channel id.
|
|
23
|
+
*
|
|
24
|
+
* The `dm` param inverts what `chatId` means: it marks the value as a
|
|
25
|
+
* *recipient user* snowflake to be reached privately, which is resolved to a
|
|
26
|
+
* DM channel here. Callers that address a person rather than a room set it,
|
|
27
|
+
* because a Discord user id and a channel id are both bare snowflakes and
|
|
28
|
+
* nothing in the value itself says which one arrived.
|
|
29
|
+
*
|
|
30
|
+
* `channel` and `threadId` are the only coordinates delivery needs;
|
|
31
|
+
* `POST /channels/{id}/messages` addresses a channel and nothing more.
|
|
32
|
+
*/
|
|
33
|
+
async function sendTarget(
|
|
34
|
+
ctx: CallbackContext,
|
|
35
|
+
chatId: string,
|
|
36
|
+
): Promise<DiscordSendTarget> {
|
|
37
|
+
if (ctx.params.dm === "1") {
|
|
38
|
+
return { channelId: await openDiscordDmChannel(chatId) };
|
|
39
|
+
}
|
|
40
|
+
return { channelId: ctx.params.threadId?.trim() || chatId };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const discordTransport: ChannelTransport = {
|
|
44
|
+
channel: "discord",
|
|
45
|
+
|
|
46
|
+
async deliver(ctx, payload) {
|
|
47
|
+
const { chatId, text, attachments, approval } = payload;
|
|
48
|
+
const target = await sendTarget(ctx, chatId);
|
|
49
|
+
|
|
50
|
+
let sentId: string | undefined;
|
|
51
|
+
if (text) {
|
|
52
|
+
const result = await sendDiscordReply(target, text);
|
|
53
|
+
sentId = result.lastMessageId;
|
|
54
|
+
} else if (approval) {
|
|
55
|
+
// Approvals deliver as plain text, so the prompt is readable but not
|
|
56
|
+
// clickable. Discord is not a guardian channel: approval prompts are
|
|
57
|
+
// actioned from the guardian's own channel, not from here.
|
|
58
|
+
const result = await sendDiscordReply(
|
|
59
|
+
target,
|
|
60
|
+
approval.plainTextFallback || "Approval required",
|
|
61
|
+
);
|
|
62
|
+
sentId = result.lastMessageId;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (attachments && attachments.length > 0) {
|
|
66
|
+
const result = await sendDiscordAttachments(target, attachments);
|
|
67
|
+
if (result.allFailed && !text) {
|
|
68
|
+
throw new ChannelDeliveryError(
|
|
69
|
+
502,
|
|
70
|
+
`All ${result.failureCount} attachments failed to deliver`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
log.info(
|
|
76
|
+
{ channelId: target.channelId, hasText: !!text },
|
|
77
|
+
"Discord reply delivered (direct)",
|
|
78
|
+
);
|
|
79
|
+
return { ok: true, ts: sentId };
|
|
80
|
+
},
|
|
81
|
+
};
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* lives in `callback-routing.ts`. The gateway-client consults
|
|
6
6
|
* `isDirectDelivery()` before falling back to the HTTP proxy path.
|
|
7
7
|
*
|
|
8
|
-
* Supported: Slack, Telegram, WhatsApp, A2A.
|
|
8
|
+
* Supported: Slack, Telegram, WhatsApp, A2A, Discord.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type {
|
|
@@ -17,6 +17,7 @@ import { a2aTransport } from "./a2a/transport.js";
|
|
|
17
17
|
import type { DirectDeliveryChannel } from "./callback-routing.js";
|
|
18
18
|
import { channelForCallback } from "./callback-routing.js";
|
|
19
19
|
import type { CallbackContext, ChannelTransport } from "./channel-transport.js";
|
|
20
|
+
import { discordTransport } from "./discord/transport.js";
|
|
20
21
|
import { slackTransport } from "./slack/transport.js";
|
|
21
22
|
import { telegramTransport } from "./telegram-bot/transport.js";
|
|
22
23
|
import { whatsappTransport } from "./whatsapp/transport.js";
|
|
@@ -30,6 +31,7 @@ const TRANSPORTS: Record<DirectDeliveryChannel, ChannelTransport> = {
|
|
|
30
31
|
telegram: telegramTransport,
|
|
31
32
|
whatsapp: whatsappTransport,
|
|
32
33
|
a2a: a2aTransport,
|
|
34
|
+
discord: discordTransport,
|
|
33
35
|
};
|
|
34
36
|
|
|
35
37
|
/**
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type { RetryableCall } from "./retry-policy.js";
|
|
4
|
+
import {
|
|
5
|
+
computeRetryDelayMs,
|
|
6
|
+
isRetryableStatus,
|
|
7
|
+
MAX_TIMER_DELAY_MS,
|
|
8
|
+
retryableCall,
|
|
9
|
+
} from "./retry-policy.js";
|
|
10
|
+
|
|
11
|
+
describe("isRetryableStatus", () => {
|
|
12
|
+
test("retries rate limits and server faults", () => {
|
|
13
|
+
expect(isRetryableStatus(429)).toBe(true);
|
|
14
|
+
expect(isRetryableStatus(500)).toBe(true);
|
|
15
|
+
expect(isRetryableStatus(502)).toBe(true);
|
|
16
|
+
expect(isRetryableStatus(503)).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("does not retry a request the server rejected on its merits", () => {
|
|
20
|
+
// A retry reproduces these exactly, so repeating them only burns quota.
|
|
21
|
+
for (const status of [400, 401, 403, 404, 413, 422]) {
|
|
22
|
+
expect(isRetryableStatus(status)).toBe(false);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("does not retry success", () => {
|
|
27
|
+
expect(isRetryableStatus(200)).toBe(false);
|
|
28
|
+
expect(isRetryableStatus(204)).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("computeRetryDelayMs", () => {
|
|
33
|
+
test("honours a Retry-After given in seconds", () => {
|
|
34
|
+
expect(computeRetryDelayMs(1, 1000, "3")).toBe(3000);
|
|
35
|
+
// Discord sends fractional seconds.
|
|
36
|
+
expect(computeRetryDelayMs(1, 1000, "0.75")).toBe(750);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("honours a Retry-After given as an HTTP date", () => {
|
|
40
|
+
const target = new Date(Date.now() + 5000).toUTCString();
|
|
41
|
+
const delay = computeRetryDelayMs(1, 1000, target);
|
|
42
|
+
// Whole-second date resolution, so allow the truncation either way.
|
|
43
|
+
expect(delay).toBeGreaterThan(3000);
|
|
44
|
+
expect(delay).toBeLessThanOrEqual(5000);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("caps a server-advertised wait at the caller's bound", () => {
|
|
48
|
+
// A caller holding a lock across the wait passes a small bound so a long
|
|
49
|
+
// rate-limit window fails the call instead of stalling behind it.
|
|
50
|
+
expect(computeRetryDelayMs(1, 1000, "3600", 60_000)).toBe(60_000);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("caps at the timer ceiling by default, so the wait is real", () => {
|
|
54
|
+
// Past this setTimeout fires immediately, turning a long wait into none.
|
|
55
|
+
expect(computeRetryDelayMs(1, 1000, "999999999")).toBe(MAX_TIMER_DELAY_MS);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("backs off exponentially when the server gives no hint", () => {
|
|
59
|
+
for (const [attempt, base] of [
|
|
60
|
+
[1, 1000],
|
|
61
|
+
[2, 2000],
|
|
62
|
+
[3, 4000],
|
|
63
|
+
] as const) {
|
|
64
|
+
const delay = computeRetryDelayMs(attempt, 1000, null);
|
|
65
|
+
expect(delay).toBeGreaterThanOrEqual(base);
|
|
66
|
+
// Jitter is additive and bounded at half the exponential term.
|
|
67
|
+
expect(delay).toBeLessThanOrEqual(base * 1.5);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("jitters, so callers that failed together do not resume together", () => {
|
|
72
|
+
const delays = new Set(
|
|
73
|
+
Array.from({ length: 20 }, () => computeRetryDelayMs(3, 1000, null)),
|
|
74
|
+
);
|
|
75
|
+
expect(delays.size).toBeGreaterThan(1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("falls back to backoff for unusable Retry-After values", () => {
|
|
79
|
+
for (const value of ["", "soon", "-5", "0"]) {
|
|
80
|
+
const delay = computeRetryDelayMs(1, 1000, value);
|
|
81
|
+
expect(delay).toBeGreaterThanOrEqual(1000);
|
|
82
|
+
expect(delay).toBeLessThanOrEqual(1500);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("ignores an already-elapsed Retry-After date", () => {
|
|
87
|
+
const past = new Date(Date.now() - 10_000).toUTCString();
|
|
88
|
+
const delay = computeRetryDelayMs(1, 1000, past);
|
|
89
|
+
expect(delay).toBeGreaterThanOrEqual(1000);
|
|
90
|
+
expect(delay).toBeLessThanOrEqual(1500);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("retryableCall", () => {
|
|
95
|
+
const silent = { debug() {}, warn() {} };
|
|
96
|
+
|
|
97
|
+
function spec<T>(overrides: Partial<RetryableCall<T>>): RetryableCall<T> {
|
|
98
|
+
return {
|
|
99
|
+
provider: "Test",
|
|
100
|
+
operation: "doThing",
|
|
101
|
+
maxRetries: 2,
|
|
102
|
+
initialBackoffMs: 1,
|
|
103
|
+
log: silent,
|
|
104
|
+
doFetch: async () => new Response("{}", { status: 200 }),
|
|
105
|
+
nonRetryableError: ({ message }) => new Error(message),
|
|
106
|
+
decode: (body) => JSON.parse(body) as T,
|
|
107
|
+
...overrides,
|
|
108
|
+
} as RetryableCall<T>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
test("returns the decoded body without retrying on success", async () => {
|
|
112
|
+
let calls = 0;
|
|
113
|
+
const result = await retryableCall<{ v: number }>(
|
|
114
|
+
spec({
|
|
115
|
+
doFetch: async () => {
|
|
116
|
+
calls++;
|
|
117
|
+
return new Response(JSON.stringify({ v: 7 }), { status: 200 });
|
|
118
|
+
},
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
expect(result).toEqual({ v: 7 });
|
|
122
|
+
expect(calls).toBe(1);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("repeats a rate limit and returns the eventual success", async () => {
|
|
126
|
+
let calls = 0;
|
|
127
|
+
const result = await retryableCall<{ ok: boolean }>(
|
|
128
|
+
spec({
|
|
129
|
+
doFetch: async () => {
|
|
130
|
+
calls++;
|
|
131
|
+
return calls === 1
|
|
132
|
+
? new Response("{}", { status: 429 })
|
|
133
|
+
: new Response(JSON.stringify({ ok: true }), { status: 200 });
|
|
134
|
+
},
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
expect(result).toEqual({ ok: true });
|
|
138
|
+
expect(calls).toBe(2);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("throws immediately on a status a retry would reproduce", async () => {
|
|
142
|
+
let calls = 0;
|
|
143
|
+
await expect(
|
|
144
|
+
retryableCall(
|
|
145
|
+
spec({
|
|
146
|
+
doFetch: async () => {
|
|
147
|
+
calls++;
|
|
148
|
+
return new Response('{"m":"nope"}', { status: 403 });
|
|
149
|
+
},
|
|
150
|
+
nonRetryableError: ({ status, message }) =>
|
|
151
|
+
new Error(`terminal ${status}: ${message}`),
|
|
152
|
+
}),
|
|
153
|
+
),
|
|
154
|
+
).rejects.toThrow(/terminal 403/);
|
|
155
|
+
// The point of the distinction: a 403 is not retried.
|
|
156
|
+
expect(calls).toBe(1);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("surfaces the last failure, not a generic one, after exhausting retries", async () => {
|
|
160
|
+
await expect(
|
|
161
|
+
retryableCall(
|
|
162
|
+
spec({
|
|
163
|
+
maxRetries: 1,
|
|
164
|
+
doFetch: async () =>
|
|
165
|
+
new Response('{"detail":"upstream on fire"}', { status: 503 }),
|
|
166
|
+
detailFrom: (body) =>
|
|
167
|
+
(JSON.parse(body) as { detail?: string }).detail,
|
|
168
|
+
}),
|
|
169
|
+
),
|
|
170
|
+
).rejects.toThrow(/upstream on fire/);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("retries a transport error and reports it if it never recovers", async () => {
|
|
174
|
+
let calls = 0;
|
|
175
|
+
await expect(
|
|
176
|
+
retryableCall(
|
|
177
|
+
spec({
|
|
178
|
+
maxRetries: 1,
|
|
179
|
+
doFetch: async () => {
|
|
180
|
+
calls++;
|
|
181
|
+
throw new Error("socket hang up");
|
|
182
|
+
},
|
|
183
|
+
}),
|
|
184
|
+
),
|
|
185
|
+
).rejects.toThrow(/socket hang up/);
|
|
186
|
+
expect(calls).toBe(2);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("redacts before text reaches a message", async () => {
|
|
190
|
+
// Providers whose errors echo a credential rely on this.
|
|
191
|
+
await expect(
|
|
192
|
+
retryableCall(
|
|
193
|
+
spec({
|
|
194
|
+
maxRetries: 0,
|
|
195
|
+
doFetch: async () => new Response("token=SECRET", { status: 500 }),
|
|
196
|
+
redact: (v) => v.replace("SECRET", "[REDACTED]"),
|
|
197
|
+
}),
|
|
198
|
+
),
|
|
199
|
+
).rejects.toThrow(/\[REDACTED\]/);
|
|
200
|
+
await expect(
|
|
201
|
+
retryableCall(
|
|
202
|
+
spec({
|
|
203
|
+
maxRetries: 0,
|
|
204
|
+
doFetch: async () => new Response("token=SECRET", { status: 500 }),
|
|
205
|
+
redact: (v) => v.replace("SECRET", "[REDACTED]"),
|
|
206
|
+
}),
|
|
207
|
+
),
|
|
208
|
+
).rejects.not.toThrow(/SECRET(?!\])/);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("prefers a body-carried Retry-After over the header", async () => {
|
|
212
|
+
const seen: (string | null)[] = [];
|
|
213
|
+
let calls = 0;
|
|
214
|
+
await retryableCall(
|
|
215
|
+
spec({
|
|
216
|
+
doFetch: async () => {
|
|
217
|
+
calls++;
|
|
218
|
+
return calls === 1
|
|
219
|
+
? new Response('{"retry_after":0.001}', {
|
|
220
|
+
status: 429,
|
|
221
|
+
headers: { "retry-after": "999" },
|
|
222
|
+
})
|
|
223
|
+
: new Response("{}", { status: 200 });
|
|
224
|
+
},
|
|
225
|
+
retryAfterFrom: (_r, body) => {
|
|
226
|
+
const v = (JSON.parse(body) as { retry_after?: number }).retry_after;
|
|
227
|
+
const picked = v != null ? String(v) : null;
|
|
228
|
+
seen.push(picked);
|
|
229
|
+
return picked;
|
|
230
|
+
},
|
|
231
|
+
}),
|
|
232
|
+
);
|
|
233
|
+
// The header said 999 seconds; the body's value is the one used.
|
|
234
|
+
expect(seen).toEqual(["0.001"]);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("passes an empty body to decode rather than assuming JSON", async () => {
|
|
238
|
+
// A 204 has nothing to parse, and only the caller knows what that means.
|
|
239
|
+
const result = await retryableCall<string>(
|
|
240
|
+
spec({
|
|
241
|
+
doFetch: async () => new Response(null, { status: 204 }),
|
|
242
|
+
decode: (body) => (body === "" ? "empty" : "unexpected"),
|
|
243
|
+
}),
|
|
244
|
+
);
|
|
245
|
+
expect(result).toBe("empty");
|
|
246
|
+
});
|
|
247
|
+
});
|