@vellumai/assistant 0.11.1 → 0.11.2-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/Dockerfile +1 -3
- package/README.md +1 -1
- package/eslint.config.mjs +28 -8
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/slack-text/src/index.test.ts +48 -0
- package/node_modules/@vellumai/slack-text/src/index.ts +25 -6
- package/openapi.yaml +292 -5
- package/package.json +1 -1
- package/src/__tests__/agent-loop-resume-interrupted.test.ts +223 -0
- package/src/__tests__/byok-default-profile-ensure.test.ts +43 -0
- package/src/__tests__/cli-logger-boundary-guard.test.ts +87 -0
- package/src/__tests__/compaction-events.test.ts +139 -2
- package/src/__tests__/config-loader-backfill.test.ts +35 -3
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +30 -0
- package/src/__tests__/conversation-agent-loop.test.ts +180 -7
- package/src/__tests__/conversation-error.test.ts +211 -4
- package/src/__tests__/conversation-options-turn-scoped-transport.test.ts +64 -0
- package/src/__tests__/conversation-queue.test.ts +126 -0
- package/src/__tests__/conversation-retry-route.test.ts +46 -7
- package/src/__tests__/conversation-slash.test.ts +4 -2
- package/src/__tests__/conversation-summarize-route.test.ts +7 -1
- package/src/__tests__/conversation-summarize-up-to.test.ts +71 -5
- package/src/__tests__/document-create-dedupe.test.ts +2 -1
- package/src/__tests__/document-find-replace.test.ts +2 -1
- package/src/__tests__/document-tool-security.test.ts +2 -1
- package/src/__tests__/document-update-default-surface.test.ts +2 -1
- package/src/__tests__/document-workspace-file.test.ts +467 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +290 -49
- package/src/__tests__/guardian-card-withdrawal.test.ts +91 -3
- package/src/__tests__/http-user-message-parity.test.ts +66 -0
- package/src/__tests__/list-messages-attachments.test.ts +87 -0
- package/src/__tests__/list-messages-provider-error.test.ts +143 -0
- package/src/__tests__/list-messages-system-card.test.ts +100 -0
- package/src/__tests__/llm-resolver.test.ts +19 -9
- package/src/__tests__/managed-profile-guard.test.ts +23 -0
- package/src/__tests__/notification-platform-adapter.test.ts +130 -2
- package/src/__tests__/notification-telegram-adapter.test.ts +6 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +45 -0
- package/src/__tests__/plugin-api-model-profiles.test.ts +10 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/provider-error-scenarios.test.ts +140 -0
- package/src/__tests__/provider-send-message-override-profile.test.ts +95 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +66 -3
- package/src/__tests__/scripted-turn-metadata-persistence.test.ts +209 -0
- package/src/__tests__/skills.test.ts +27 -0
- package/src/__tests__/slack-channels-routes.test.ts +0 -2
- package/src/__tests__/slack-share-routes.test.ts +0 -3
- package/src/__tests__/slack-users-routes.test.ts +0 -2
- package/src/__tests__/subagent-tools.test.ts +11 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +58 -0
- package/src/__tests__/tool-result-spool.test.ts +5 -4
- package/src/__tests__/turn-boundary-resolution.test.ts +53 -0
- package/src/__tests__/turn-events-store.test.ts +26 -0
- package/src/__tests__/ui-visual-surface.test.ts +695 -0
- package/src/__tests__/unified-turn-context-visible-app.test.ts +99 -0
- package/src/__tests__/visible-app-context.test.ts +189 -0
- package/src/__tests__/workspace-git-service.test.ts +173 -33
- package/src/__tests__/workspace-migration-137-repair-retired-fireworks-minimax-model-id.test.ts +157 -0
- package/src/__tests__/workspace-migration-138-backfill-home-feed-titles.test.ts +373 -0
- package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +137 -0
- package/src/agent/loop.ts +36 -7
- package/src/api/events/context-window-usage.ts +31 -0
- package/src/api/events/notification-intent.ts +8 -0
- package/src/api/events/ui-surface-pending.ts +35 -0
- package/src/api/index.ts +14 -0
- package/src/api/responses/conversation-message.ts +25 -4
- package/src/api/surfaces.ts +90 -1
- package/src/approvals/guardian-card-withdrawal.ts +66 -31
- package/src/approvals/guardian-decision-primitive.ts +4 -0
- package/src/calls/__tests__/call-setup-router.test.ts +156 -26
- package/src/calls/__tests__/voice-session-bridge.test.ts +201 -12
- package/src/calls/call-setup-router.ts +80 -36
- package/src/calls/voice-session-bridge.ts +173 -26
- package/src/cli/commands/inference.help.ts +3 -3
- package/src/cli/commands/notifications.help.ts +3 -2
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +42 -128
- package/src/cli/commands/platform/__tests__/credits.test.ts +9 -78
- package/src/cli/commands/platform/__tests__/helpers.ts +90 -0
- package/src/cli/commands/platform/__tests__/invoices.test.ts +238 -0
- package/src/cli/commands/platform/__tests__/plans.test.ts +9 -88
- package/src/cli/commands/platform/__tests__/status.test.ts +12 -87
- package/src/cli/commands/platform/__tests__/subscription.test.ts +9 -86
- package/src/cli/commands/platform/index.help.ts +92 -0
- package/src/cli/commands/platform/index.ts +7 -0
- package/src/cli/commands/platform/invoices.ts +132 -0
- package/src/cli/commands/usage.help.ts +1 -1
- package/src/cli/lib/list-installed-plugins.ts +2 -1
- package/src/config/__tests__/default-profile-catalog.test.ts +7 -10
- package/src/config/__tests__/deployment-context-defaults.test.ts +27 -5
- package/src/config/assistant-feature-flags.ts +7 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +3 -2
- package/src/config/bundled-skills/subagent/SKILL.md +3 -1
- package/src/config/bundled-skills/subagent/TOOLS.json +3 -3
- package/src/config/bundled-skills/visualize/SKILL.md +163 -0
- package/src/config/call-site-defaults.ts +3 -2
- package/src/config/default-profile-catalog.ts +75 -70
- package/src/config/default-profile-names.ts +13 -28
- package/src/config/env-registry.ts +1 -0
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/llm-resolver.ts +21 -5
- package/src/config/loader.ts +18 -11
- package/src/config/schemas/llm.ts +1 -9
- package/src/config/schemas/memory-retrospective.ts +9 -0
- package/src/config/schemas/monitoring.ts +28 -2
- package/src/config/schemas/workspace-git.ts +15 -0
- package/src/config/seed-inference-profiles.ts +19 -5
- package/src/context/post-turn-tool-result-truncation.ts +2 -2
- package/src/conversations/__tests__/message-consolidation.test.ts +54 -0
- package/src/conversations/message-consolidation.ts +17 -15
- package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +182 -0
- package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +181 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +90 -7
- package/src/daemon/conversation-agent-loop.ts +107 -22
- package/src/daemon/conversation-error.ts +169 -48
- package/src/daemon/conversation-messaging.ts +87 -4
- package/src/daemon/conversation-process.ts +57 -45
- package/src/daemon/conversation-runtime-assembly.ts +57 -0
- package/src/daemon/conversation-store.ts +36 -1
- package/src/daemon/conversation-surfaces.ts +68 -6
- package/src/daemon/conversation-turn-finalize.ts +76 -18
- package/src/daemon/conversation.ts +122 -27
- package/src/daemon/lifecycle.ts +9 -0
- package/src/daemon/message-types/conversations.ts +8 -0
- package/src/daemon/message-types/surfaces.ts +3 -0
- package/src/documents/document-store.ts +247 -10
- package/src/home/__tests__/feed-types.test.ts +43 -8
- package/src/home/__tests__/feed-writer.test.ts +59 -0
- package/src/home/feed-types.ts +34 -7
- package/src/home/feed-writer.ts +15 -6
- package/src/live-voice/__tests__/activity-label.test.ts +95 -0
- package/src/live-voice/__tests__/live-activity-reporter.test.ts +86 -5
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +310 -25
- package/src/live-voice/__tests__/live-voice-events.test.ts +4 -4
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +8 -4
- package/src/live-voice/__tests__/live-voice-vad.test.ts +9 -1
- package/src/live-voice/activity-label.ts +169 -0
- package/src/live-voice/live-activity-reporter.ts +35 -6
- package/src/live-voice/live-voice-session.ts +282 -41
- package/src/live-voice/protocol.ts +35 -0
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +84 -2
- package/src/messaging/providers/slack/adapter.ts +86 -19
- package/src/messaging/providers/slack/api.test.ts +85 -1
- package/src/messaging/providers/slack/api.ts +121 -288
- package/src/messaging/providers/slack/client.ts +20 -251
- package/src/messaging/providers/slack/send.test.ts +4 -9
- package/src/messaging/providers/slack/send.ts +1 -1
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/messaging/providers/slack/web-api-transport.test.ts +181 -0
- package/src/messaging/providers/slack/web-api-transport.ts +367 -0
- package/src/messaging/providers/slack/withdraw.ts +6 -14
- package/src/messaging/providers/telegram-bot/send.test.ts +31 -1
- package/src/messaging/providers/telegram-bot/send.ts +23 -2
- package/src/messaging/providers/telegram-bot/withdraw.test.ts +152 -0
- package/src/messaging/providers/telegram-bot/withdraw.ts +166 -0
- package/src/monitoring/__tests__/db-integrity-sample.test.ts +18 -4
- package/src/monitoring/__tests__/file-descriptors.test.ts +144 -0
- package/src/monitoring/file-descriptors.ts +262 -0
- package/src/monitoring/process-memory.ts +4 -11
- package/src/monitoring/resource-sampler.ts +5 -0
- package/src/monitoring/worker.ts +12 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +740 -0
- package/src/notifications/__tests__/broadcaster.test.ts +347 -7
- package/src/notifications/__tests__/copy-composer.test.ts +53 -3
- package/src/notifications/__tests__/decision-engine.test.ts +349 -29
- package/src/notifications/__tests__/deterministic-checks.test.ts +21 -0
- package/src/notifications/__tests__/edit-notification.test.ts +330 -0
- package/src/notifications/__tests__/guardian-delivery-recorder.test.ts +71 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +170 -12
- package/src/notifications/adapters/macos.ts +3 -0
- package/src/notifications/adapters/platform.ts +90 -14
- package/src/notifications/adapters/telegram.ts +6 -4
- package/src/notifications/assistant-reply-producer.ts +219 -0
- package/src/notifications/broadcaster.ts +523 -317
- package/src/notifications/copy-composer.ts +27 -6
- package/src/notifications/decision-engine.ts +154 -77
- package/src/notifications/deterministic-checks.ts +6 -7
- package/src/notifications/edit-notification.ts +8 -4
- package/src/notifications/emit-signal.ts +49 -16
- package/src/notifications/guardian-delivery-recorder.ts +8 -2
- package/src/notifications/home-feed-side-effect.ts +43 -6
- package/src/notifications/notification-utils.ts +39 -4
- package/src/notifications/signal.ts +10 -0
- package/src/notifications/types.ts +17 -0
- package/src/persistence/bookmark-crud.ts +18 -9
- package/src/persistence/conversation-attention-store.ts +26 -0
- package/src/persistence/conversation-crud.ts +79 -32
- package/src/persistence/conversation-queries.ts +30 -15
- package/src/persistence/conversation-title-service.ts +5 -170
- package/src/persistence/conversation-types.ts +192 -1
- package/src/persistence/db-init.ts +14 -1
- package/src/persistence/db-maintenance.ts +5 -4
- package/src/persistence/embeddings/__tests__/plugin-index-qdrant-init.test.ts +219 -0
- package/src/persistence/embeddings/__tests__/plugin-index.test.ts +22 -0
- package/src/persistence/embeddings/__tests__/worker-script-version.test.ts +76 -0
- package/src/persistence/embeddings/embedding-local.ts +2 -1
- package/src/persistence/embeddings/embedding-runtime-manager.ts +29 -5
- package/src/persistence/embeddings/plugin-index.ts +84 -12
- package/src/persistence/migrations/360-add-document-workspace-path.test.ts +110 -0
- package/src/persistence/migrations/360-add-document-workspace-path.ts +39 -0
- package/src/persistence/planner-statistics.ts +14 -0
- package/src/persistence/schema/documents.ts +26 -11
- package/src/persistence/steps.ts +2 -0
- package/src/platform/client.test.ts +172 -2
- package/src/platform/client.ts +178 -55
- package/src/plugin-api/conversation-turn.ts +13 -0
- package/src/plugin-api/model-profiles.test.ts +6 -2
- package/src/plugins/defaults/memory/__tests__/bookmark-crud.test.ts +34 -0
- package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +22 -0
- package/src/plugins/defaults/memory/__tests__/jobs-store-enqueue-gate.test.ts +11 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +199 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +98 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +105 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +24 -2
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +524 -0
- package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +1 -1
- package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +9 -4
- package/src/plugins/defaults/memory/host-utils.ts +5 -0
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +105 -1
- package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +64 -5
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +36 -1
- package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +12 -3
- package/src/plugins/defaults/memory/src/__tests__/memory-v2-simulate-route.test.ts +1 -0
- package/src/plugins/defaults/memory/substrate/__tests__/skill-store.test.ts +195 -1
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +1 -1
- package/src/plugins/defaults/memory/substrate/skill-store.ts +167 -29
- package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +131 -0
- package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +1 -0
- package/src/plugins/defaults/memory/v2/activation-log-store.ts +4 -0
- package/src/plugins/defaults/memory/v2/injection.ts +48 -3
- package/src/plugins/defaults/memory/v2/rerank-local.ts +6 -2
- package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -1
- package/src/plugins/defaults/platform-hosted/routes/reengage.ts +1 -1
- package/src/plugins/defaults/turn-context/injectors.ts +1 -0
- package/src/plugins/defaults/turn-context/unified-turn-context.ts +26 -0
- package/src/plugins/types.ts +17 -0
- package/src/prompts/templates/system-sections.ts +2 -2
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +43 -2
- package/src/providers/__tests__/vellum-mismatch-routing.test.ts +8 -0
- package/src/providers/call-site-routing.ts +77 -14
- package/src/providers/connection-resolution.ts +42 -2
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +127 -1
- package/src/providers/inference/adapter-factory.ts +70 -12
- package/src/providers/model-catalog.ts +0 -11
- package/src/providers/model-intents.ts +11 -0
- package/src/providers/registry.ts +8 -0
- package/src/providers/retry.ts +85 -17
- package/src/providers/types.ts +8 -1
- package/src/runtime/__tests__/agent-wake.test.ts +181 -0
- package/src/runtime/agent-wake.ts +132 -9
- package/src/runtime/channel-approval-types.ts +25 -0
- package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +2 -2
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +11 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +121 -9
- package/src/runtime/routes/__tests__/platform-invoice-routes.test.ts +392 -0
- package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +7 -0
- package/src/runtime/routes/__tests__/workspace-commit-routes.test.ts +63 -0
- package/src/runtime/routes/consolidation-routes.ts +1 -1
- package/src/runtime/routes/conversation-management-routes.ts +18 -6
- package/src/runtime/routes/conversation-query-routes.ts +14 -23
- package/src/runtime/routes/conversation-routes.ts +113 -8
- package/src/runtime/routes/credential-routes.ts +1 -5
- package/src/runtime/routes/documents-routes.ts +207 -12
- package/src/runtime/routes/identity-routes.ts +3 -93
- package/src/runtime/routes/inbound-stages/admission-policy.test.ts +31 -1
- package/src/runtime/routes/inbound-stages/admission-policy.ts +18 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +52 -3
- package/src/runtime/routes/notification-routes.ts +4 -1
- package/src/runtime/routes/platform-routes.ts +238 -3
- package/src/runtime/routes/playground/guard.ts +1 -2
- package/src/runtime/routes/slack-channel-routes.ts +2 -4
- package/src/runtime/routes/workspace-commit-routes.ts +49 -2
- package/src/runtime/routes/workspace-routes.ts +11 -12
- package/src/runtime/routes/workspace-utils.ts +49 -2
- package/src/runtime/services/conversation-serializer.ts +2 -4
- package/src/subagent/__tests__/consult-context-gating.test.ts +98 -0
- package/src/subagent/__tests__/consult-context-skills.test.ts +82 -0
- package/src/subagent/__tests__/consult-context.test.ts +84 -0
- package/src/subagent/__tests__/consult-prompt.test.ts +36 -0
- package/src/subagent/consult-context.ts +410 -0
- package/src/subagent/consult-prompt.ts +38 -5
- package/src/subagent/manager.ts +9 -4
- package/src/subagent/types.ts +8 -0
- package/src/telemetry/telemetry-event-sources.ts +7 -0
- package/src/telemetry/telemetry-wire-source.json +1 -1
- package/src/telemetry/telemetry-wire.generated.ts +1 -0
- package/src/telemetry/turn-events-store.ts +30 -1
- package/src/telemetry/types.ts +24 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
- package/src/tools/browser/pinned-tabs.ts +3 -1
- package/src/tools/subagent/spawn.ts +23 -1
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/tools/ui-surface/definitions.ts +39 -1
- package/src/tools/ui-surface/surface-shape-docs.ts +38 -4
- package/src/tools/ui-surface/visual-validation.ts +787 -0
- package/src/tools/workflows/run-workflow.test.ts +1 -0
- package/src/util/__tests__/short-title.test.ts +229 -0
- package/src/util/__tests__/worker-compute.test.ts +65 -0
- package/src/util/cgroup-cpu.ts +93 -0
- package/src/util/errors.ts +27 -0
- package/src/util/process-tree.ts +19 -0
- package/src/util/short-title.ts +189 -0
- package/src/util/worker-compute.ts +83 -0
- package/src/workspace/byok-default-profile-ensure.ts +25 -15
- package/src/workspace/git-service.ts +79 -30
- package/src/workspace/migrations/137-repair-retired-fireworks-minimax-model-id.ts +131 -0
- package/src/workspace/migrations/138-backfill-home-feed-titles.ts +179 -0
- package/src/workspace/migrations/139-clear-renamed-cost-profile-label.ts +92 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/plugins/defaults/memory/substrate/constants.ts +0 -8
|
@@ -170,6 +170,112 @@ describe("classifyConversationError", () => {
|
|
|
170
170
|
expect(result.userMessage).toContain("AI provider");
|
|
171
171
|
expect(result.errorCategory).toBe("rate_limit");
|
|
172
172
|
});
|
|
173
|
+
|
|
174
|
+
it("uses the ChatGPT OAuth route instead of the OpenAI registry default", () => {
|
|
175
|
+
providerRoutingSources.openai = "managed-proxy";
|
|
176
|
+
const err = new ProviderError(
|
|
177
|
+
"OpenAI API error (429): Too many requests",
|
|
178
|
+
"openai",
|
|
179
|
+
429,
|
|
180
|
+
{ reason: "rate_limited" },
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const result = classifyConversationError(err, {
|
|
184
|
+
...baseCtx,
|
|
185
|
+
connectionName: "chatgpt-subscription",
|
|
186
|
+
isManagedRoute: false,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
expect(result.code).toBe("PROVIDER_RATE_LIMIT");
|
|
190
|
+
expect(result.userMessage).toContain("AI provider");
|
|
191
|
+
expect(result.errorCategory).toBe("rate_limit");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("prefers the failed call's direct route over stale managed turn attribution", () => {
|
|
195
|
+
providerRoutingSources.openai = "managed-proxy";
|
|
196
|
+
const err = new ProviderError(
|
|
197
|
+
"OpenAI API error (429): Too many requests",
|
|
198
|
+
"openai",
|
|
199
|
+
429,
|
|
200
|
+
{ reason: "rate_limited" },
|
|
201
|
+
);
|
|
202
|
+
err.attachRouteAttribution({
|
|
203
|
+
connectionName: "chatgpt-subscription",
|
|
204
|
+
profileName: "chatgpt",
|
|
205
|
+
isManagedRoute: false,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const result = classifyConversationError(err, {
|
|
209
|
+
...baseCtx,
|
|
210
|
+
connectionName: "vellum",
|
|
211
|
+
profileName: "managed",
|
|
212
|
+
isManagedRoute: true,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(result.code).toBe("PROVIDER_RATE_LIMIT");
|
|
216
|
+
expect(result.userMessage).toContain("AI provider");
|
|
217
|
+
expect(result.errorCategory).toBe("rate_limit");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("prefers the failed call's managed fallback over stale direct turn attribution", () => {
|
|
221
|
+
const err = new ProviderError(
|
|
222
|
+
"Anthropic API error (429): Too many requests",
|
|
223
|
+
"anthropic",
|
|
224
|
+
429,
|
|
225
|
+
{ reason: "rate_limited" },
|
|
226
|
+
);
|
|
227
|
+
err.attachRouteAttribution({
|
|
228
|
+
profileName: "direct",
|
|
229
|
+
isManagedRoute: true,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const result = classifyConversationError(err, {
|
|
233
|
+
...baseCtx,
|
|
234
|
+
connectionName: "anthropic-key",
|
|
235
|
+
profileName: "direct",
|
|
236
|
+
isManagedRoute: false,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
expect(result.code).toBe("MANAGED_USAGE_LIMIT");
|
|
240
|
+
expect(result.userMessage).toContain("Vellum managed inference");
|
|
241
|
+
expect(result.errorCategory).toBe("managed_usage_limit");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("does not apply a managed LLM route to a plain rate-limit error", () => {
|
|
245
|
+
const result = classifyConversationError(
|
|
246
|
+
new Error("429 Too Many Requests from a tool API"),
|
|
247
|
+
{
|
|
248
|
+
...baseCtx,
|
|
249
|
+
isManagedRoute: true,
|
|
250
|
+
},
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
expect(result.code).toBe("PROVIDER_RATE_LIMIT");
|
|
254
|
+
expect(result.errorCategory).toBe("rate_limit");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("falls back to context for fields the failed call's route omits", () => {
|
|
258
|
+
const err = new ProviderError("Unauthorized", "anthropic", 401);
|
|
259
|
+
err.attachRouteAttribution({ profileName: "direct" });
|
|
260
|
+
|
|
261
|
+
const result = classifyConversationError(err, {
|
|
262
|
+
...baseCtx,
|
|
263
|
+
connectionName: "anthropic-key",
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
expect(result.connectionName).toBe("anthropic-key");
|
|
267
|
+
expect(result.profileName).toBe("direct");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("still recognizes a rewrapped Vellum quota body", () => {
|
|
271
|
+
const result = classifyConversationError(
|
|
272
|
+
new Error('429 {"code":"daily_quota_exceeded"}'),
|
|
273
|
+
baseCtx,
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
expect(result.code).toBe("MANAGED_USAGE_LIMIT");
|
|
277
|
+
expect(result.errorCategory).toBe("managed_usage_limit");
|
|
278
|
+
});
|
|
173
279
|
});
|
|
174
280
|
|
|
175
281
|
describe("provider overloaded errors", () => {
|
|
@@ -671,6 +777,9 @@ describe("classifyConversationError", () => {
|
|
|
671
777
|
expect(result.code).toBe("PROVIDER_INVALID_KEY");
|
|
672
778
|
expect(result.retryable).toBe(false);
|
|
673
779
|
expect(result.errorCategory).toBe("provider_invalid_key");
|
|
780
|
+
expect(result.userMessage).toBe(
|
|
781
|
+
"Your personal Anthropic API key was rejected by Anthropic. Update that key in Settings → Models & Services.",
|
|
782
|
+
);
|
|
674
783
|
});
|
|
675
784
|
|
|
676
785
|
it("classifies managed-proxy auth failures as managed credential refresh failures", () => {
|
|
@@ -685,12 +794,30 @@ describe("classifyConversationError", () => {
|
|
|
685
794
|
|
|
686
795
|
expect(result.code).toBe("MANAGED_KEY_INVALID");
|
|
687
796
|
expect(result.userMessage).toBe(
|
|
688
|
-
"
|
|
797
|
+
"Vellum's managed inference credential was rejected. This isn't a personal provider API key — Vellum provisions this one, so there's nothing to update in Settings.",
|
|
689
798
|
);
|
|
690
799
|
expect(result.retryable).toBe(false);
|
|
691
800
|
expect(result.errorCategory).toBe("managed_key_invalid");
|
|
692
801
|
});
|
|
693
802
|
|
|
803
|
+
it("keeps credential-shaped 4xx failures on the request's managed route", () => {
|
|
804
|
+
providerRoutingSources.anthropic = "user-key";
|
|
805
|
+
const err = new ProviderError(
|
|
806
|
+
"Authentication error: invalid API key",
|
|
807
|
+
"anthropic",
|
|
808
|
+
400,
|
|
809
|
+
);
|
|
810
|
+
err.attachRouteAttribution({ credentialSource: "vellum-managed" });
|
|
811
|
+
|
|
812
|
+
const result = classifyConversationError(err, baseCtx);
|
|
813
|
+
|
|
814
|
+
expect(result.code).toBe("MANAGED_KEY_INVALID");
|
|
815
|
+
expect(result.errorCategory).toBe("managed_key_invalid");
|
|
816
|
+
expect(result.userMessage).toBe(
|
|
817
|
+
"Vellum's managed inference credential was rejected. This isn't a personal provider API key — Vellum provisions this one, so there's nothing to update in Settings.",
|
|
818
|
+
);
|
|
819
|
+
});
|
|
820
|
+
|
|
694
821
|
it("classifies ProviderError 401 with 'invalid x-api-key' message as PROVIDER_INVALID_KEY", () => {
|
|
695
822
|
// Regex-match branch — Anthropic's standard 401 wording.
|
|
696
823
|
const err = new ProviderError(
|
|
@@ -708,6 +835,8 @@ describe("classifyConversationError", () => {
|
|
|
708
835
|
const result = classifyConversationError(err, baseCtx);
|
|
709
836
|
expect(result.code).toBe("PROVIDER_INVALID_KEY");
|
|
710
837
|
expect(result.errorCategory).toBe("provider_invalid_key");
|
|
838
|
+
expect(result.userMessage).toContain("personal OpenAI API key");
|
|
839
|
+
expect(result.userMessage).toContain("rejected by OpenAI");
|
|
711
840
|
});
|
|
712
841
|
|
|
713
842
|
it("includes connection/profile attribution in PROVIDER_INVALID_KEY when provided", () => {
|
|
@@ -720,7 +849,84 @@ describe("classifyConversationError", () => {
|
|
|
720
849
|
expect(result.code).toBe("PROVIDER_INVALID_KEY");
|
|
721
850
|
expect(result.connectionName).toBe("my-anthropic");
|
|
722
851
|
expect(result.profileName).toBe("personal");
|
|
723
|
-
expect(result.userMessage).
|
|
852
|
+
expect(result.userMessage).toBe(
|
|
853
|
+
'Your personal Anthropic API key for profile "personal" (connection "my-anthropic") was rejected by Anthropic. Update that key in Settings → Models & Services.',
|
|
854
|
+
);
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
it("uses the request's BYOK attribution when the same provider also has a managed route", () => {
|
|
858
|
+
providerRoutingSources.anthropic = "managed-proxy";
|
|
859
|
+
const err = new ProviderError("Unauthorized", "anthropic", 401);
|
|
860
|
+
err.attachRouteAttribution({
|
|
861
|
+
credentialSource: "byok",
|
|
862
|
+
connectionName: "anthropic-personal",
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
const result = classifyConversationError(err, baseCtx);
|
|
866
|
+
|
|
867
|
+
expect(result.code).toBe("PROVIDER_INVALID_KEY");
|
|
868
|
+
expect(result.connectionName).toBe("anthropic-personal");
|
|
869
|
+
expect(result.userMessage).toBe(
|
|
870
|
+
'Your personal Anthropic API key for connection "anthropic-personal" was rejected by Anthropic. Update that key in Settings → Models & Services.',
|
|
871
|
+
);
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
it("uses the request's managed attribution when the same provider also has a BYOK route", () => {
|
|
875
|
+
providerRoutingSources.anthropic = "user-key";
|
|
876
|
+
const err = new ProviderError("Unauthorized", "anthropic", 401);
|
|
877
|
+
err.attachRouteAttribution({
|
|
878
|
+
credentialSource: "vellum-managed",
|
|
879
|
+
connectionName: "vellum",
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
const result = classifyConversationError(err, baseCtx);
|
|
883
|
+
|
|
884
|
+
expect(result.code).toBe("MANAGED_KEY_INVALID");
|
|
885
|
+
expect(result.userMessage).toBe(
|
|
886
|
+
"Vellum's managed inference credential was rejected. This isn't a personal provider API key — Vellum provisions this one, so there's nothing to update in Settings.",
|
|
887
|
+
);
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
it("uses subscription-specific recovery when the same provider also has a managed route", () => {
|
|
891
|
+
providerRoutingSources.openai = "managed-proxy";
|
|
892
|
+
const err = new ProviderError("Unauthorized", "openai", 401, {
|
|
893
|
+
reason: "invalid_credentials",
|
|
894
|
+
});
|
|
895
|
+
err.attachRouteAttribution({
|
|
896
|
+
credentialSource: "oauth-subscription",
|
|
897
|
+
connectionName: "chatgpt-subscription",
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
const result = classifyConversationError(err, baseCtx);
|
|
901
|
+
|
|
902
|
+
expect(result.code).toBe("PROVIDER_API");
|
|
903
|
+
expect(result.errorCategory).toBe("provider_subscription_auth");
|
|
904
|
+
expect(result.retryable).toBe(false);
|
|
905
|
+
expect(result.connectionName).toBe("chatgpt-subscription");
|
|
906
|
+
expect(result.userMessage).toBe(
|
|
907
|
+
'Your OpenAI subscription login for connection "chatgpt-subscription" was rejected by OpenAI. Reconnect that account in Settings → Models & Services.',
|
|
908
|
+
);
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
it("explains when a no-auth endpoint rejects an unauthenticated request", () => {
|
|
912
|
+
providerRoutingSources["openai-compatible"] = "managed-proxy";
|
|
913
|
+
const err = new ProviderError("Unauthorized", "openai-compatible", 401, {
|
|
914
|
+
reason: "invalid_credentials",
|
|
915
|
+
});
|
|
916
|
+
err.attachRouteAttribution({
|
|
917
|
+
credentialSource: "no-auth",
|
|
918
|
+
connectionName: "local-model-server",
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
const result = classifyConversationError(err, baseCtx);
|
|
922
|
+
|
|
923
|
+
expect(result.code).toBe("PROVIDER_API");
|
|
924
|
+
expect(result.errorCategory).toBe("provider_endpoint_auth_required");
|
|
925
|
+
expect(result.retryable).toBe(false);
|
|
926
|
+
expect(result.connectionName).toBe("local-model-server");
|
|
927
|
+
expect(result.userMessage).toBe(
|
|
928
|
+
'The OpenAI-compatible endpoint for connection "local-model-server" requires authentication, but that connection is configured without credentials. Configure authentication for that endpoint in Settings → Models & Services.',
|
|
929
|
+
);
|
|
724
930
|
});
|
|
725
931
|
|
|
726
932
|
it("classifies direct ProviderError with 402 as provider_billing (non-retryable)", () => {
|
|
@@ -788,8 +994,9 @@ describe("classifyConversationError", () => {
|
|
|
788
994
|
expect(result.code).toBe("PROVIDER_BILLING");
|
|
789
995
|
expect(result.errorCategory).toBe("credits_exhausted");
|
|
790
996
|
expect(result.retryable).toBe(false);
|
|
791
|
-
expect(result.userMessage).
|
|
792
|
-
|
|
997
|
+
expect(result.userMessage).toBe(
|
|
998
|
+
"You're out of credits. Add credits in Settings → Billing to continue.",
|
|
999
|
+
);
|
|
793
1000
|
});
|
|
794
1001
|
|
|
795
1002
|
it("classifies direct Anthropic, OpenAI, and OpenRouter 402 responses as provider_billing", () => {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `withoutTurnScopedTransport`, the guard that keeps per-message view
|
|
3
|
+
* state out of the durable `conversationOptions` map.
|
|
4
|
+
*
|
|
5
|
+
* `getOrCreateConversation` re-applies the stored transport whenever it rebuilds
|
|
6
|
+
* an evicted or stale conversation. Anything describing what the client had on
|
|
7
|
+
* screen for one message must be stripped before it is stored, or a rebuild
|
|
8
|
+
* hours later (a scheduled wake, a background follow-up) would resurrect an app
|
|
9
|
+
* the user has since closed and tell the model it is on screen.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, expect, test } from "bun:test";
|
|
13
|
+
|
|
14
|
+
import { withoutTurnScopedTransport } from "../daemon/conversation-store.js";
|
|
15
|
+
import type { ConversationCreateOptions } from "../daemon/handlers/shared.js";
|
|
16
|
+
|
|
17
|
+
describe("withoutTurnScopedTransport", () => {
|
|
18
|
+
test("drops the visible app while keeping durable transport fields", () => {
|
|
19
|
+
const options: ConversationCreateOptions = {
|
|
20
|
+
transport: {
|
|
21
|
+
channelId: "vellum",
|
|
22
|
+
interfaceId: "web",
|
|
23
|
+
clientOs: "macos",
|
|
24
|
+
clientTimezone: "America/Denver",
|
|
25
|
+
visibleAppId: "app-on-screen",
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const stored = withoutTurnScopedTransport(options);
|
|
30
|
+
|
|
31
|
+
expect(stored.transport?.visibleAppId).toBeUndefined();
|
|
32
|
+
expect(stored.transport?.clientOs).toBe("macos");
|
|
33
|
+
expect(stored.transport?.clientTimezone).toBe("America/Denver");
|
|
34
|
+
expect(stored.transport?.interfaceId).toBe("web");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("does not mutate the caller's options, which still drive this turn", () => {
|
|
38
|
+
const options: ConversationCreateOptions = {
|
|
39
|
+
transport: {
|
|
40
|
+
channelId: "vellum",
|
|
41
|
+
interfaceId: "web",
|
|
42
|
+
visibleAppId: "app-on-screen",
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
withoutTurnScopedTransport(options);
|
|
47
|
+
|
|
48
|
+
expect(options.transport?.visibleAppId).toBe("app-on-screen");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("passes options through untouched when there is no app in view", () => {
|
|
52
|
+
const options: ConversationCreateOptions = {
|
|
53
|
+
transport: { channelId: "vellum", interfaceId: "web", clientOs: "ios" },
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
expect(withoutTurnScopedTransport(options)).toBe(options);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("passes options through untouched when there is no transport", () => {
|
|
60
|
+
const options: ConversationCreateOptions = {};
|
|
61
|
+
|
|
62
|
+
expect(withoutTurnScopedTransport(options)).toBe(options);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -138,6 +138,12 @@ mock.module("../telemetry/turn-outcome.js", () => ({
|
|
|
138
138
|
stampTurnOutcome: mockStampTurnOutcome,
|
|
139
139
|
}));
|
|
140
140
|
|
|
141
|
+
const emitAssistantReplyNotificationMock = mock(async () => {});
|
|
142
|
+
|
|
143
|
+
mock.module("../notifications/assistant-reply-producer.js", () => ({
|
|
144
|
+
emitAssistantReplyNotification: emitAssistantReplyNotificationMock,
|
|
145
|
+
}));
|
|
146
|
+
|
|
141
147
|
let linkAttachmentShouldThrow = false;
|
|
142
148
|
let mockAttachmentIdCounter = 0;
|
|
143
149
|
|
|
@@ -669,6 +675,52 @@ describe("Conversation message queue", () => {
|
|
|
669
675
|
expect(pendingRuns.length).toBe(3);
|
|
670
676
|
});
|
|
671
677
|
|
|
678
|
+
test("[experimental] queued passthrough siblings viewing different apps do NOT batch", async () => {
|
|
679
|
+
// A batched turn applies only the head's `visibleAppId`, which drives the
|
|
680
|
+
// `visible_app:` context line. Coalescing messages sent while different
|
|
681
|
+
// apps were on screen would point "the app" at the head's app for both.
|
|
682
|
+
const conversation = makeConversation();
|
|
683
|
+
await conversation.loadFromDb();
|
|
684
|
+
|
|
685
|
+
const p1 = conversation.processMessage({
|
|
686
|
+
content: "msg-1",
|
|
687
|
+
attachments: [],
|
|
688
|
+
onEvent: () => {},
|
|
689
|
+
requestId: "req-1",
|
|
690
|
+
});
|
|
691
|
+
await waitForPendingRun(1);
|
|
692
|
+
|
|
693
|
+
conversation.enqueueMessage({
|
|
694
|
+
content: "msg-2",
|
|
695
|
+
onEvent: () => {},
|
|
696
|
+
requestId: "req-2",
|
|
697
|
+
transport: {
|
|
698
|
+
channelId: "vellum",
|
|
699
|
+
interfaceId: "web",
|
|
700
|
+
visibleAppId: "app-a",
|
|
701
|
+
},
|
|
702
|
+
});
|
|
703
|
+
conversation.enqueueMessage({
|
|
704
|
+
content: "msg-3",
|
|
705
|
+
onEvent: () => {},
|
|
706
|
+
requestId: "req-3",
|
|
707
|
+
transport: {
|
|
708
|
+
channelId: "vellum",
|
|
709
|
+
interfaceId: "web",
|
|
710
|
+
visibleAppId: "app-b",
|
|
711
|
+
},
|
|
712
|
+
});
|
|
713
|
+
expect(conversation.getQueueDepth()).toBe(2);
|
|
714
|
+
|
|
715
|
+
await resolveRun(0);
|
|
716
|
+
await p1;
|
|
717
|
+
await waitForPendingRun(2);
|
|
718
|
+
await resolveRun(1);
|
|
719
|
+
await waitForPendingRun(3);
|
|
720
|
+
|
|
721
|
+
expect(pendingRuns.length).toBe(3);
|
|
722
|
+
});
|
|
723
|
+
|
|
672
724
|
test("message_queued and message_dequeued events are emitted", async () => {
|
|
673
725
|
const conversation = makeConversation();
|
|
674
726
|
await conversation.loadFromDb();
|
|
@@ -1793,6 +1845,80 @@ describe("Batched drain correctness fixes", () => {
|
|
|
1793
1845
|
await new Promise((r) => setTimeout(r, 10));
|
|
1794
1846
|
});
|
|
1795
1847
|
|
|
1848
|
+
// The batch's shared reply answers the person's prompt; the trailing row
|
|
1849
|
+
// only rode along behind it. A channel send gets its reply delivered back to
|
|
1850
|
+
// Slack and a hidden marker is nobody's prompt, so pointing the producer at
|
|
1851
|
+
// either would suppress a push the user is waiting on.
|
|
1852
|
+
// expectEcho pins the deliberate asymmetry between the two gates: the
|
|
1853
|
+
// echo broadcast uses the narrower echo-suppression predicate, so a
|
|
1854
|
+
// channel send is push-ineligible yet must still echo to passive devices.
|
|
1855
|
+
const TRAILING_INELIGIBLE_ROW_CASES: Array<{
|
|
1856
|
+
name: string;
|
|
1857
|
+
metadata: Record<string, unknown>;
|
|
1858
|
+
expectEcho: boolean;
|
|
1859
|
+
}> = [
|
|
1860
|
+
{ name: "hidden marker", metadata: { hidden: true }, expectEcho: false },
|
|
1861
|
+
{
|
|
1862
|
+
name: "channel send",
|
|
1863
|
+
metadata: { userMessageChannel: "slack" },
|
|
1864
|
+
expectEcho: true,
|
|
1865
|
+
},
|
|
1866
|
+
];
|
|
1867
|
+
|
|
1868
|
+
for (const { name, metadata, expectEcho } of TRAILING_INELIGIBLE_ROW_CASES) {
|
|
1869
|
+
test(`drainBatch notifies about the genuine prompt, not a trailing ${name}`, async () => {
|
|
1870
|
+
emitAssistantReplyNotificationMock.mockClear();
|
|
1871
|
+
const conversation = makeConversation();
|
|
1872
|
+
await conversation.loadFromDb();
|
|
1873
|
+
|
|
1874
|
+
const p1 = conversation.processMessage({
|
|
1875
|
+
content: "msg-1",
|
|
1876
|
+
attachments: [],
|
|
1877
|
+
requestId: "req-1",
|
|
1878
|
+
});
|
|
1879
|
+
await waitForPendingRun(1);
|
|
1880
|
+
|
|
1881
|
+
conversation.enqueueMessage({
|
|
1882
|
+
content: "batch-prompt-genuine",
|
|
1883
|
+
requestId: "req-prompt",
|
|
1884
|
+
});
|
|
1885
|
+
const trailingEvents: AssistantEvent[] = [];
|
|
1886
|
+
conversation.enqueueMessage({
|
|
1887
|
+
content: "batch-prompt-trailing",
|
|
1888
|
+
requestId: "req-trailing",
|
|
1889
|
+
metadata,
|
|
1890
|
+
onEvent: (e) => {
|
|
1891
|
+
trailingEvents.push(e);
|
|
1892
|
+
},
|
|
1893
|
+
});
|
|
1894
|
+
|
|
1895
|
+
await resolveRun(0);
|
|
1896
|
+
await p1;
|
|
1897
|
+
await waitForPendingRun(2);
|
|
1898
|
+
await resolveRun(1);
|
|
1899
|
+
// Two turns finish here: msg-1's own run, then the batch's shared run.
|
|
1900
|
+
await waitForCondition(
|
|
1901
|
+
() => emitAssistantReplyNotificationMock.mock.calls.length >= 2,
|
|
1902
|
+
);
|
|
1903
|
+
|
|
1904
|
+
const genuineRow = capturedAddMessages.find((m) =>
|
|
1905
|
+
m.content.includes("batch-prompt-genuine"),
|
|
1906
|
+
);
|
|
1907
|
+
expect(genuineRow?.id).toBeDefined();
|
|
1908
|
+
|
|
1909
|
+
const notifyCalls = emitAssistantReplyNotificationMock.mock
|
|
1910
|
+
.calls as unknown as Array<[{ userMessageId: string | undefined }]>;
|
|
1911
|
+
expect(notifyCalls.at(-1)?.[0].userMessageId).toBe(genuineRow!.id);
|
|
1912
|
+
|
|
1913
|
+
const trailingEchoes = trailingEvents.filter(
|
|
1914
|
+
(e) => e.type === "user_message_echo",
|
|
1915
|
+
);
|
|
1916
|
+
expect(trailingEchoes.length).toBe(expectEcho ? 1 : 0);
|
|
1917
|
+
|
|
1918
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1796
1922
|
// Defensive recovery path: buildPassthroughBatch is designed to make
|
|
1797
1923
|
// the invariant throw unreachable in practice, so neither the head
|
|
1798
1924
|
// branch (re-dispatch batch.slice(1) to drainBatch/drainSingleMessage/
|
|
@@ -17,6 +17,15 @@ import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
|
17
17
|
|
|
18
18
|
import { z } from "zod";
|
|
19
19
|
|
|
20
|
+
// The route's hidden-prompt branch is exactly what these pure predicates
|
|
21
|
+
// decide, so the crud mock below hands back the real ones rather than a
|
|
22
|
+
// restatement that can drift. Imported from the `conversation-types` leaf so
|
|
23
|
+
// naming them here pulls in no DB graph.
|
|
24
|
+
import {
|
|
25
|
+
isBackgroundEventMetadata,
|
|
26
|
+
isEchoSuppressedUserMessage,
|
|
27
|
+
} from "../persistence/conversation-types.js";
|
|
28
|
+
|
|
20
29
|
mock.module("../config/env.js", () => ({
|
|
21
30
|
isHttpAuthDisabled: () => true,
|
|
22
31
|
hasUngatedHttpAuthDisabled: () => false,
|
|
@@ -24,13 +33,6 @@ mock.module("../config/env.js", () => ({
|
|
|
24
33
|
|
|
25
34
|
mock.module("../daemon/conversation-process.js", () => ({
|
|
26
35
|
formatSummarizeUpToResult: () => "",
|
|
27
|
-
isEchoSuppressedUserMessage: (
|
|
28
|
-
metadata: Record<string, unknown> | undefined,
|
|
29
|
-
) =>
|
|
30
|
-
metadata?.hidden === true ||
|
|
31
|
-
typeof metadata?.backgroundEventSource === "string",
|
|
32
|
-
isBackgroundEventMetadata: (metadata: Record<string, unknown> | undefined) =>
|
|
33
|
-
typeof metadata?.backgroundEventSource === "string",
|
|
34
36
|
}));
|
|
35
37
|
|
|
36
38
|
mock.module("../daemon/handlers/conversations.js", () => ({
|
|
@@ -77,6 +79,8 @@ mock.module("../persistence/conversation-crud.js", () => ({
|
|
|
77
79
|
extractImageSourcePaths: () => undefined,
|
|
78
80
|
forkConversation: () => ({ id: "forked" }),
|
|
79
81
|
getConversation: getConversationMock,
|
|
82
|
+
isBackgroundEventMetadata,
|
|
83
|
+
isEchoSuppressedUserMessage,
|
|
80
84
|
provenanceFromTrustContext: () => ({ provenanceTrustClass: "unknown" }),
|
|
81
85
|
setConversationSurfaced: () => null,
|
|
82
86
|
unarchiveConversation: () => true,
|
|
@@ -316,6 +320,41 @@ describe("POST /v1/conversations/:id/retry", () => {
|
|
|
316
320
|
expect(typeof (options as { onEvent?: unknown }).onEvent).toBe("function");
|
|
317
321
|
});
|
|
318
322
|
|
|
323
|
+
// The re-run carries none of the anchor's original delivery orchestration,
|
|
324
|
+
// so a channel or voice anchor's regenerated reply reaches SSE subscribers
|
|
325
|
+
// alone and the reply push must not read the anchor's origin markers.
|
|
326
|
+
const OFF_APP_ANCHOR_CASES: Array<{ name: string; metadata: unknown }> = [
|
|
327
|
+
{ name: "Slack", metadata: { userMessageChannel: "slack" } },
|
|
328
|
+
{
|
|
329
|
+
name: "voice-session",
|
|
330
|
+
metadata: { voiceSessionTurn: true, userMessageChannel: "phone" },
|
|
331
|
+
},
|
|
332
|
+
{ name: "plain in-app", metadata: { userMessageChannel: "vellum" } },
|
|
333
|
+
];
|
|
334
|
+
|
|
335
|
+
for (const { name, metadata } of OFF_APP_ANCHOR_CASES) {
|
|
336
|
+
test(`marks the re-run of a ${name} anchor as app-only delivery`, async () => {
|
|
337
|
+
discardResult = {
|
|
338
|
+
anchor: anchorRow({ metadata: JSON.stringify(metadata) }),
|
|
339
|
+
deletedMessageIds: ["assistant-msg-1"],
|
|
340
|
+
};
|
|
341
|
+
const ctx = makeConversation();
|
|
342
|
+
activeConversation = ctx.conversation;
|
|
343
|
+
|
|
344
|
+
const res = await callHandler(
|
|
345
|
+
retryHandler,
|
|
346
|
+
makeRequest(),
|
|
347
|
+
{ id: "conv-retry-test" },
|
|
348
|
+
202,
|
|
349
|
+
);
|
|
350
|
+
expect(res.status).toBe(202);
|
|
351
|
+
await settle();
|
|
352
|
+
|
|
353
|
+
const [, , options] = ctx.runAgentLoop.mock.calls[0];
|
|
354
|
+
expect(options).toMatchObject({ replyDeliveredInAppOnly: true });
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
319
358
|
test("legacy background-event anchor (no recorded mode) re-runs hidden AND non-interactive", async () => {
|
|
320
359
|
// Anchors persisted before `backgroundEventInteractive` existed carry only
|
|
321
360
|
// `backgroundEventSource`. Fall back to non-interactive so a retried
|
|
@@ -51,7 +51,8 @@ describe("/model list", () => {
|
|
|
51
51
|
const message = ((await resolveSlash("/model")) as { message: string })
|
|
52
52
|
.message;
|
|
53
53
|
expect(message).toContain("Best results with the most capable model");
|
|
54
|
-
expect(message).toContain("
|
|
54
|
+
expect(message).toContain("Cheapest responses, for high-volume work");
|
|
55
|
+
expect(message).toContain("Fastest responses, with reasoning turned off");
|
|
55
56
|
expect(message).not.toContain("(DeepSeek V4 Flash)");
|
|
56
57
|
});
|
|
57
58
|
|
|
@@ -63,7 +64,8 @@ describe("/model list", () => {
|
|
|
63
64
|
expect(message).toContain(
|
|
64
65
|
"High-quality results with the most capable model",
|
|
65
66
|
);
|
|
66
|
-
expect(message).toContain("
|
|
67
|
+
expect(message).toContain("Cheapest responses, for high-volume work");
|
|
68
|
+
expect(message).toContain("Fastest responses, with reasoning turned off");
|
|
67
69
|
expect(message).not.toContain("(DeepSeek V4 Flash)");
|
|
68
70
|
});
|
|
69
71
|
});
|
|
@@ -257,7 +257,13 @@ describe("POST /v1/conversations/summarize", () => {
|
|
|
257
257
|
|
|
258
258
|
await settle();
|
|
259
259
|
|
|
260
|
-
|
|
260
|
+
// The second arg is the sink the context-window usage push rides. This
|
|
261
|
+
// route's conversation may hold the store's no-op sender, so it hands in
|
|
262
|
+
// the broadcast path its result card goes out on.
|
|
263
|
+
expect(ctx.summarizeUpToMessage).toHaveBeenCalledWith(
|
|
264
|
+
"msg-42",
|
|
265
|
+
expect.any(Function),
|
|
266
|
+
);
|
|
261
267
|
expect(ctx.emitActivityState).toHaveBeenCalledWith(
|
|
262
268
|
"thinking",
|
|
263
269
|
"context_compacting",
|