@vellumai/assistant 0.8.1 → 0.8.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 +2 -7
- package/Dockerfile +75 -1
- package/bun.lock +11 -1
- package/docker-entrypoint.sh +5 -0
- package/docker-init-apt-root.sh +94 -0
- package/docker-kata-apt-env.sh +39 -0
- package/docs/plugins.md +88 -47
- package/docs/skills.md +9 -7
- package/examples/plugins/echo/README.md +27 -27
- package/examples/plugins/echo/package.json +3 -0
- package/examples/plugins/echo/register.ts +31 -31
- package/node_modules/@vellumai/slack-text/src/index.test.ts +114 -14
- package/node_modules/@vellumai/slack-text/src/index.ts +82 -18
- package/openapi.yaml +325 -3
- package/package.json +3 -1
- package/scripts/generate-openapi.ts +83 -10
- package/scripts/sync-llm-catalog.ts +2 -2
- package/scripts/sync-web-search-catalog.ts +47 -25
- package/src/__tests__/agent-image-optimize.test.ts +11 -3
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +131 -0
- package/src/__tests__/anthropic-provider.test.ts +45 -0
- package/src/__tests__/app-builder-tool-scripts.test.ts +9 -3
- package/src/__tests__/app-executors.test.ts +220 -4
- package/src/__tests__/auto-analysis-end-to-end.test.ts +35 -0
- package/src/__tests__/bundled-asset.test.ts +6 -6
- package/src/__tests__/channel-availability-routes.test.ts +206 -0
- package/src/__tests__/channel-delivery-store.test.ts +289 -1
- package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -1
- package/src/__tests__/clawhub.test.ts +75 -16
- package/src/__tests__/compactor-tail-resolution.test.ts +41 -0
- package/src/__tests__/config-schema.test.ts +21 -0
- package/src/__tests__/config-set-route.test.ts +80 -0
- package/src/__tests__/config-sounds-sync.test.ts +97 -0
- package/src/__tests__/config-watcher-skill-reseed.test.ts +453 -0
- package/src/__tests__/context-search-conversations-source.test.ts +117 -2
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
- package/src/__tests__/context-search-workspace-source.test.ts +7 -0
- package/src/__tests__/context-token-estimator.test.ts +1 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +92 -92
- package/src/__tests__/conversation-agent-loop.test.ts +2 -0
- package/src/__tests__/conversation-error.test.ts +42 -3
- package/src/__tests__/conversation-fork-crud.test.ts +82 -0
- package/src/__tests__/conversation-inference-profile-route.test.ts +40 -4
- package/src/__tests__/conversation-lifecycle.test.ts +173 -0
- package/src/__tests__/conversation-message-sync-tags.test.ts +97 -0
- package/src/__tests__/conversation-pairing.test.ts +54 -0
- package/src/__tests__/conversation-process-callsite.test.ts +4 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -1
- package/src/__tests__/conversation-queue.test.ts +4 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +76 -9
- package/src/__tests__/conversation-slash-queue.test.ts +59 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +4 -1
- package/src/__tests__/conversation-surfaces-table-action.test.ts +360 -0
- package/src/__tests__/conversation-sync-tags.test.ts +235 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
- package/src/__tests__/credential-security-invariants.test.ts +3 -2
- package/src/__tests__/db-slack-external-content-normalization.test.ts +301 -0
- package/src/__tests__/delete-managed-skill-tool.test.ts +55 -13
- package/src/__tests__/disk-pressure-tools.test.ts +1 -0
- package/src/__tests__/dm-backfill.test.ts +121 -10
- package/src/__tests__/document-tool-security.test.ts +258 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
- package/src/__tests__/edit-propagation.test.ts +33 -0
- package/src/__tests__/empty-response-pipeline.test.ts +0 -4
- package/src/__tests__/external-plugin-loader.test.ts +60 -36
- package/src/__tests__/filing-service.test.ts +140 -0
- package/src/__tests__/get-skill-detail-audit.test.ts +0 -4
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +43 -62
- package/src/__tests__/helpers/tar-fixtures.ts +39 -0
- package/src/__tests__/helpers/wait-for.ts +21 -0
- package/src/__tests__/history-repair-pipeline.test.ts +0 -3
- package/src/__tests__/history-repair.test.ts +73 -0
- package/src/__tests__/host-app-control-proxy.test.ts +266 -10
- package/src/__tests__/image-credentials.test.ts +1 -1
- package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
- package/src/__tests__/inference-no-mode-boot-e2e.test.ts +1 -1
- package/src/__tests__/inference-profile-reaper.test.ts +4 -2
- package/src/__tests__/inference-profile-session-handler.test.ts +18 -6
- package/src/__tests__/inference-profile-session-ipc.test.ts +17 -5
- package/src/__tests__/injector-chain.test.ts +10 -8
- package/src/__tests__/install-skill-routing.test.ts +155 -37
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +92 -3
- package/src/__tests__/list-messages-page-latest.test.ts +55 -0
- package/src/__tests__/llm-call-pipeline.test.ts +0 -3
- package/src/__tests__/llm-catalog-parity.test.ts +55 -13
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +34 -0
- package/src/__tests__/llm-request-log-source-factory.test.ts +29 -53
- package/src/__tests__/llm-usage-store.test.ts +114 -0
- package/src/__tests__/managed-profile-guard.test.ts +31 -29
- package/src/__tests__/managed-skill-lifecycle.test.ts +109 -18
- package/src/__tests__/managed-store.test.ts +84 -192
- package/src/__tests__/media-generate-image.test.ts +1 -1
- package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -2
- package/src/__tests__/messages-after-tiebreaker.test.ts +122 -0
- package/src/__tests__/oauth-commands-routes.test.ts +168 -16
- package/src/__tests__/oauth-provider-profiles.test.ts +9 -0
- package/src/__tests__/openai-provider.test.ts +24 -0
- package/src/__tests__/openai-responses-cutover-guard.test.ts +17 -9
- package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -2
- package/src/__tests__/persistence-pipeline.test.ts +0 -2
- package/src/__tests__/{managed-proxy-context.test.ts → platform-proxy-context.test.ts} +1 -1
- package/src/__tests__/platform.test.ts +2 -0
- package/src/__tests__/plugin-api-shim.test.ts +125 -0
- package/src/__tests__/plugin-bootstrap.test.ts +10 -36
- package/src/__tests__/plugin-external-api.test.ts +68 -0
- package/src/__tests__/plugin-registry.test.ts +0 -77
- package/src/__tests__/plugin-route-contribution.test.ts +0 -1
- package/src/__tests__/plugin-skill-contribution.test.ts +0 -2
- package/src/__tests__/plugin-tool-contribution.test.ts +16 -15
- package/src/__tests__/plugin-types.test.ts +3 -13
- package/src/__tests__/process-message-background-slack.test.ts +8 -1
- package/src/__tests__/process-message-display-content.test.ts +421 -0
- package/src/__tests__/provider-catalog-visibility.test.ts +142 -0
- package/src/__tests__/provider-error-scenarios.test.ts +111 -0
- package/src/__tests__/{provider-managed-proxy-integration.test.ts → provider-platform-proxy-integration.test.ts} +8 -8
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +65 -13
- package/src/__tests__/schedule-routes.test.ts +50 -3
- package/src/__tests__/schedule-store.test.ts +94 -0
- package/src/__tests__/scheduler-reuse-conversation.test.ts +54 -7
- package/src/__tests__/schema-transforms.test.ts +20 -0
- package/src/__tests__/search-skills-unified.test.ts +0 -5
- package/src/__tests__/server-history-render.test.ts +43 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +0 -12
- package/src/__tests__/skill-load-tool.test.ts +27 -89
- package/src/__tests__/skill-memory.test.ts +23 -3
- package/src/__tests__/skills-file-content-endpoint.test.ts +9 -38
- package/src/__tests__/skills-files-catalog-fallback.test.ts +0 -3
- package/src/__tests__/skills-install-extract.test.ts +49 -38
- package/src/__tests__/skills-install-staging.test.ts +159 -0
- package/src/__tests__/skills-uninstall.test.ts +9 -41
- package/src/__tests__/skills.test.ts +51 -58
- package/src/__tests__/slack-channel-config.test.ts +9 -0
- package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
- package/src/__tests__/system-prompt.test.ts +737 -63
- package/src/__tests__/terminal-tools.test.ts +28 -1
- package/src/__tests__/thread-backfill.test.ts +557 -27
- package/src/__tests__/title-generate-pipeline.test.ts +0 -13
- package/src/__tests__/token-estimate-pipeline.test.ts +0 -3
- package/src/__tests__/tool-error-pipeline.test.ts +0 -3
- package/src/__tests__/tool-execute-pipeline.test.ts +0 -5
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
- package/src/__tests__/tool-executor.test.ts +16 -4
- package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -12
- package/src/__tests__/turn-events-store.test.ts +256 -0
- package/src/__tests__/twilio-routes.test.ts +4 -0
- package/src/__tests__/user-plugin-loader.test.ts +0 -7
- package/src/__tests__/voice-session-bridge.test.ts +198 -0
- package/src/__tests__/web-search-catalog-parity.test.ts +32 -10
- package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +115 -3
- package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +50 -0
- package/src/__tests__/workspace-migration-073-repair-recall-callsite-empty-profile.test.ts +153 -0
- package/src/__tests__/workspace-migration-085-memory-v2-bm25-b-reembed-disabled-v2-pages.test.ts +220 -0
- package/src/__tests__/workspace-migration-086-revert-stale-gemini-mis-rewrites.test.ts +269 -0
- package/src/__tests__/workspace-migration-remove-legacy-skills-index.test.ts +309 -0
- package/src/__tests__/workspace-migrations-runner.test.ts +111 -3
- package/src/acp/resolve-agent.ts +1 -1
- package/src/agent/image-optimize.ts +13 -5
- package/src/calls/voice-session-bridge.ts +61 -42
- package/src/channels/types.ts +108 -0
- package/src/cli/__tests__/unknown-command.test.ts +24 -0
- package/src/cli/commands/__tests__/changelog.test.ts +304 -319
- package/src/cli/commands/__tests__/schedules.test.ts +491 -0
- package/src/cli/commands/changelog.ts +106 -42
- package/src/cli/commands/conversations.ts +102 -17
- package/src/cli/commands/default-action.ts +10 -53
- package/src/cli/commands/notifications.ts +329 -317
- package/src/cli/commands/plugins.ts +185 -0
- package/src/cli/commands/schedules.ts +391 -0
- package/src/cli/commands/telemetry.ts +40 -0
- package/src/cli/lib/__tests__/cli-colors.test.ts +48 -0
- package/src/cli/lib/__tests__/confirm-prompt.test.ts +159 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +355 -0
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +154 -0
- package/src/cli/lib/__tests__/uninstall-plugin.test.ts +124 -0
- package/src/cli/lib/__tests__/unknown-command.test.ts +106 -0
- package/src/cli/lib/cli-colors.ts +12 -0
- package/src/cli/lib/confirm-prompt.ts +79 -0
- package/src/cli/lib/install-from-github.ts +304 -0
- package/src/cli/lib/list-installed-plugins.ts +137 -0
- package/src/cli/lib/uninstall-plugin.ts +82 -0
- package/src/cli/lib/unknown-command.ts +111 -0
- package/src/cli/program.ts +38 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +23 -21
- package/src/config/bundled-skills/app-builder/TOOLS.json +7 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +15 -52
- package/src/config/bundled-skills/document/SKILL.md +23 -3
- package/src/config/bundled-skills/document/TOOLS.json +53 -0
- package/src/config/bundled-skills/document/tools/document-delete.ts +12 -0
- package/src/config/bundled-skills/document/tools/document-list.ts +12 -0
- package/src/config/bundled-skills/document/tools/document-read.ts +12 -0
- package/src/config/bundled-skills/skill-management/SKILL.md +2 -2
- package/src/config/bundled-skills/skill-management/TOOLS.json +7 -7
- package/src/config/bundled-tool-registry.ts +6 -0
- package/src/config/feature-flag-registry.json +41 -1
- package/src/config/loader.ts +64 -38
- package/src/config/schema.ts +7 -10
- package/src/config/schemas/__tests__/llm-request-logs.test.ts +36 -0
- package/src/config/schemas/channels.ts +8 -0
- package/src/config/schemas/compaction.ts +28 -0
- package/src/config/schemas/heartbeat.ts +9 -0
- package/src/config/schemas/llm-request-logs.ts +31 -7
- package/src/config/schemas/llm.ts +3 -0
- package/src/config/schemas/memory-retrieval.ts +18 -0
- package/src/config/schemas/tools.ts +14 -0
- package/src/config/skills.ts +3 -96
- package/src/context/compactor.ts +1047 -0
- package/src/context/token-estimator.ts +2 -2
- package/src/context/window-manager.ts +197 -1520
- package/src/credential-execution/managed-catalog.ts +37 -0
- package/src/credential-health/credential-health-service.ts +280 -19
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +34 -0
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +138 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +74 -0
- package/src/daemon/approval-generators.ts +8 -6
- package/src/daemon/config-watcher.ts +94 -31
- package/src/daemon/conversation-agent-loop.ts +169 -9
- package/src/daemon/conversation-error.ts +171 -37
- package/src/daemon/conversation-lifecycle.ts +53 -40
- package/src/daemon/conversation-messaging.ts +25 -6
- package/src/daemon/conversation-process.ts +49 -12
- package/src/daemon/conversation-runtime-assembly.ts +16 -1
- package/src/daemon/conversation-slash.ts +12 -5
- package/src/daemon/conversation-store.ts +11 -4
- package/src/daemon/conversation-tool-setup.ts +39 -7
- package/src/daemon/conversation.ts +33 -1
- package/src/daemon/external-plugins-bootstrap.ts +217 -181
- package/src/daemon/first-greeting.ts +22 -2
- package/src/daemon/handlers/config-model.ts +6 -5
- package/src/daemon/handlers/config-slack-channel.ts +15 -3
- package/src/daemon/handlers/shared.ts +14 -5
- package/src/daemon/handlers/skills.ts +111 -108
- package/src/daemon/history-repair.ts +28 -1
- package/src/daemon/host-app-control-proxy.ts +98 -23
- package/src/daemon/lifecycle.ts +45 -35
- package/src/daemon/meet-host-supervisor.ts +5 -4
- package/src/daemon/memory-v2-startup.ts +49 -0
- package/src/daemon/message-protocol.ts +1 -0
- package/src/daemon/message-types/conversations.ts +25 -0
- package/src/daemon/message-types/messages.ts +61 -0
- package/src/daemon/message-types/subagents.ts +1 -0
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/pkb-reminder-builder.test.ts +1 -1
- package/src/daemon/pkb-reminder-builder.ts +1 -1
- package/src/daemon/plugin-source-watcher.ts +146 -0
- package/src/daemon/process-message.ts +21 -3
- package/src/daemon/server.ts +11 -2
- package/src/daemon/skill-memory-refresh.ts +29 -0
- package/src/documents/document-store.ts +221 -3
- package/src/embedded/plugin-api.ts +40 -0
- package/src/filing/filing-service.ts +39 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +91 -6
- package/src/heartbeat/heartbeat-run-store.ts +2 -1
- package/src/heartbeat/heartbeat-service.ts +41 -0
- package/src/home/__tests__/feed-types.test.ts +40 -0
- package/src/home/feed-types.ts +22 -0
- package/src/home/post-connect-feed.ts +1 -0
- package/src/index.ts +18 -1
- package/src/live-voice/__tests__/live-voice-stt.test.ts +57 -0
- package/src/mcp/client.ts +20 -4
- package/src/media/image-credentials.ts +3 -3
- package/src/memory/__tests__/bookmark-crud.test.ts +33 -27
- package/src/memory/__tests__/conversation-queries.test.ts +263 -0
- package/src/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +113 -0
- package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +119 -14
- package/src/memory/__tests__/message-content.test.ts +35 -0
- package/src/memory/bookmark-crud.ts +42 -10
- package/src/memory/context-search/sources/conversations.ts +62 -2
- package/src/memory/context-search/sources/workspace.ts +4 -0
- package/src/memory/conversation-crud.ts +63 -19
- package/src/memory/conversation-queries.ts +110 -10
- package/src/memory/db-init.ts +6 -0
- package/src/memory/delivery-crud.ts +152 -5
- package/src/memory/embedding-backend.ts +4 -4
- package/src/memory/external-conversation-store.ts +66 -5
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +66 -9
- package/src/memory/graph/conversation-graph-memory.ts +31 -15
- package/src/memory/graph/tools.ts +3 -3
- package/src/memory/indexer.ts +34 -29
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +73 -0
- package/src/memory/jobs/embed-concept-page.ts +20 -11
- package/src/memory/jobs-worker.ts +6 -1
- package/src/memory/llm-request-log-source-clickhouse.ts +17 -10
- package/src/memory/llm-request-log-source.ts +19 -52
- package/src/memory/llm-usage-store.ts +125 -5
- package/src/memory/memory-retrospective-startup-cleanup.ts +72 -5
- package/src/memory/message-content.ts +1 -1
- package/src/memory/migrations/109-external-conversation-bindings.ts +15 -4
- package/src/memory/migrations/229-delete-private-conversations.test.ts +38 -1
- package/src/memory/migrations/229-delete-private-conversations.ts +7 -0
- package/src/memory/migrations/247-external-conversation-binding-thread-id.ts +78 -0
- package/src/memory/migrations/248-create-onboarding-events.ts +21 -0
- package/src/memory/migrations/249-normalize-slack-external-content.ts +240 -0
- package/src/memory/migrations/index.ts +6 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/onboarding-events-store.ts +106 -0
- package/src/memory/schema/bookmarks.ts +0 -2
- package/src/memory/schema/calls.ts +1 -0
- package/src/memory/schema/inference.ts +1 -3
- package/src/memory/schema/infrastructure.ts +12 -0
- package/src/memory/turn-events-store.ts +127 -2
- package/src/memory/v2/__tests__/activation.test.ts +0 -8
- package/src/memory/v2/__tests__/injection.test.ts +98 -8
- package/src/memory/v2/__tests__/migration.test.ts +87 -0
- package/src/memory/v2/__tests__/page-index.test.ts +83 -0
- package/src/memory/v2/__tests__/prompts-router.test.ts +58 -6
- package/src/memory/v2/__tests__/qdrant.test.ts +66 -3
- package/src/memory/v2/__tests__/router.test.ts +15 -0
- package/src/memory/v2/__tests__/skill-store.test.ts +387 -8
- package/src/memory/v2/injection.ts +32 -6
- package/src/memory/v2/migration.ts +49 -19
- package/src/memory/v2/page-index.ts +35 -5
- package/src/memory/v2/prompts/router.ts +11 -8
- package/src/memory/v2/prompts/sweep.ts +2 -2
- package/src/memory/v2/qdrant.ts +135 -7
- package/src/memory/v2/router.ts +9 -8
- package/src/memory/v2/skill-store.ts +120 -35
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +45 -5
- package/src/messaging/providers/slack/__tests__/download.test.ts +231 -0
- package/src/messaging/providers/slack/adapter.ts +43 -5
- package/src/messaging/providers/slack/client.ts +27 -0
- package/src/messaging/providers/slack/deep-link.ts +65 -0
- package/src/messaging/providers/slack/download.ts +104 -0
- package/src/messaging/providers/slack/message-metadata.test.ts +32 -0
- package/src/messaging/providers/slack/message-metadata.ts +27 -0
- package/src/messaging/providers/slack/render-transcript.test.ts +134 -0
- package/src/messaging/providers/slack/render-transcript.ts +69 -5
- package/src/messaging/providers/slack/types.ts +20 -1
- package/src/notifications/conversation-pairing.ts +2 -1
- package/src/notifications/decision-engine.ts +2 -1
- package/src/notifications/emit-signal.ts +20 -1
- package/src/notifications/home-feed-side-effect.ts +54 -0
- package/src/notifications/signal.ts +3 -1
- package/src/oauth/connection-resolver.ts +8 -4
- package/src/oauth/platform-connection.ts +6 -2
- package/src/oauth/seed-providers.ts +10 -1
- package/src/permissions/checker.ts +2 -0
- package/src/permissions/ipc-risk-types.ts +1 -0
- package/src/permissions/question-prompter.test.ts +416 -0
- package/src/permissions/question-prompter.ts +294 -0
- package/src/platform/client.test.ts +1 -1
- package/src/platform/client.ts +1 -1
- package/src/plugin-api/constants.ts +26 -0
- package/src/plugin-api/index.ts +34 -1
- package/src/plugin-api/types.ts +104 -22
- package/src/plugins/defaults/circuit-breaker.ts +0 -5
- package/src/plugins/defaults/compaction.ts +0 -4
- package/src/plugins/defaults/empty-response.ts +0 -2
- package/src/plugins/defaults/history-repair.ts +0 -2
- package/src/plugins/defaults/injectors.ts +36 -3
- package/src/plugins/defaults/llm-call.ts +0 -2
- package/src/plugins/defaults/memory-retrieval.ts +0 -1
- package/src/plugins/defaults/overflow-reduce.ts +0 -1
- package/src/plugins/defaults/persistence.ts +0 -2
- package/src/plugins/defaults/title-generate.ts +0 -5
- package/src/plugins/defaults/token-estimate.ts +0 -2
- package/src/plugins/defaults/tool-error.ts +0 -7
- package/src/plugins/defaults/tool-execute.ts +0 -2
- package/src/plugins/defaults/tool-result-truncate.ts +0 -4
- package/src/plugins/ensure-plugin-api-shim.ts +96 -0
- package/src/plugins/external-api.ts +104 -0
- package/src/plugins/external-plugin-loader.ts +105 -32
- package/src/plugins/feature-gate.ts +22 -0
- package/src/plugins/pipeline.ts +37 -0
- package/src/plugins/registry.ts +48 -80
- package/src/plugins/types.ts +31 -26
- package/src/plugins/user-loader.ts +21 -2
- package/src/proactive-artifact/aux-message-injector.ts +11 -0
- package/src/proactive-artifact/job.test.ts +37 -5
- package/src/prompts/__tests__/system-prompt.test.ts +12 -0
- package/src/prompts/__tests__/task-progress-hint-section.test.ts +99 -0
- package/src/prompts/normalize-onboarding.ts +27 -0
- package/src/prompts/sections.ts +302 -0
- package/src/prompts/system-prompt.ts +63 -166
- package/src/prompts/templates/BOOTSTRAP.md +17 -1
- package/src/prompts/templates/system-sections.ts +173 -0
- package/src/providers/__tests__/inference.test.ts +22 -7
- package/src/providers/anthropic/client.ts +28 -28
- package/src/providers/connection-resolution.ts +7 -0
- package/src/providers/inference/adapter-factory.ts +41 -4
- package/src/providers/inference/connections.ts +74 -29
- package/src/providers/inference/resolve-auth.ts +12 -4
- package/src/providers/model-catalog.ts +294 -12
- package/src/providers/openai/chat-completions-provider.ts +10 -2
- package/src/providers/openrouter/client.ts +7 -0
- package/src/providers/{managed-proxy → platform-proxy}/constants.ts +4 -1
- package/src/providers/{managed-proxy → platform-proxy}/context.ts +3 -3
- package/src/providers/provider-availability.ts +17 -2
- package/src/providers/provider-catalog-visibility.ts +36 -0
- package/src/providers/registry.ts +22 -14
- package/src/providers/retry.ts +47 -1
- package/src/runtime/__tests__/agent-wake.test.ts +152 -0
- package/src/runtime/agent-wake.ts +42 -14
- package/src/runtime/auth/route-policy.ts +8 -1
- package/src/runtime/btw-sidechain.ts +2 -0
- package/src/runtime/http-types.ts +19 -0
- package/src/runtime/migrations/origin-mode.ts +1 -1
- package/src/runtime/pending-interactions.ts +1 -0
- package/src/runtime/routes/__tests__/bookmark-routes.test.ts +17 -0
- package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +5 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +107 -20
- package/src/runtime/routes/__tests__/question-routes.test.ts +395 -0
- package/src/runtime/routes/__tests__/tts-routes.test.ts +64 -1
- package/src/runtime/routes/acp-routes-list.test.ts +143 -0
- package/src/runtime/routes/acp-routes.ts +5 -3
- package/src/runtime/routes/auth-routes.ts +1 -1
- package/src/runtime/routes/bookmark-routes.ts +5 -3
- package/src/runtime/routes/btw-routes.ts +5 -1
- package/src/runtime/routes/channel-availability-routes.ts +121 -0
- package/src/runtime/routes/conversation-cli-routes.ts +44 -3
- package/src/runtime/routes/conversation-list-routes.ts +3 -20
- package/src/runtime/routes/conversation-management-routes.ts +17 -42
- package/src/runtime/routes/conversation-query-routes.ts +40 -35
- package/src/runtime/routes/conversation-routes.ts +90 -11
- package/src/runtime/routes/documents-routes.ts +25 -86
- package/src/runtime/routes/group-routes.ts +5 -0
- package/src/runtime/routes/inbound-conversation.ts +28 -8
- package/src/runtime/routes/inbound-message-handler.ts +236 -41
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +111 -0
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +32 -1
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +17 -4
- package/src/runtime/routes/index.ts +6 -0
- package/src/runtime/routes/inference-profile-session-handler.ts +17 -44
- package/src/runtime/routes/inference-profile-session-reaper.ts +7 -21
- package/src/runtime/routes/inference-provider-connection-routes.ts +65 -21
- package/src/runtime/routes/integrations/slack/share.ts +4 -52
- package/src/runtime/routes/integrations/slack/token.ts +43 -0
- package/src/runtime/routes/integrations/twilio.ts +6 -13
- package/src/runtime/routes/notification-routes.ts +1 -1
- package/src/runtime/routes/oauth-commands-routes.ts +105 -15
- package/src/runtime/routes/oauth-lifecycle-routes.ts +43 -0
- package/src/runtime/routes/question-routes.ts +259 -0
- package/src/runtime/routes/rename-conversation-routes.ts +2 -33
- package/src/runtime/routes/schedule-routes.ts +4 -7
- package/src/runtime/routes/subagents-routes.ts +57 -18
- package/src/runtime/routes/telemetry-routes.ts +27 -0
- package/src/runtime/routes/tts-routes.ts +27 -2
- package/src/runtime/routes/workspace-routes.test.ts +43 -0
- package/src/runtime/routes/workspace-routes.ts +28 -0
- package/src/runtime/services/conversation-serializer.ts +39 -7
- package/src/runtime/sync/resource-sync-events.ts +93 -1
- package/src/schedule/schedule-store.ts +27 -2
- package/src/schedule/scheduler.ts +9 -1
- package/src/security/__tests__/untrusted-content.test.ts +86 -0
- package/src/security/untrusted-content.ts +93 -8
- package/src/skills/catalog-files.ts +1 -1
- package/src/skills/catalog-install.ts +233 -116
- package/src/skills/clawhub.ts +70 -13
- package/src/skills/managed-store.ts +4 -119
- package/src/skills/skillssh-registry.ts +27 -48
- package/src/subagent/manager.ts +15 -7
- package/src/telemetry/types.ts +113 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +312 -5
- package/src/telemetry/usage-telemetry-reporter.ts +113 -7
- package/src/tools/apps/executors.ts +58 -7
- package/src/tools/ask-question/ask-question-tool.test.ts +509 -0
- package/src/tools/ask-question/ask-question-tool.ts +304 -0
- package/src/tools/browser/browser-execution.ts +15 -11
- package/src/tools/computer-use/definitions.ts +3 -3
- package/src/tools/credentials/vault.ts +1 -1
- package/src/tools/document/document-tool.ts +124 -1
- package/src/tools/filesystem/edit.ts +1 -1
- package/src/tools/filesystem/list.ts +1 -1
- package/src/tools/filesystem/read.ts +1 -1
- package/src/tools/filesystem/write.ts +5 -2
- package/src/tools/host-filesystem/transfer.ts +1 -1
- package/src/tools/host-terminal/host-shell.ts +1 -1
- package/src/tools/permission-checker.ts +1 -1
- package/src/tools/registry.ts +17 -7
- package/src/tools/schedule/create.ts +2 -2
- package/src/tools/schema-transforms.ts +7 -2
- package/src/tools/side-effects.ts +1 -0
- package/src/tools/skills/delete-managed.ts +4 -4
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/skills/scaffold-managed.ts +3 -2
- package/src/tools/subagent/notify-parent.ts +1 -1
- package/src/tools/system/request-permission.ts +2 -2
- package/src/tools/terminal/safe-env.ts +60 -1
- package/src/tools/tool-manifest.ts +2 -0
- package/src/tools/types.ts +72 -21
- package/src/tools/ui-surface/definitions.ts +6 -5
- package/src/tts/__tests__/provider-adapters.test.ts +76 -2
- package/src/tts/providers/elevenlabs-provider.ts +75 -1
- package/src/types/onboarding-context.ts +2 -0
- package/src/util/errors.ts +17 -0
- package/src/util/platform.ts +10 -0
- package/src/watcher/__tests__/engine.test.ts +22 -0
- package/src/watcher/engine.ts +6 -2
- package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +80 -15
- package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +35 -22
- package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +3 -1
- package/src/workspace/migrations/083-system-prompt-prefix-to-file.ts +191 -0
- package/src/workspace/migrations/084-remove-legacy-skills-index.ts +276 -0
- package/src/workspace/migrations/085-memory-v2-bm25-b-reembed-disabled-v2-pages.ts +137 -0
- package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +198 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/workspace/migrations/runner.ts +39 -9
- package/src/workspace/migrations/types.ts +4 -0
- package/examples/plugins/echo/bun.lock +0 -25
- package/src/__tests__/context-window-manager.test.ts +0 -2481
- package/src/context/__tests__/compact-prompt.test.ts +0 -63
- package/src/context/prompts/compact.md +0 -26
- package/src/prompts/__tests__/build-cli-reference-section.test.ts +0 -37
- /package/src/__tests__/{secret-routes-managed-proxy.test.ts → secret-routes-platform-proxy.test.ts} +0 -0
|
@@ -14,6 +14,12 @@ import {
|
|
|
14
14
|
|
|
15
15
|
let rawConfigFixture: Record<string, unknown> = {};
|
|
16
16
|
let savedRawConfig: Record<string, unknown> | null = null;
|
|
17
|
+
// Counters / spies so tests can assert that `commitConfigWrite` ran its
|
|
18
|
+
// post-write side effects. Each `replaceProfileRoute.handler` call that
|
|
19
|
+
// hits `commitConfigWrite` should bump these once.
|
|
20
|
+
let invalidateConfigCacheCalls = 0;
|
|
21
|
+
let initializeProvidersCalls = 0;
|
|
22
|
+
let clearEmbeddingBackendCacheCalls = 0;
|
|
17
23
|
|
|
18
24
|
mock.module("../../../config/loader.js", () => ({
|
|
19
25
|
loadRawConfig: () => structuredClone(rawConfigFixture),
|
|
@@ -26,6 +32,28 @@ mock.module("../../../config/loader.js", () => ({
|
|
|
26
32
|
) => {
|
|
27
33
|
Object.assign(target, overrides);
|
|
28
34
|
},
|
|
35
|
+
// `commitConfigWrite` (used by `handleReplaceInferenceProfile`) pulls
|
|
36
|
+
// in `getConfig` for the provider reinit's config arg and
|
|
37
|
+
// `invalidateConfigCache` so the next caller sees the fresh write.
|
|
38
|
+
// Stub both: getConfig returns whatever was last saved (or the fixture
|
|
39
|
+
// if nothing has been saved yet) and the cache-invalidation function
|
|
40
|
+
// is a counter so we can assert it fired.
|
|
41
|
+
getConfig: () => structuredClone(savedRawConfig ?? rawConfigFixture),
|
|
42
|
+
invalidateConfigCache: () => {
|
|
43
|
+
invalidateConfigCacheCalls += 1;
|
|
44
|
+
},
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
mock.module("../../../providers/registry.js", () => ({
|
|
48
|
+
initializeProviders: async () => {
|
|
49
|
+
initializeProvidersCalls += 1;
|
|
50
|
+
},
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
mock.module("../../../memory/embedding-backend.js", () => ({
|
|
54
|
+
clearEmbeddingBackendCache: () => {
|
|
55
|
+
clearEmbeddingBackendCacheCalls += 1;
|
|
56
|
+
},
|
|
29
57
|
}));
|
|
30
58
|
|
|
31
59
|
import type { ConversationCreateType } from "../../../memory/conversation-crud.js";
|
|
@@ -288,6 +316,9 @@ describe("GET /v1/messages/:id/llm-context — conversationTotalEstimatedCostUsd
|
|
|
288
316
|
describe("PUT /v1/config/llm/profiles/:name", () => {
|
|
289
317
|
beforeEach(() => {
|
|
290
318
|
savedRawConfig = null;
|
|
319
|
+
invalidateConfigCacheCalls = 0;
|
|
320
|
+
initializeProvidersCalls = 0;
|
|
321
|
+
clearEmbeddingBackendCacheCalls = 0;
|
|
291
322
|
rawConfigFixture = {
|
|
292
323
|
llm: {
|
|
293
324
|
profiles: {
|
|
@@ -313,8 +344,8 @@ describe("PUT /v1/config/llm/profiles/:name", () => {
|
|
|
313
344
|
};
|
|
314
345
|
});
|
|
315
346
|
|
|
316
|
-
test("owns contextWindow maxInputTokens while preserving non-UI profile leaves", () => {
|
|
317
|
-
const result = replaceProfileRoute.handler({
|
|
347
|
+
test("owns contextWindow maxInputTokens while preserving non-UI profile leaves", async () => {
|
|
348
|
+
const result = await replaceProfileRoute.handler({
|
|
318
349
|
pathParams: { name: "custom" },
|
|
319
350
|
body: {
|
|
320
351
|
provider: "openai",
|
|
@@ -343,8 +374,8 @@ describe("PUT /v1/config/llm/profiles/:name", () => {
|
|
|
343
374
|
expect(savedProfile.openrouter).toEqual({ only: ["anthropic"] });
|
|
344
375
|
});
|
|
345
376
|
|
|
346
|
-
test("writes only the replacement contextWindow maxInputTokens override", () => {
|
|
347
|
-
const result = replaceProfileRoute.handler({
|
|
377
|
+
test("writes only the replacement contextWindow maxInputTokens override", async () => {
|
|
378
|
+
const result = await replaceProfileRoute.handler({
|
|
348
379
|
pathParams: { name: "custom" },
|
|
349
380
|
body: {
|
|
350
381
|
provider: "openai",
|
|
@@ -375,8 +406,8 @@ describe("PUT /v1/config/llm/profiles/:name", () => {
|
|
|
375
406
|
expect(savedProfile.openrouter).toEqual({ only: ["anthropic"] });
|
|
376
407
|
});
|
|
377
408
|
|
|
378
|
-
test("writes provider_connection when present in body", () => {
|
|
379
|
-
const result = replaceProfileRoute.handler({
|
|
409
|
+
test("writes provider_connection when present in body", async () => {
|
|
410
|
+
const result = await replaceProfileRoute.handler({
|
|
380
411
|
pathParams: { name: "custom" },
|
|
381
412
|
body: {
|
|
382
413
|
provider: "openai",
|
|
@@ -396,7 +427,7 @@ describe("PUT /v1/config/llm/profiles/:name", () => {
|
|
|
396
427
|
expect(savedProfile.provider_connection).toBe("personal-openai");
|
|
397
428
|
});
|
|
398
429
|
|
|
399
|
-
test("clears provider_connection when omitted from body (UI-owned key)", () => {
|
|
430
|
+
test("clears provider_connection when omitted from body (UI-owned key)", async () => {
|
|
400
431
|
// Seed an existing binding so the test starts from a non-empty state.
|
|
401
432
|
(
|
|
402
433
|
rawConfigFixture.llm as {
|
|
@@ -404,7 +435,7 @@ describe("PUT /v1/config/llm/profiles/:name", () => {
|
|
|
404
435
|
}
|
|
405
436
|
).profiles.custom.provider_connection = "stale-openai";
|
|
406
437
|
|
|
407
|
-
const result = replaceProfileRoute.handler({
|
|
438
|
+
const result = await replaceProfileRoute.handler({
|
|
408
439
|
pathParams: { name: "custom" },
|
|
409
440
|
body: {
|
|
410
441
|
provider: "openai",
|
|
@@ -439,8 +470,8 @@ describe("PUT /v1/config/llm/profiles/:name", () => {
|
|
|
439
470
|
};
|
|
440
471
|
});
|
|
441
472
|
|
|
442
|
-
test("allows label edit on managed profile, preserving seed fields", () => {
|
|
443
|
-
const result = replaceProfileRoute.handler({
|
|
473
|
+
test("allows label edit on managed profile, preserving seed fields", async () => {
|
|
474
|
+
const result = await replaceProfileRoute.handler({
|
|
444
475
|
pathParams: { name: "balanced" },
|
|
445
476
|
body: { label: "My Balanced" },
|
|
446
477
|
});
|
|
@@ -459,8 +490,8 @@ describe("PUT /v1/config/llm/profiles/:name", () => {
|
|
|
459
490
|
expect(savedProfile.source).toBe("managed");
|
|
460
491
|
});
|
|
461
492
|
|
|
462
|
-
test("allows status edit on managed profile", () => {
|
|
463
|
-
const result = replaceProfileRoute.handler({
|
|
493
|
+
test("allows status edit on managed profile", async () => {
|
|
494
|
+
const result = await replaceProfileRoute.handler({
|
|
464
495
|
pathParams: { name: "balanced" },
|
|
465
496
|
body: { status: "disabled" },
|
|
466
497
|
});
|
|
@@ -476,8 +507,8 @@ describe("PUT /v1/config/llm/profiles/:name", () => {
|
|
|
476
507
|
expect(savedProfile.provider).toBe("anthropic");
|
|
477
508
|
});
|
|
478
509
|
|
|
479
|
-
test("allows label+status edit together", () => {
|
|
480
|
-
const result = replaceProfileRoute.handler({
|
|
510
|
+
test("allows label+status edit together", async () => {
|
|
511
|
+
const result = await replaceProfileRoute.handler({
|
|
481
512
|
pathParams: { name: "balanced" },
|
|
482
513
|
body: { label: "Renamed", status: "disabled" },
|
|
483
514
|
});
|
|
@@ -493,25 +524,81 @@ describe("PUT /v1/config/llm/profiles/:name", () => {
|
|
|
493
524
|
expect(savedProfile.status).toBe("disabled");
|
|
494
525
|
});
|
|
495
526
|
|
|
496
|
-
test("rejects provider edit on managed profile with disallowed-keys error", () => {
|
|
497
|
-
|
|
527
|
+
test("rejects provider edit on managed profile with disallowed-keys error", async () => {
|
|
528
|
+
// The handler is `async`, so synchronous BadRequest throws still
|
|
529
|
+
// surface as a rejected promise; assert via `.rejects.toThrow`.
|
|
530
|
+
await expect(
|
|
498
531
|
replaceProfileRoute.handler({
|
|
499
532
|
pathParams: { name: "balanced" },
|
|
500
533
|
body: { provider: "openai", model: "gpt-5" },
|
|
501
534
|
}),
|
|
502
|
-
).toThrow(
|
|
535
|
+
).rejects.toThrow(
|
|
536
|
+
/Cannot edit managed profile "balanced" fields \[provider, model\]/,
|
|
537
|
+
);
|
|
503
538
|
});
|
|
504
539
|
|
|
505
|
-
test("rejects mixed allowed+disallowed fields", () => {
|
|
540
|
+
test("rejects mixed allowed+disallowed fields", async () => {
|
|
506
541
|
// label is allowed but maxTokens is not — must reject without partially
|
|
507
542
|
// applying label, so saver should never be invoked.
|
|
508
|
-
expect(
|
|
543
|
+
await expect(
|
|
509
544
|
replaceProfileRoute.handler({
|
|
510
545
|
pathParams: { name: "balanced" },
|
|
511
546
|
body: { label: "Try", maxTokens: 999 },
|
|
512
547
|
}),
|
|
513
|
-
).toThrow(
|
|
548
|
+
).rejects.toThrow(
|
|
549
|
+
/Cannot edit managed profile "balanced" fields \[maxTokens\]/,
|
|
550
|
+
);
|
|
514
551
|
expect(savedRawConfig).toBeNull();
|
|
552
|
+
// Reject path skips commitConfigWrite entirely — no provider reinit
|
|
553
|
+
// or cache invalidation should fire on a guard rejection.
|
|
554
|
+
expect(initializeProvidersCalls).toBe(0);
|
|
555
|
+
expect(invalidateConfigCacheCalls).toBe(0);
|
|
556
|
+
expect(clearEmbeddingBackendCacheCalls).toBe(0);
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
describe("commitConfigWrite side effects", () => {
|
|
561
|
+
test("status flip on managed profile triggers provider reinit + cache invalidation", async () => {
|
|
562
|
+
// Seed a managed profile that the user will disable. commitConfigWrite
|
|
563
|
+
// must reinit the provider registry so the status change is reflected
|
|
564
|
+
// in the running daemon immediately, not at the next watcher tick.
|
|
565
|
+
(rawConfigFixture.llm as { profiles: Record<string, unknown> }).profiles[
|
|
566
|
+
"balanced"
|
|
567
|
+
] = {
|
|
568
|
+
source: "managed",
|
|
569
|
+
provider: "anthropic",
|
|
570
|
+
model: "claude-sonnet-4-6",
|
|
571
|
+
label: "Balanced",
|
|
572
|
+
status: "active",
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
const result = await replaceProfileRoute.handler({
|
|
576
|
+
pathParams: { name: "balanced" },
|
|
577
|
+
body: { status: "disabled" },
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
expect(result).toEqual({ ok: true });
|
|
581
|
+
expect(initializeProvidersCalls).toBe(1);
|
|
582
|
+
expect(invalidateConfigCacheCalls).toBe(1);
|
|
583
|
+
expect(clearEmbeddingBackendCacheCalls).toBe(1);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
test("custom profile provider swap triggers provider reinit + cache invalidation", async () => {
|
|
587
|
+
// Custom profile path: provider/model swap on a user-owned profile.
|
|
588
|
+
// Same side-effect contract — registry must reinit so the new
|
|
589
|
+
// provider is wired into the running daemon without restart.
|
|
590
|
+
const result = await replaceProfileRoute.handler({
|
|
591
|
+
pathParams: { name: "custom" },
|
|
592
|
+
body: {
|
|
593
|
+
provider: "openai",
|
|
594
|
+
model: "gpt-5.5",
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
expect(result).toEqual({ ok: true });
|
|
599
|
+
expect(initializeProvidersCalls).toBe(1);
|
|
600
|
+
expect(invalidateConfigCacheCalls).toBe(1);
|
|
601
|
+
expect(clearEmbeddingBackendCacheCalls).toBe(1);
|
|
515
602
|
});
|
|
516
603
|
});
|
|
517
604
|
});
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the `/v1/question-response` route in `question-routes.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - kind: "submit" — single-entry happy path (option + free_text).
|
|
6
|
+
* - kind: "submit" — multi-entry batch resolves with the full result.
|
|
7
|
+
* - kind: "close" — every entry reported as skipped, overall="closed".
|
|
8
|
+
* - Validation: missing questionId from the batch → 400.
|
|
9
|
+
* - Validation: unknown questionId → 400.
|
|
10
|
+
* - Validation: option submission with unknown optionId → 400.
|
|
11
|
+
* - Cross-talk safety: a registered "confirmation" requestId returns 404.
|
|
12
|
+
* - Legacy single-question shim: works against a one-element batch,
|
|
13
|
+
* 400s against a multi-element batch.
|
|
14
|
+
* - The pending interaction is removed after a successful resolve.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
18
|
+
|
|
19
|
+
import type { QuestionPromptResult } from "../../../permissions/question-prompter.js";
|
|
20
|
+
import * as pendingInteractions from "../../pending-interactions.js";
|
|
21
|
+
import { BadRequestError, NotFoundError } from "../errors.js";
|
|
22
|
+
import { ROUTES as QUESTION_ROUTES } from "../question-routes.js";
|
|
23
|
+
import type { RouteDefinition, RouteHandlerArgs } from "../types.js";
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Helpers
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
function findHandler(operationId: string): RouteDefinition["handler"] {
|
|
30
|
+
const route = QUESTION_ROUTES.find((r) => r.operationId === operationId);
|
|
31
|
+
if (!route) throw new Error(`Route ${operationId} not found`);
|
|
32
|
+
return route.handler;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const handler = findHandler("question_response");
|
|
36
|
+
|
|
37
|
+
async function call(args: RouteHandlerArgs): Promise<unknown> {
|
|
38
|
+
return await handler(args);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Register a pending "question" interaction with the metadata the route
|
|
43
|
+
* needs to validate batched submissions. Mirrors what
|
|
44
|
+
* QuestionPrompter.prompt() does internally.
|
|
45
|
+
*/
|
|
46
|
+
function registerQuestion(
|
|
47
|
+
requestId: string,
|
|
48
|
+
questions: Array<{ id: string; options: string[] }>,
|
|
49
|
+
rpcResolve: (value: unknown) => void = () => {},
|
|
50
|
+
): void {
|
|
51
|
+
const optionsById: Record<string, string[]> = {};
|
|
52
|
+
for (const q of questions) optionsById[q.id] = q.options;
|
|
53
|
+
pendingInteractions.register(requestId, {
|
|
54
|
+
conversationId: "conv-1",
|
|
55
|
+
kind: "question",
|
|
56
|
+
rpcResolve,
|
|
57
|
+
metadata: {
|
|
58
|
+
orderedIds: questions.map((q) => q.id),
|
|
59
|
+
optionsById,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
pendingInteractions.clear();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
pendingInteractions.clear();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Tests
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
describe("POST /v1/question-response", () => {
|
|
77
|
+
test("submit: resolves a one-question batch with an option entry", async () => {
|
|
78
|
+
const resolved: QuestionPromptResult[] = [];
|
|
79
|
+
registerQuestion(
|
|
80
|
+
"req-1",
|
|
81
|
+
[{ id: "q1", options: ["yes", "no"] }],
|
|
82
|
+
(v) => resolved.push(v as QuestionPromptResult),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const result = await call({
|
|
86
|
+
body: {
|
|
87
|
+
requestId: "req-1",
|
|
88
|
+
kind: "submit",
|
|
89
|
+
responses: [{ questionId: "q1", kind: "option", optionId: "yes" }],
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(result).toEqual({ success: true });
|
|
94
|
+
expect(resolved).toEqual([
|
|
95
|
+
{
|
|
96
|
+
entries: [{ questionId: "q1", decision: "option", optionId: "yes" }],
|
|
97
|
+
overall: "completed",
|
|
98
|
+
},
|
|
99
|
+
]);
|
|
100
|
+
expect(pendingInteractions.get("req-1")).toBeUndefined();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("submit: three-question batch with two options + one free-text", async () => {
|
|
104
|
+
const resolved: QuestionPromptResult[] = [];
|
|
105
|
+
registerQuestion(
|
|
106
|
+
"req-3",
|
|
107
|
+
[
|
|
108
|
+
{ id: "q1", options: ["alice_work", "alice_personal"] },
|
|
109
|
+
{ id: "q2", options: ["yes", "no"] },
|
|
110
|
+
{ id: "q3", options: ["noon", "1pm"] },
|
|
111
|
+
],
|
|
112
|
+
(v) => resolved.push(v as QuestionPromptResult),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const result = await call({
|
|
116
|
+
body: {
|
|
117
|
+
requestId: "req-3",
|
|
118
|
+
kind: "submit",
|
|
119
|
+
responses: [
|
|
120
|
+
{ questionId: "q1", kind: "option", optionId: "alice_work" },
|
|
121
|
+
{ questionId: "q3", kind: "free_text", text: "noon-ish" },
|
|
122
|
+
{ questionId: "q2", kind: "option", optionId: "yes" },
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(result).toEqual({ success: true });
|
|
128
|
+
expect(resolved[0]?.overall).toBe("completed");
|
|
129
|
+
// Entries are ordered to match the original questions array.
|
|
130
|
+
expect(resolved[0]?.entries).toEqual([
|
|
131
|
+
{ questionId: "q1", decision: "option", optionId: "alice_work" },
|
|
132
|
+
{ questionId: "q2", decision: "option", optionId: "yes" },
|
|
133
|
+
{ questionId: "q3", decision: "free_text", text: "noon-ish" },
|
|
134
|
+
]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("submit: all-skip resolves with completed + skipped entries", async () => {
|
|
138
|
+
const resolved: QuestionPromptResult[] = [];
|
|
139
|
+
registerQuestion(
|
|
140
|
+
"req-skip-all",
|
|
141
|
+
[
|
|
142
|
+
{ id: "q1", options: ["a", "b"] },
|
|
143
|
+
{ id: "q2", options: ["x", "y"] },
|
|
144
|
+
],
|
|
145
|
+
(v) => resolved.push(v as QuestionPromptResult),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
await call({
|
|
149
|
+
body: {
|
|
150
|
+
requestId: "req-skip-all",
|
|
151
|
+
kind: "submit",
|
|
152
|
+
responses: [
|
|
153
|
+
{ questionId: "q1", kind: "skip" },
|
|
154
|
+
{ questionId: "q2", kind: "skip" },
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(resolved[0]).toEqual({
|
|
160
|
+
entries: [
|
|
161
|
+
{ questionId: "q1", decision: "skipped" },
|
|
162
|
+
{ questionId: "q2", decision: "skipped" },
|
|
163
|
+
],
|
|
164
|
+
overall: "completed",
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("close: every entry reported as skipped with overall=closed", async () => {
|
|
169
|
+
const resolved: QuestionPromptResult[] = [];
|
|
170
|
+
registerQuestion(
|
|
171
|
+
"req-close",
|
|
172
|
+
[
|
|
173
|
+
{ id: "q1", options: ["a", "b"] },
|
|
174
|
+
{ id: "q2", options: ["x", "y"] },
|
|
175
|
+
],
|
|
176
|
+
(v) => resolved.push(v as QuestionPromptResult),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const result = await call({
|
|
180
|
+
body: { requestId: "req-close", kind: "close" },
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
expect(result).toEqual({ success: true });
|
|
184
|
+
expect(resolved[0]).toEqual({
|
|
185
|
+
entries: [
|
|
186
|
+
{ questionId: "q1", decision: "skipped" },
|
|
187
|
+
{ questionId: "q2", decision: "skipped" },
|
|
188
|
+
],
|
|
189
|
+
overall: "closed",
|
|
190
|
+
});
|
|
191
|
+
expect(pendingInteractions.get("req-close")).toBeUndefined();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("returns 404 when no pending interaction exists for the requestId", async () => {
|
|
195
|
+
let thrown: unknown;
|
|
196
|
+
try {
|
|
197
|
+
await call({
|
|
198
|
+
body: {
|
|
199
|
+
requestId: "missing",
|
|
200
|
+
kind: "submit",
|
|
201
|
+
responses: [{ questionId: "q1", kind: "option", optionId: "a" }],
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
} catch (err) {
|
|
205
|
+
thrown = err;
|
|
206
|
+
}
|
|
207
|
+
expect(thrown).toBeInstanceOf(NotFoundError);
|
|
208
|
+
expect((thrown as NotFoundError).statusCode).toBe(404);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("returns 400 when the request body fails schema validation", async () => {
|
|
212
|
+
registerQuestion("req-bad", [{ id: "q1", options: ["a", "b"] }]);
|
|
213
|
+
let thrown: unknown;
|
|
214
|
+
try {
|
|
215
|
+
// Missing `responses` for kind: "submit".
|
|
216
|
+
await call({ body: { requestId: "req-bad", kind: "submit" } });
|
|
217
|
+
} catch (err) {
|
|
218
|
+
thrown = err;
|
|
219
|
+
}
|
|
220
|
+
expect(thrown).toBeInstanceOf(BadRequestError);
|
|
221
|
+
expect((thrown as BadRequestError).statusCode).toBe(400);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("returns 400 when kind is unknown", async () => {
|
|
225
|
+
let thrown: unknown;
|
|
226
|
+
try {
|
|
227
|
+
await call({ body: { requestId: "req-1", kind: "bogus" } });
|
|
228
|
+
} catch (err) {
|
|
229
|
+
thrown = err;
|
|
230
|
+
}
|
|
231
|
+
expect(thrown).toBeInstanceOf(BadRequestError);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("returns 400 when body is missing entirely", async () => {
|
|
235
|
+
let thrown: unknown;
|
|
236
|
+
try {
|
|
237
|
+
await call({});
|
|
238
|
+
} catch (err) {
|
|
239
|
+
thrown = err;
|
|
240
|
+
}
|
|
241
|
+
expect(thrown).toBeInstanceOf(BadRequestError);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("validation: batch missing a questionId from the original set → 400", async () => {
|
|
245
|
+
const resolved: unknown[] = [];
|
|
246
|
+
registerQuestion(
|
|
247
|
+
"req-miss",
|
|
248
|
+
[
|
|
249
|
+
{ id: "q1", options: ["a", "b"] },
|
|
250
|
+
{ id: "q2", options: ["x", "y"] },
|
|
251
|
+
],
|
|
252
|
+
(v) => resolved.push(v),
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
let thrown: unknown;
|
|
256
|
+
try {
|
|
257
|
+
await call({
|
|
258
|
+
body: {
|
|
259
|
+
requestId: "req-miss",
|
|
260
|
+
kind: "submit",
|
|
261
|
+
responses: [{ questionId: "q1", kind: "option", optionId: "a" }],
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
} catch (err) {
|
|
265
|
+
thrown = err;
|
|
266
|
+
}
|
|
267
|
+
expect(thrown).toBeInstanceOf(BadRequestError);
|
|
268
|
+
// Pending interaction left in place so the user can retry.
|
|
269
|
+
expect(pendingInteractions.get("req-miss")).toBeDefined();
|
|
270
|
+
expect(resolved).toEqual([]);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("validation: unknown questionId → 400", async () => {
|
|
274
|
+
registerQuestion("req-uq", [{ id: "q1", options: ["a", "b"] }]);
|
|
275
|
+
|
|
276
|
+
let thrown: unknown;
|
|
277
|
+
try {
|
|
278
|
+
await call({
|
|
279
|
+
body: {
|
|
280
|
+
requestId: "req-uq",
|
|
281
|
+
kind: "submit",
|
|
282
|
+
responses: [
|
|
283
|
+
{ questionId: "q1", kind: "option", optionId: "a" },
|
|
284
|
+
{ questionId: "qX", kind: "skip" },
|
|
285
|
+
],
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
} catch (err) {
|
|
289
|
+
thrown = err;
|
|
290
|
+
}
|
|
291
|
+
expect(thrown).toBeInstanceOf(BadRequestError);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("validation: unknown optionId → 400", async () => {
|
|
295
|
+
registerQuestion("req-uo", [{ id: "q1", options: ["a", "b"] }]);
|
|
296
|
+
|
|
297
|
+
let thrown: unknown;
|
|
298
|
+
try {
|
|
299
|
+
await call({
|
|
300
|
+
body: {
|
|
301
|
+
requestId: "req-uo",
|
|
302
|
+
kind: "submit",
|
|
303
|
+
responses: [{ questionId: "q1", kind: "option", optionId: "nope" }],
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
} catch (err) {
|
|
307
|
+
thrown = err;
|
|
308
|
+
}
|
|
309
|
+
expect(thrown).toBeInstanceOf(BadRequestError);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test("cross-talk safe: confirmation requestId returns 404", async () => {
|
|
313
|
+
const resolved: unknown[] = [];
|
|
314
|
+
pendingInteractions.register("req-confirm", {
|
|
315
|
+
conversationId: "conv-1",
|
|
316
|
+
kind: "confirmation",
|
|
317
|
+
rpcResolve: (value) => resolved.push(value),
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
let thrown: unknown;
|
|
321
|
+
try {
|
|
322
|
+
await call({
|
|
323
|
+
body: {
|
|
324
|
+
requestId: "req-confirm",
|
|
325
|
+
kind: "submit",
|
|
326
|
+
responses: [{ questionId: "q1", kind: "option", optionId: "yes" }],
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
} catch (err) {
|
|
330
|
+
thrown = err;
|
|
331
|
+
}
|
|
332
|
+
expect(thrown).toBeInstanceOf(NotFoundError);
|
|
333
|
+
expect(resolved).toEqual([]);
|
|
334
|
+
expect(pendingInteractions.get("req-confirm")?.kind).toBe("confirmation");
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("legacy single-question shim: resolves against a one-element batch", async () => {
|
|
338
|
+
const resolved: QuestionPromptResult[] = [];
|
|
339
|
+
registerQuestion(
|
|
340
|
+
"req-legacy",
|
|
341
|
+
[{ id: "q1", options: ["yes", "no"] }],
|
|
342
|
+
(v) => resolved.push(v as QuestionPromptResult),
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
const result = await call({
|
|
346
|
+
body: { requestId: "req-legacy", kind: "option", optionId: "yes" },
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
expect(result).toEqual({ success: true });
|
|
350
|
+
expect(resolved[0]).toEqual({
|
|
351
|
+
entries: [{ questionId: "q1", decision: "option", optionId: "yes" }],
|
|
352
|
+
overall: "completed",
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("legacy single-question shim: free-text resolves against a one-element batch", async () => {
|
|
357
|
+
const resolved: QuestionPromptResult[] = [];
|
|
358
|
+
registerQuestion(
|
|
359
|
+
"req-legacy-ft",
|
|
360
|
+
[{ id: "q1", options: ["yes", "no"] }],
|
|
361
|
+
(v) => resolved.push(v as QuestionPromptResult),
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
await call({
|
|
365
|
+
body: { requestId: "req-legacy-ft", kind: "free_text", text: "maybe" },
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
expect(resolved[0]).toEqual({
|
|
369
|
+
entries: [{ questionId: "q1", decision: "free_text", text: "maybe" }],
|
|
370
|
+
overall: "completed",
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("legacy single-question shim: rejects against a multi-element batch", async () => {
|
|
375
|
+
registerQuestion("req-legacy-multi", [
|
|
376
|
+
{ id: "q1", options: ["a", "b"] },
|
|
377
|
+
{ id: "q2", options: ["x", "y"] },
|
|
378
|
+
]);
|
|
379
|
+
|
|
380
|
+
let thrown: unknown;
|
|
381
|
+
try {
|
|
382
|
+
await call({
|
|
383
|
+
body: { requestId: "req-legacy-multi", kind: "option", optionId: "a" },
|
|
384
|
+
});
|
|
385
|
+
} catch (err) {
|
|
386
|
+
thrown = err;
|
|
387
|
+
}
|
|
388
|
+
expect(thrown).toBeInstanceOf(BadRequestError);
|
|
389
|
+
expect((thrown as BadRequestError).message.toLowerCase()).toContain(
|
|
390
|
+
"multi-question",
|
|
391
|
+
);
|
|
392
|
+
// Pending interaction left in place.
|
|
393
|
+
expect(pendingInteractions.get("req-legacy-multi")).toBeDefined();
|
|
394
|
+
});
|
|
395
|
+
});
|
|
@@ -94,7 +94,7 @@ mock.module("../../../tts/synthesize-text.js", () => ({
|
|
|
94
94
|
// ---------------------------------------------------------------------------
|
|
95
95
|
|
|
96
96
|
import { RouteError } from "../errors.js";
|
|
97
|
-
import { ROUTES } from "../tts-routes.js";
|
|
97
|
+
import { formatTtsFailureMessage, ROUTES } from "../tts-routes.js";
|
|
98
98
|
import type { RouteHandlerArgs } from "../types.js";
|
|
99
99
|
|
|
100
100
|
// ---------------------------------------------------------------------------
|
|
@@ -286,6 +286,69 @@ describe("tts-routes", () => {
|
|
|
286
286
|
"BAD_GATEWAY",
|
|
287
287
|
);
|
|
288
288
|
});
|
|
289
|
+
|
|
290
|
+
test("propagates the underlying error message into the 502 response", async () => {
|
|
291
|
+
// Mimics what `synthesize-text.ts` re-throws when an ElevenLabs adapter
|
|
292
|
+
// raises ELEVENLABS_TTS_HTTP_ERROR with a parsed upstream message.
|
|
293
|
+
mockSynthesizeError = new MockTtsSynthesisError(
|
|
294
|
+
"TTS_SYNTHESIS_FAILED",
|
|
295
|
+
"TTS synthesis failed (provider: elevenlabs): Free users cannot use library voices via the API. Please upgrade your subscription to use this voice.",
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
const { handler } = getRoute("messages/:messageId/tts");
|
|
299
|
+
const err = await expectRouteError(
|
|
300
|
+
() => handler(makeMessageTtsArgs()),
|
|
301
|
+
502,
|
|
302
|
+
"BAD_GATEWAY",
|
|
303
|
+
);
|
|
304
|
+
expect(err.message).toContain("Free users cannot use library voices");
|
|
305
|
+
expect(err.message).toContain("Please upgrade your subscription");
|
|
306
|
+
// No double-prefix — message stays as the inner self-describing form.
|
|
307
|
+
expect(err.message.startsWith("TTS synthesis failed")).toBe(true);
|
|
308
|
+
expect(
|
|
309
|
+
err.message.startsWith("TTS synthesis failed: TTS synthesis failed"),
|
|
310
|
+
).toBe(false);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
// Tests — formatTtsFailureMessage
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
describe("formatTtsFailureMessage", () => {
|
|
319
|
+
test("returns the base message when given a non-Error value", () => {
|
|
320
|
+
expect(formatTtsFailureMessage(undefined)).toBe("TTS synthesis failed");
|
|
321
|
+
expect(formatTtsFailureMessage(null)).toBe("TTS synthesis failed");
|
|
322
|
+
expect(formatTtsFailureMessage("oops")).toBe("TTS synthesis failed");
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("returns the base message when the error has no message text", () => {
|
|
326
|
+
const err = new Error("");
|
|
327
|
+
expect(formatTtsFailureMessage(err)).toBe("TTS synthesis failed");
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test("prefixes raw provider error messages with the base", () => {
|
|
331
|
+
const err = new Error("Voice not found");
|
|
332
|
+
expect(formatTtsFailureMessage(err)).toBe(
|
|
333
|
+
"TTS synthesis failed: Voice not found",
|
|
334
|
+
);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("passes pre-prefixed messages through verbatim (no double-prefix)", () => {
|
|
338
|
+
const err = new Error(
|
|
339
|
+
"TTS synthesis failed (provider: elevenlabs): Free users cannot use library voices via the API.",
|
|
340
|
+
);
|
|
341
|
+
expect(formatTtsFailureMessage(err)).toBe(
|
|
342
|
+
"TTS synthesis failed (provider: elevenlabs): Free users cannot use library voices via the API.",
|
|
343
|
+
);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("trims surrounding whitespace from messages", () => {
|
|
347
|
+
const err = new Error(" Quota exceeded ");
|
|
348
|
+
expect(formatTtsFailureMessage(err)).toBe(
|
|
349
|
+
"TTS synthesis failed: Quota exceeded",
|
|
350
|
+
);
|
|
351
|
+
});
|
|
289
352
|
});
|
|
290
353
|
|
|
291
354
|
// ---------------------------------------------------------------------------
|