@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
|
@@ -232,6 +232,28 @@ describe("RetryProvider — rate limit backoff", () => {
|
|
|
232
232
|
}
|
|
233
233
|
});
|
|
234
234
|
|
|
235
|
+
test("attributes the credential selected for the failed request", async () => {
|
|
236
|
+
const inner = makeFailing(
|
|
237
|
+
new ProviderError("invalid key", "anthropic", 401),
|
|
238
|
+
);
|
|
239
|
+
const provider = new RetryProvider(inner, {
|
|
240
|
+
credentialSource: "byok",
|
|
241
|
+
connectionName: "anthropic-personal",
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
await provider.sendMessage(MESSAGES);
|
|
246
|
+
expect(true).toBe(false);
|
|
247
|
+
} catch (err) {
|
|
248
|
+
expect(err).toBeInstanceOf(ProviderError);
|
|
249
|
+
const providerError = err as ProviderError;
|
|
250
|
+
expect(providerError.routeAttribution).toMatchObject({
|
|
251
|
+
credentialSource: "byok",
|
|
252
|
+
connectionName: "anthropic-personal",
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
235
257
|
test("tags final error with retriesExhausted=true after retry loop gives up (JARVIS-513)", async () => {
|
|
236
258
|
// Transient socket flap from Bun's native fetch: wrapped in a
|
|
237
259
|
// ProviderError but still network-retryable via message pattern match.
|
|
@@ -333,6 +355,124 @@ describe("RetryProvider — rate limit backoff", () => {
|
|
|
333
355
|
});
|
|
334
356
|
});
|
|
335
357
|
|
|
358
|
+
describe("RetryProvider managed credential refresh", () => {
|
|
359
|
+
test("reloads managed credentials once and keeps the refreshed provider", async () => {
|
|
360
|
+
sleepSpy.mockClear();
|
|
361
|
+
const initial = makeFailing(
|
|
362
|
+
new ProviderError("assistant key expired", "anthropic", 403, {
|
|
363
|
+
reason: "invalid_credentials",
|
|
364
|
+
}),
|
|
365
|
+
"anthropic",
|
|
366
|
+
);
|
|
367
|
+
const refreshed = makeFlaky(
|
|
368
|
+
0,
|
|
369
|
+
new ProviderError("unused", "anthropic"),
|
|
370
|
+
"anthropic",
|
|
371
|
+
);
|
|
372
|
+
let refreshCalls = 0;
|
|
373
|
+
const provider = new RetryProvider(initial, {
|
|
374
|
+
credentialSource: "vellum-managed",
|
|
375
|
+
connectionName: "vellum",
|
|
376
|
+
refreshCredentialProvider: async () => {
|
|
377
|
+
refreshCalls++;
|
|
378
|
+
return refreshed;
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
await expect(provider.sendMessage(MESSAGES)).resolves.toMatchObject({
|
|
383
|
+
model: "test-model",
|
|
384
|
+
});
|
|
385
|
+
await expect(provider.sendMessage(MESSAGES)).resolves.toMatchObject({
|
|
386
|
+
model: "test-model",
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
expect(initial.calls).toBe(1);
|
|
390
|
+
expect(refreshed.calls).toBe(2);
|
|
391
|
+
expect(refreshCalls).toBe(1);
|
|
392
|
+
expect(sleepSpy).not.toHaveBeenCalled();
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test("surfaces a managed auth failure when refreshed credentials are also rejected", async () => {
|
|
396
|
+
const initial = makeFailing(
|
|
397
|
+
new ProviderError("old key rejected", "gemini", 401),
|
|
398
|
+
"gemini",
|
|
399
|
+
);
|
|
400
|
+
const refreshed = makeFailing(
|
|
401
|
+
new ProviderError("new key rejected", "gemini", 403),
|
|
402
|
+
"gemini",
|
|
403
|
+
);
|
|
404
|
+
let refreshCalls = 0;
|
|
405
|
+
const provider = new RetryProvider(initial, {
|
|
406
|
+
credentialSource: "vellum-managed",
|
|
407
|
+
connectionName: "vellum",
|
|
408
|
+
refreshCredentialProvider: async () => {
|
|
409
|
+
refreshCalls++;
|
|
410
|
+
return refreshed;
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
try {
|
|
415
|
+
await provider.sendMessage(MESSAGES);
|
|
416
|
+
expect.unreachable();
|
|
417
|
+
} catch (error) {
|
|
418
|
+
expect(error).toBeInstanceOf(ProviderError);
|
|
419
|
+
const providerError = error as ProviderError;
|
|
420
|
+
expect(providerError.message).toBe("new key rejected");
|
|
421
|
+
expect(providerError.routeAttribution).toMatchObject({
|
|
422
|
+
credentialSource: "vellum-managed",
|
|
423
|
+
connectionName: "vellum",
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
expect(initial.calls).toBe(1);
|
|
427
|
+
expect(refreshed.calls).toBe(1);
|
|
428
|
+
expect(refreshCalls).toBe(1);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("does not reload credentials for a personal provider key rejection", async () => {
|
|
432
|
+
const initial = makeFailing(
|
|
433
|
+
new ProviderError("personal key rejected", "anthropic", 401),
|
|
434
|
+
"anthropic",
|
|
435
|
+
);
|
|
436
|
+
let refreshCalls = 0;
|
|
437
|
+
const provider = new RetryProvider(initial, {
|
|
438
|
+
credentialSource: "byok",
|
|
439
|
+
refreshCredentialProvider: async () => {
|
|
440
|
+
refreshCalls++;
|
|
441
|
+
return null;
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
await expect(provider.sendMessage(MESSAGES)).rejects.toThrow(
|
|
446
|
+
"personal key rejected",
|
|
447
|
+
);
|
|
448
|
+
expect(initial.calls).toBe(1);
|
|
449
|
+
expect(refreshCalls).toBe(0);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test("does not reload managed credentials for a model restriction", async () => {
|
|
453
|
+
const initial = makeFailing(
|
|
454
|
+
new ProviderError("model unavailable", "anthropic", 403, {
|
|
455
|
+
reason: "model_restricted",
|
|
456
|
+
}),
|
|
457
|
+
"anthropic",
|
|
458
|
+
);
|
|
459
|
+
let refreshCalls = 0;
|
|
460
|
+
const provider = new RetryProvider(initial, {
|
|
461
|
+
credentialSource: "vellum-managed",
|
|
462
|
+
refreshCredentialProvider: async () => {
|
|
463
|
+
refreshCalls++;
|
|
464
|
+
return null;
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
await expect(provider.sendMessage(MESSAGES)).rejects.toThrow(
|
|
469
|
+
"model unavailable",
|
|
470
|
+
);
|
|
471
|
+
expect(initial.calls).toBe(1);
|
|
472
|
+
expect(refreshCalls).toBe(0);
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
|
|
336
476
|
// ---------------------------------------------------------------------------
|
|
337
477
|
// RetryProvider — ContextOverflowError short-circuit
|
|
338
478
|
// ---------------------------------------------------------------------------
|
|
@@ -27,6 +27,7 @@ import type {
|
|
|
27
27
|
ProviderResponse,
|
|
28
28
|
SendMessageOptions,
|
|
29
29
|
} from "../providers/types.js";
|
|
30
|
+
import { ProviderError } from "../util/errors.js";
|
|
30
31
|
import { setConfig } from "./helpers/set-config.js";
|
|
31
32
|
|
|
32
33
|
const DUMMY_MESSAGES: Message[] = [
|
|
@@ -239,6 +240,91 @@ describe("SendMessageOptions.config.overrideProfile", () => {
|
|
|
239
240
|
expect(response.model).toBe("openai");
|
|
240
241
|
});
|
|
241
242
|
|
|
243
|
+
test("CallSiteRoutingProvider attributes provider errors to the actual resolved connection", async () => {
|
|
244
|
+
setLlmConfig({
|
|
245
|
+
profiles: {
|
|
246
|
+
fast: {
|
|
247
|
+
provider: "openai",
|
|
248
|
+
provider_connection: "openai-conn",
|
|
249
|
+
model: "gpt-5.4",
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const providerError = new ProviderError(
|
|
255
|
+
"OpenAI API error (429): Too many requests",
|
|
256
|
+
"openai",
|
|
257
|
+
429,
|
|
258
|
+
{ reason: "rate_limited" },
|
|
259
|
+
);
|
|
260
|
+
const defaultProvider = makeThrowingProvider(
|
|
261
|
+
"anthropic",
|
|
262
|
+
new Error("default provider should not be called"),
|
|
263
|
+
);
|
|
264
|
+
const altProvider = makeThrowingProvider("openai", providerError);
|
|
265
|
+
altProvider.routeAttribution = {
|
|
266
|
+
connectionName: "openai-recovered",
|
|
267
|
+
isManagedRoute: false,
|
|
268
|
+
};
|
|
269
|
+
const wrapped = new CallSiteRoutingProvider(
|
|
270
|
+
defaultProvider,
|
|
271
|
+
async (connectionName) =>
|
|
272
|
+
connectionName === "openai-conn" ? altProvider : null,
|
|
273
|
+
{ connectionName: "vellum", isManagedRoute: true },
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
await expect(
|
|
277
|
+
wrapped.sendMessage(DUMMY_MESSAGES, {
|
|
278
|
+
config: { callSite: "mainAgent", overrideProfile: "fast" },
|
|
279
|
+
}),
|
|
280
|
+
).rejects.toBe(providerError);
|
|
281
|
+
expect(providerError.routeAttribution).toEqual({
|
|
282
|
+
connectionName: "openai-recovered",
|
|
283
|
+
profileName: "fast",
|
|
284
|
+
isManagedRoute: false,
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("CallSiteRoutingProvider attributes soft fallback errors to the actual default connection", async () => {
|
|
289
|
+
setLlmConfig({
|
|
290
|
+
profiles: {
|
|
291
|
+
fast: {
|
|
292
|
+
provider: "openai",
|
|
293
|
+
provider_connection: "openai-conn",
|
|
294
|
+
model: "gpt-5.4",
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const providerError = new ProviderError(
|
|
300
|
+
"Anthropic API error (429): Too many requests",
|
|
301
|
+
"anthropic",
|
|
302
|
+
429,
|
|
303
|
+
{ reason: "rate_limited" },
|
|
304
|
+
);
|
|
305
|
+
const defaultProvider = makeThrowingProvider("anthropic", providerError);
|
|
306
|
+
defaultProvider.routeAttribution = {
|
|
307
|
+
connectionName: "anthropic-default",
|
|
308
|
+
isManagedRoute: false,
|
|
309
|
+
};
|
|
310
|
+
const wrapped = new CallSiteRoutingProvider(
|
|
311
|
+
defaultProvider,
|
|
312
|
+
async () => null,
|
|
313
|
+
defaultProvider.routeAttribution,
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
await expect(
|
|
317
|
+
wrapped.sendMessage(DUMMY_MESSAGES, {
|
|
318
|
+
config: { callSite: "mainAgent", overrideProfile: "fast" },
|
|
319
|
+
}),
|
|
320
|
+
).rejects.toBe(providerError);
|
|
321
|
+
expect(providerError.routeAttribution).toEqual({
|
|
322
|
+
connectionName: "anthropic-default",
|
|
323
|
+
profileName: "fast",
|
|
324
|
+
isManagedRoute: false,
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
242
328
|
test("missing overrideProfile name silently falls through to base resolution", async () => {
|
|
243
329
|
setLlmConfig({
|
|
244
330
|
// The call-site tweak applies last in resolution, so it pins the model
|
|
@@ -328,6 +414,15 @@ describe("SendMessageOptions.config.overrideProfile", () => {
|
|
|
328
414
|
});
|
|
329
415
|
});
|
|
330
416
|
|
|
417
|
+
function makeThrowingProvider(name: string, error: Error): Provider {
|
|
418
|
+
return {
|
|
419
|
+
name,
|
|
420
|
+
async sendMessage(): Promise<ProviderResponse> {
|
|
421
|
+
throw error;
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
331
426
|
describe("SendMessageOptions.config.forceOverrideProfile", () => {
|
|
332
427
|
test("CallSiteConfiguredProvider forwards forceOverrideProfile into the send config", async () => {
|
|
333
428
|
let captured: SendMessageOptions | undefined;
|
|
@@ -5,6 +5,10 @@ import {
|
|
|
5
5
|
ensureConversationExists,
|
|
6
6
|
getConversation,
|
|
7
7
|
} from "../persistence/conversation-crud.js";
|
|
8
|
+
import {
|
|
9
|
+
isEchoSuppressedUserMessage,
|
|
10
|
+
isReplyPushIneligibleUserMessage,
|
|
11
|
+
} from "../persistence/conversation-types.js";
|
|
8
12
|
import { getDb } from "../persistence/db-connection.js";
|
|
9
13
|
import { initializeDb } from "../persistence/db-init.js";
|
|
10
14
|
|
|
@@ -31,6 +35,9 @@ mock.module("../runtime/sync/resource-sync-events.js", () => ({
|
|
|
31
35
|
// `ensureConversationExists`. The persistence module is intentionally NOT
|
|
32
36
|
// mocked so the real `ensureConversationExists` runs against the real DB.
|
|
33
37
|
let lastProcessMessageConversationId: string | undefined;
|
|
38
|
+
let lastProcessMessageOptions: Record<string, unknown> | undefined;
|
|
39
|
+
let lastEnqueueOptions: Record<string, unknown> | undefined;
|
|
40
|
+
let conversationIsProcessing = false;
|
|
34
41
|
mock.module("../daemon/conversation-store.js", () => ({
|
|
35
42
|
getOrCreateConversation: async (
|
|
36
43
|
conversationId: string,
|
|
@@ -50,14 +57,18 @@ mock.module("../daemon/conversation-store.js", () => ({
|
|
|
50
57
|
}
|
|
51
58
|
return {
|
|
52
59
|
abortController: undefined,
|
|
53
|
-
isProcessing: () =>
|
|
54
|
-
async processMessage() {
|
|
60
|
+
isProcessing: () => conversationIsProcessing,
|
|
61
|
+
async processMessage(processOptions: Record<string, unknown>) {
|
|
55
62
|
// The row must already exist by the time the turn persists its user
|
|
56
63
|
// message — record the id so the FK precondition can be asserted.
|
|
57
64
|
lastProcessMessageConversationId = conversationId;
|
|
65
|
+
lastProcessMessageOptions = processOptions;
|
|
58
66
|
return "user-message-id";
|
|
59
67
|
},
|
|
60
|
-
enqueueMessage: () =>
|
|
68
|
+
enqueueMessage: (enqueueOptions: Record<string, unknown>) => {
|
|
69
|
+
lastEnqueueOptions = enqueueOptions;
|
|
70
|
+
return { rejected: false };
|
|
71
|
+
},
|
|
61
72
|
};
|
|
62
73
|
},
|
|
63
74
|
}));
|
|
@@ -82,6 +93,9 @@ describe("runConversationTurn persistence", () => {
|
|
|
82
93
|
db.run("DELETE FROM conversations");
|
|
83
94
|
listChangedCalls.length = 0;
|
|
84
95
|
lastProcessMessageConversationId = undefined;
|
|
96
|
+
lastProcessMessageOptions = undefined;
|
|
97
|
+
lastEnqueueOptions = undefined;
|
|
98
|
+
conversationIsProcessing = false;
|
|
85
99
|
});
|
|
86
100
|
|
|
87
101
|
test("persists a conversations row for a freshly-minted conversation", async () => {
|
|
@@ -130,3 +144,52 @@ describe("runConversationTurn persistence", () => {
|
|
|
130
144
|
expect(listChangedCalls).toEqual([]);
|
|
131
145
|
});
|
|
132
146
|
});
|
|
147
|
+
|
|
148
|
+
// A plugin drives its turn on its own schedule, so the row that opens it is
|
|
149
|
+
// machine-initiated even when the turn runs in an ordinary standard
|
|
150
|
+
// conversation the user also types into. Each case asserts the shared
|
|
151
|
+
// eligibility predicate's verdict on the stamped metadata, so the marker and
|
|
152
|
+
// the gate that reads it cannot drift apart.
|
|
153
|
+
describe("runConversationTurn provenance", () => {
|
|
154
|
+
beforeEach(() => {
|
|
155
|
+
const db = getDb();
|
|
156
|
+
db.run("DELETE FROM messages");
|
|
157
|
+
db.run("DELETE FROM conversations");
|
|
158
|
+
lastProcessMessageOptions = undefined;
|
|
159
|
+
lastEnqueueOptions = undefined;
|
|
160
|
+
conversationIsProcessing = false;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("stamps the initiating row automated so its reply raises no push", async () => {
|
|
164
|
+
const existing = createConversation({ title: "standard conversation" });
|
|
165
|
+
|
|
166
|
+
await runConversationTurn({
|
|
167
|
+
conversationId: existing.id,
|
|
168
|
+
content: [{ type: "text", text: "transcript excerpt" }],
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const metadata = lastProcessMessageOptions?.metadata as Record<
|
|
172
|
+
string,
|
|
173
|
+
unknown
|
|
174
|
+
>;
|
|
175
|
+
expect(metadata).toEqual({ automated: true });
|
|
176
|
+
expect(isReplyPushIneligibleUserMessage(metadata)).toBe(true);
|
|
177
|
+
// Not an echo-suppression marker: the row still renders in the transcript.
|
|
178
|
+
expect(isEchoSuppressedUserMessage(metadata)).toBe(false);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("stamps the same marker on a turn queued behind a busy conversation", async () => {
|
|
182
|
+
const existing = createConversation({ title: "busy conversation" });
|
|
183
|
+
conversationIsProcessing = true;
|
|
184
|
+
|
|
185
|
+
const result = await runConversationTurn({
|
|
186
|
+
conversationId: existing.id,
|
|
187
|
+
content: [{ type: "text", text: "transcript excerpt" }],
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
expect(result.queued).toBe(true);
|
|
191
|
+
const metadata = lastEnqueueOptions?.metadata as Record<string, unknown>;
|
|
192
|
+
expect(metadata).toEqual({ automated: true });
|
|
193
|
+
expect(isReplyPushIneligibleUserMessage(metadata)).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verifies how `persistQueuedMessageBody` resolves the `scripted` marker onto
|
|
3
|
+
* `messages.metadata.scripted`, which `turn-events-store` forwards to
|
|
4
|
+
* `TurnTelemetryEvent.scripted`.
|
|
5
|
+
*
|
|
6
|
+
* `scripted` is what lets activation metrics exclude auto-sent onboarding
|
|
7
|
+
* turns for EVERY owner, rather than only those who opted into diagnostics
|
|
8
|
+
* (ANT-10). Its contract is three-state and the states are not
|
|
9
|
+
* interchangeable:
|
|
10
|
+
*
|
|
11
|
+
* true - auto-sent on the user's behalf
|
|
12
|
+
* false - a genuine typed user message
|
|
13
|
+
* absent - UNKNOWN
|
|
14
|
+
*
|
|
15
|
+
* The absent case is the one worth protecting: downstream falls back to the
|
|
16
|
+
* legacy trace-text classifier when the flag is missing, but TRUSTS an
|
|
17
|
+
* explicit `false`. So a spurious `false` is strictly worse than no value:
|
|
18
|
+
* it re-inflates activation past the point where the fallback can catch it.
|
|
19
|
+
*
|
|
20
|
+
* Mirrors the mock harness of `client-os-metadata-persistence.test.ts`:
|
|
21
|
+
* exercises `persistQueuedMessageBody` directly with a captured `addMessage`.
|
|
22
|
+
*/
|
|
23
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
24
|
+
|
|
25
|
+
const addMessageCalls: Array<{
|
|
26
|
+
conversationId: string;
|
|
27
|
+
role: string;
|
|
28
|
+
content: string;
|
|
29
|
+
metadata?: Record<string, unknown>;
|
|
30
|
+
}> = [];
|
|
31
|
+
|
|
32
|
+
mock.module("../persistence/conversation-crud.js", () => ({
|
|
33
|
+
setConversationProcessingStartedAt: () => {},
|
|
34
|
+
isConversationProcessing: () => false,
|
|
35
|
+
addMessage: async (
|
|
36
|
+
conversationId: string,
|
|
37
|
+
role: string,
|
|
38
|
+
content: string,
|
|
39
|
+
options?: { metadata?: Record<string, unknown> },
|
|
40
|
+
) => {
|
|
41
|
+
addMessageCalls.push({
|
|
42
|
+
conversationId,
|
|
43
|
+
role,
|
|
44
|
+
content,
|
|
45
|
+
metadata: options?.metadata,
|
|
46
|
+
});
|
|
47
|
+
return { id: `persisted-${addMessageCalls.length}` };
|
|
48
|
+
},
|
|
49
|
+
getConversation: () => null,
|
|
50
|
+
provenanceFromTrustContext: () => ({}),
|
|
51
|
+
setConversationOriginChannelIfUnset: () => {},
|
|
52
|
+
setConversationOriginInterfaceIfUnset: () => {},
|
|
53
|
+
reserveMessage: mock(async () => ({ id: "msg-reserve" })),
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
mock.module("../persistence/conversation-disk-view.js", () => ({
|
|
57
|
+
syncMessageToDisk: () => {},
|
|
58
|
+
updateMetaFile: () => {},
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
mock.module("../persistence/attachments-store.js", () => ({
|
|
62
|
+
attachmentExists: () => false,
|
|
63
|
+
linkAttachmentToMessage: () => {},
|
|
64
|
+
attachInlineAttachmentToMessage: () => {},
|
|
65
|
+
validateAttachmentUpload: () => ({ ok: true }),
|
|
66
|
+
AttachmentUploadError: class extends Error {},
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
import type {
|
|
70
|
+
TurnChannelContext,
|
|
71
|
+
TurnInterfaceContext,
|
|
72
|
+
} from "../channels/types.js";
|
|
73
|
+
import type { MessagingConversationContext } from "../daemon/conversation-messaging.js";
|
|
74
|
+
import { persistQueuedMessageBody } from "../daemon/conversation-messaging.js";
|
|
75
|
+
import type { MessageQueue } from "../daemon/conversation-queue-manager.js";
|
|
76
|
+
|
|
77
|
+
function createContext(): MessagingConversationContext {
|
|
78
|
+
const channel: TurnChannelContext = {
|
|
79
|
+
userMessageChannel: "vellum",
|
|
80
|
+
assistantMessageChannel: "vellum",
|
|
81
|
+
};
|
|
82
|
+
const iface: TurnInterfaceContext = {
|
|
83
|
+
userMessageInterface: "web",
|
|
84
|
+
assistantMessageInterface: "web",
|
|
85
|
+
};
|
|
86
|
+
const queueStub = {
|
|
87
|
+
push: () => true,
|
|
88
|
+
drain: () => [],
|
|
89
|
+
size: () => 0,
|
|
90
|
+
} as unknown as MessageQueue;
|
|
91
|
+
let processing = false;
|
|
92
|
+
return {
|
|
93
|
+
conversationId: "conv-scripted-test",
|
|
94
|
+
messages: [],
|
|
95
|
+
isProcessing: () => processing,
|
|
96
|
+
setProcessing: (value: boolean) => {
|
|
97
|
+
processing = value;
|
|
98
|
+
},
|
|
99
|
+
abortController: null,
|
|
100
|
+
queue: queueStub,
|
|
101
|
+
clientOs: undefined,
|
|
102
|
+
getTurnChannelContext: () => channel,
|
|
103
|
+
getTurnInterfaceContext: () => iface,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function lastUserMetadata(): Record<string, unknown> {
|
|
108
|
+
expect(addMessageCalls.length).toBeGreaterThan(0);
|
|
109
|
+
const metadata = addMessageCalls.at(-1)?.metadata;
|
|
110
|
+
expect(metadata).toBeDefined();
|
|
111
|
+
return metadata!;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
describe("scripted-turn metadata persistence", () => {
|
|
115
|
+
beforeEach(() => {
|
|
116
|
+
addMessageCalls.length = 0;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("stamps the typed option", async () => {
|
|
120
|
+
const ctx = createContext();
|
|
121
|
+
await persistQueuedMessageBody(ctx, {
|
|
122
|
+
content: "[User action on choice surface: Work]",
|
|
123
|
+
requestId: "req-option-true",
|
|
124
|
+
scripted: true,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(lastUserMetadata().scripted).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("carries the flag through the metadata bag for queued sends", async () => {
|
|
131
|
+
// The queue round-trips `metadata` but not `PersistMessageOptions`, so a
|
|
132
|
+
// surface action that was enqueued while a turn was in flight can only
|
|
133
|
+
// deliver its marker this way. If this regresses, queued surface actions
|
|
134
|
+
// silently become "unknown" while direct ones stay marked.
|
|
135
|
+
const ctx = createContext();
|
|
136
|
+
await persistQueuedMessageBody(ctx, {
|
|
137
|
+
content: "[User action on app: Answer Selected]",
|
|
138
|
+
requestId: "req-metadata-true",
|
|
139
|
+
metadata: { scripted: true },
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(lastUserMetadata().scripted).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("treats `automated` as implying scripted", async () => {
|
|
146
|
+
// Machine-authored by definition, so it is not a turn the user typed.
|
|
147
|
+
const ctx = createContext();
|
|
148
|
+
await persistQueuedMessageBody(ctx, {
|
|
149
|
+
content: "automated skill message",
|
|
150
|
+
requestId: "req-automated",
|
|
151
|
+
metadata: { automated: true },
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(lastUserMetadata().scripted).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("lets an explicit option override the automated default", async () => {
|
|
158
|
+
const ctx = createContext();
|
|
159
|
+
await persistQueuedMessageBody(ctx, {
|
|
160
|
+
content: "automated but counts as a real turn",
|
|
161
|
+
requestId: "req-automated-override",
|
|
162
|
+
metadata: { automated: true },
|
|
163
|
+
scripted: false,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(lastUserMetadata().scripted).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("defaults an ordinary send to false, asserting a typed turn", async () => {
|
|
170
|
+
// The default is what makes activation MEASURABLE: absent would mean
|
|
171
|
+
// "unknown", which is strictly worse information than a truthful false.
|
|
172
|
+
// Safe only because every auto-send path is marked at its source (web
|
|
173
|
+
// onboarding flows, surface synthetics, `automated`).
|
|
174
|
+
const ctx = createContext();
|
|
175
|
+
await persistQueuedMessageBody(ctx, {
|
|
176
|
+
content: "a message the user actually typed",
|
|
177
|
+
requestId: "req-ordinary",
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
expect(lastUserMetadata().scripted).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("falls back to the default for a non-boolean `scripted` in the bag", async () => {
|
|
184
|
+
// Guards against a truthy string ("true", "1") from an untyped caller
|
|
185
|
+
// being read as a scripted assertion. It must not survive the metadata
|
|
186
|
+
// spread either: sqlite would store the string verbatim and the store's
|
|
187
|
+
// narrowing turns anything that isn't 1 into `false`, so a leaked "true"
|
|
188
|
+
// would invert into "the user typed this".
|
|
189
|
+
const ctx = createContext();
|
|
190
|
+
await persistQueuedMessageBody(ctx, {
|
|
191
|
+
content: "hello",
|
|
192
|
+
requestId: "req-bogus",
|
|
193
|
+
metadata: { scripted: "true" },
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(lastUserMetadata().scripted).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("stamps false when the caller explicitly asserts a typed turn", async () => {
|
|
200
|
+
const ctx = createContext();
|
|
201
|
+
await persistQueuedMessageBody(ctx, {
|
|
202
|
+
content: "hello",
|
|
203
|
+
requestId: "req-explicit-false",
|
|
204
|
+
scripted: false,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
expect(lastUserMetadata().scripted).toBe(false);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -798,6 +798,33 @@ describe("always-candidate frontmatter parsing", () => {
|
|
|
798
798
|
const wf = loadSkillCatalog().find((s) => s.id === "workflows");
|
|
799
799
|
expect(wf?.alwaysCandidate).toBe(true);
|
|
800
800
|
});
|
|
801
|
+
|
|
802
|
+
test("the bundled visualize skill is flagged always-candidate", () => {
|
|
803
|
+
const viz = loadSkillCatalog().find((s) => s.id === "visualize");
|
|
804
|
+
expect(viz?.alwaysCandidate).toBe(true);
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
test("every always-candidate bundled skill's card fits its budget", async () => {
|
|
808
|
+
// The card is pinned into the selector's stable prefix every turn, and
|
|
809
|
+
// `buildSkillContent` hard-truncates at the budget — a skill that overruns
|
|
810
|
+
// silently loses its last activation hints and its avoid-when list, which
|
|
811
|
+
// is exactly the guidance the pinning was added to deliver.
|
|
812
|
+
const { ALWAYS_CANDIDATE_CARD_CHARS, buildSkillContent } =
|
|
813
|
+
await import("../plugins/defaults/memory/substrate/skill-content.js");
|
|
814
|
+
const pinned = loadSkillCatalog().filter(
|
|
815
|
+
(skill) => skill.bundled && skill.alwaysCandidate === true,
|
|
816
|
+
);
|
|
817
|
+
expect(pinned.length).toBeGreaterThan(0);
|
|
818
|
+
|
|
819
|
+
const overrun = pinned
|
|
820
|
+
.map((skill) => ({
|
|
821
|
+
id: skill.id,
|
|
822
|
+
chars: buildSkillContent(skill, Number.MAX_SAFE_INTEGER).length,
|
|
823
|
+
}))
|
|
824
|
+
.filter(({ chars }) => chars > ALWAYS_CANDIDATE_CARD_CHARS)
|
|
825
|
+
.map(({ id, chars }) => `${id}: ${chars}`);
|
|
826
|
+
expect(overrun).toEqual([]);
|
|
827
|
+
});
|
|
801
828
|
});
|
|
802
829
|
|
|
803
830
|
describe("bundled skill categories", () => {
|
|
@@ -40,8 +40,6 @@ mock.module("../messaging/providers/slack/client.js", () => ({
|
|
|
40
40
|
}
|
|
41
41
|
throw new Error(`User not found: ${userId}`);
|
|
42
42
|
},
|
|
43
|
-
// auth.ts imports SlackApiError from the client; export it from the mock.
|
|
44
|
-
SlackApiError: class SlackApiError extends Error {},
|
|
45
43
|
}));
|
|
46
44
|
|
|
47
45
|
// ---------------------------------------------------------------------------
|
|
@@ -51,9 +51,6 @@ mock.module("../messaging/providers/slack/client.js", () => ({
|
|
|
51
51
|
_text: string,
|
|
52
52
|
_opts?: unknown,
|
|
53
53
|
) => postMessageResult,
|
|
54
|
-
// auth.ts imports SlackApiError from the client; export it so the import
|
|
55
|
-
// graph loads (this file never triggers the fallback that inspects it).
|
|
56
|
-
SlackApiError: class SlackApiError extends Error {},
|
|
57
54
|
}));
|
|
58
55
|
|
|
59
56
|
let appStoreResult: unknown = null;
|
|
@@ -35,8 +35,6 @@ mock.module("../messaging/providers/slack/client.js", () => ({
|
|
|
35
35
|
listUsersCalls.push({ cursor });
|
|
36
36
|
return listUsersPages[listUsersCalls.length - 1];
|
|
37
37
|
},
|
|
38
|
-
// auth.ts imports SlackApiError from the client; export it from the mock.
|
|
39
|
-
SlackApiError: class SlackApiError extends Error {},
|
|
40
38
|
}));
|
|
41
39
|
|
|
42
40
|
// ---------------------------------------------------------------------------
|
|
@@ -1691,6 +1691,17 @@ describe("Subagent advisor-role consult", () => {
|
|
|
1691
1691
|
expect(captured.current!.config.systemPromptOverride).toContain(
|
|
1692
1692
|
"PARENT SYSTEM PROMPT",
|
|
1693
1693
|
);
|
|
1694
|
+
// The situational context pack must never ride display surfaces: the
|
|
1695
|
+
// system prompt stays minimal and `objective` (rendered verbatim by the
|
|
1696
|
+
// subagent detail panel) stays the concise advice request. Only the
|
|
1697
|
+
// non-display `requestText` may carry the pack.
|
|
1698
|
+
expect(captured.current!.config.systemPromptOverride).not.toContain(
|
|
1699
|
+
"<agent_environment>",
|
|
1700
|
+
);
|
|
1701
|
+
expect(captured.current!.config.objective).not.toContain(
|
|
1702
|
+
"<agent_environment>",
|
|
1703
|
+
);
|
|
1704
|
+
expect(captured.current!.config.requestText).toContain("advise me");
|
|
1694
1705
|
} finally {
|
|
1695
1706
|
restore();
|
|
1696
1707
|
mockFindConversation = () => undefined;
|