@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
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { describe, expect, test } from "bun:test";
|
|
4
|
+
|
|
5
|
+
import { Glob } from "bun";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Guard test for the CLI-logger boundary.
|
|
9
|
+
*
|
|
10
|
+
* `src/cli/logger.ts` is the plain-stdout writer for short-lived `assistant …`
|
|
11
|
+
* invocations. It has no level filtering and no log-file routing, so a
|
|
12
|
+
* daemon-side module that reaches for it writes debug output straight to a
|
|
13
|
+
* stdout the daemon may not own. When the daemon runs detached and its
|
|
14
|
+
* spawning parent has closed the read end of the stdout pipe, those writes
|
|
15
|
+
* fail with `EPIPE` and take the process down through the fatal-error handler.
|
|
16
|
+
*
|
|
17
|
+
* Daemon and shared modules use `getLogger()` from `util/logger.js`, which
|
|
18
|
+
* routes to the rotating log file and skips stdout entirely when stdout is not
|
|
19
|
+
* a TTY.
|
|
20
|
+
*
|
|
21
|
+
* `no-restricted-imports` in `eslint.config.mjs` bans this too, but matches on
|
|
22
|
+
* the import specifier. This guard resolves each relative import to a path, so
|
|
23
|
+
* it holds regardless of how the specifier is spelled.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** Tests run from `assistant/`. */
|
|
27
|
+
const ASSISTANT_ROOT = process.cwd();
|
|
28
|
+
const CLI_LOGGER_PATH = resolve(ASSISTANT_ROOT, "src/cli/logger.ts");
|
|
29
|
+
|
|
30
|
+
/** Modules allowed to import the CLI logger: the CLI itself, and tests. */
|
|
31
|
+
function isExempt(relPath: string): boolean {
|
|
32
|
+
return (
|
|
33
|
+
relPath.startsWith("src/cli/") ||
|
|
34
|
+
relPath.includes("/__tests__/") ||
|
|
35
|
+
relPath.endsWith(".test.ts")
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function findCliLoggerImporters(): string[] {
|
|
40
|
+
const pattern = /\b(?:from|import)\s*\(?\s*["'](\.[^"']*logger\.js)["']/g;
|
|
41
|
+
const violations = new Set<string>();
|
|
42
|
+
|
|
43
|
+
for (const relPath of new Glob("src/**/*.ts").scanSync({
|
|
44
|
+
cwd: ASSISTANT_ROOT,
|
|
45
|
+
})) {
|
|
46
|
+
if (isExempt(relPath)) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const filePath = join(ASSISTANT_ROOT, relPath);
|
|
50
|
+
for (const match of readFileSync(filePath, "utf-8").matchAll(pattern)) {
|
|
51
|
+
// Imports carry the compiled `.js` extension; sources are `.ts`.
|
|
52
|
+
const resolved = resolve(
|
|
53
|
+
dirname(filePath),
|
|
54
|
+
match[1]!.replace(/\.js$/, ".ts"),
|
|
55
|
+
);
|
|
56
|
+
if (resolved === CLI_LOGGER_PATH) {
|
|
57
|
+
violations.add(relPath);
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return Array.from(violations).sort();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe("CLI logger boundary", () => {
|
|
66
|
+
test("no production module outside src/cli/ imports cli/logger", () => {
|
|
67
|
+
expect(findCliLoggerImporters()).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("the guard resolves the CLI logger it is guarding", () => {
|
|
71
|
+
// A typo in CLI_LOGGER_PATH would make the guard above vacuously pass.
|
|
72
|
+
expect(readFileSync(CLI_LOGGER_PATH, "utf-8")).toContain("getCliLogger");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("pinned-tabs logs through the daemon logger", () => {
|
|
76
|
+
// The incident site: pinned-tab mutations run inside daemon host-browser
|
|
77
|
+
// request handling, on every tab pin, select, close, and invalidation.
|
|
78
|
+
const source = readFileSync(
|
|
79
|
+
join(ASSISTANT_ROOT, "src/tools/browser/pinned-tabs.ts"),
|
|
80
|
+
"utf-8",
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
expect(source).toContain('from "../../util/logger.js"');
|
|
84
|
+
expect(source).toContain('getLogger("pinned-tabs")');
|
|
85
|
+
expect(source).not.toContain("cli/logger");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
* turn. The `context_compacted` event is the single source of truth for the
|
|
8
8
|
* indicator — the paired `usage_update` intentionally omits
|
|
9
9
|
* `contextWindowTokens` to avoid a redundant SwiftUI invalidation.
|
|
10
|
+
*
|
|
11
|
+
* Also covers the `context_window_usage` push, which carries the provider
|
|
12
|
+
* tokenizer's post-compaction count so the web indicator matches the numbers
|
|
13
|
+
* on the `/compact` result card.
|
|
10
14
|
*/
|
|
11
15
|
import { describe, expect, mock, test } from "bun:test";
|
|
12
16
|
|
|
@@ -218,7 +222,8 @@ import { Conversation } from "../daemon/conversation.js";
|
|
|
218
222
|
// Helpers
|
|
219
223
|
// ---------------------------------------------------------------------------
|
|
220
224
|
|
|
221
|
-
function makeProvider() {
|
|
225
|
+
function makeProvider(counts?: number[]) {
|
|
226
|
+
const pending = counts ? [...counts] : null;
|
|
222
227
|
return {
|
|
223
228
|
name: "mock-provider",
|
|
224
229
|
async sendMessage(): Promise<ProviderResponse> {
|
|
@@ -229,16 +234,20 @@ function makeProvider() {
|
|
|
229
234
|
stopReason: "end_turn",
|
|
230
235
|
};
|
|
231
236
|
},
|
|
237
|
+
// Present only when the test supplies counts, so the other cases keep
|
|
238
|
+
// exercising the local-estimate fallback.
|
|
239
|
+
...(pending ? { countInputTokens: async () => pending.shift() ?? 0 } : {}),
|
|
232
240
|
};
|
|
233
241
|
}
|
|
234
242
|
|
|
235
243
|
function makeConversation(
|
|
236
244
|
collected: AssistantEvent[],
|
|
237
245
|
id = "conv-compact-events",
|
|
246
|
+
counts?: number[],
|
|
238
247
|
): Conversation {
|
|
239
248
|
return new Conversation(
|
|
240
249
|
id,
|
|
241
|
-
makeProvider(),
|
|
250
|
+
makeProvider(counts),
|
|
242
251
|
"system prompt",
|
|
243
252
|
(msg) => {
|
|
244
253
|
collected.push(msg);
|
|
@@ -383,6 +392,134 @@ describe("forceCompact event emission", () => {
|
|
|
383
392
|
});
|
|
384
393
|
});
|
|
385
394
|
|
|
395
|
+
describe("forceCompact context-window usage push", () => {
|
|
396
|
+
test("pushes the post-compaction tokenizer count the result card reports", async () => {
|
|
397
|
+
const collected: AssistantEvent[] = [];
|
|
398
|
+
mockCompactResult = {
|
|
399
|
+
messages: [],
|
|
400
|
+
compacted: true,
|
|
401
|
+
// Estimate-based figures the compaction pipeline records internally.
|
|
402
|
+
// The user-facing numbers come from the provider tokenizer instead.
|
|
403
|
+
previousEstimatedInputTokens: 43_000,
|
|
404
|
+
estimatedInputTokens: 12_000,
|
|
405
|
+
maxInputTokens: 200_000,
|
|
406
|
+
thresholdTokens: 160_000,
|
|
407
|
+
compactedMessages: 10,
|
|
408
|
+
compactedPersistedMessages: 5,
|
|
409
|
+
summaryCalls: 1,
|
|
410
|
+
summaryInputTokens: 500,
|
|
411
|
+
summaryOutputTokens: 200,
|
|
412
|
+
summaryModel: "test-model",
|
|
413
|
+
summaryText: "summary text",
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const conversation = makeConversation(
|
|
417
|
+
collected,
|
|
418
|
+
"conv-compact-usage",
|
|
419
|
+
[56_000, 18_000],
|
|
420
|
+
);
|
|
421
|
+
const result = await conversation.forceCompact();
|
|
422
|
+
|
|
423
|
+
expect(result.previousEstimatedInputTokens).toBe(56_000);
|
|
424
|
+
expect(result.estimatedInputTokens).toBe(18_000);
|
|
425
|
+
|
|
426
|
+
const usage = collected.filter((m) => m.type === "context_window_usage");
|
|
427
|
+
expect(usage.length).toBe(1);
|
|
428
|
+
const usageEvent = usage[0] as Extract<
|
|
429
|
+
AssistantEvent,
|
|
430
|
+
{ type: "context_window_usage" }
|
|
431
|
+
>;
|
|
432
|
+
expect(usageEvent.conversationId).toBe("conv-compact-usage");
|
|
433
|
+
// Same figures the `/compact` card renders, so the indicator and the card
|
|
434
|
+
// cannot disagree.
|
|
435
|
+
expect(usageEvent.tokens).toBe(result.estimatedInputTokens);
|
|
436
|
+
expect(usageEvent.maxTokens).toBe(200_000);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test("pushes to the caller's sink when the conversation's own sender is dead", async () => {
|
|
440
|
+
// A `/compact` draining behind an interactive turn: `process-message.ts`
|
|
441
|
+
// resets `sendToClient` to a no-op in its `finally`, while the queued item
|
|
442
|
+
// still carries the live sink its result card streams on. The push has to
|
|
443
|
+
// follow the card, not the dead sender.
|
|
444
|
+
const stranded: AssistantEvent[] = [];
|
|
445
|
+
const cardSink: AssistantEvent[] = [];
|
|
446
|
+
mockCompactResult = {
|
|
447
|
+
messages: [],
|
|
448
|
+
compacted: true,
|
|
449
|
+
previousEstimatedInputTokens: 43_000,
|
|
450
|
+
estimatedInputTokens: 12_000,
|
|
451
|
+
maxInputTokens: 200_000,
|
|
452
|
+
thresholdTokens: 160_000,
|
|
453
|
+
compactedMessages: 10,
|
|
454
|
+
compactedPersistedMessages: 5,
|
|
455
|
+
summaryCalls: 1,
|
|
456
|
+
summaryInputTokens: 500,
|
|
457
|
+
summaryOutputTokens: 200,
|
|
458
|
+
summaryModel: "test-model",
|
|
459
|
+
summaryText: "summary text",
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const conversation = makeConversation(
|
|
463
|
+
stranded,
|
|
464
|
+
"conv-compact-dead-sender",
|
|
465
|
+
[56_000, 18_000],
|
|
466
|
+
);
|
|
467
|
+
conversation.updateClient(() => {}, true);
|
|
468
|
+
|
|
469
|
+
const result = await conversation.forceCompact((msg) => cardSink.push(msg));
|
|
470
|
+
|
|
471
|
+
const usage = cardSink.filter((m) => m.type === "context_window_usage");
|
|
472
|
+
expect(usage.length).toBe(1);
|
|
473
|
+
expect(
|
|
474
|
+
(usage[0] as Extract<AssistantEvent, { type: "context_window_usage" }>)
|
|
475
|
+
.tokens,
|
|
476
|
+
).toBe(result.estimatedInputTokens);
|
|
477
|
+
expect(
|
|
478
|
+
stranded.filter((m) => m.type === "context_window_usage").length,
|
|
479
|
+
).toBe(0);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test("pushes the current count when there was nothing to compact", async () => {
|
|
483
|
+
const collected: AssistantEvent[] = [];
|
|
484
|
+
mockCompactResult = {
|
|
485
|
+
messages: [],
|
|
486
|
+
compacted: false,
|
|
487
|
+
previousEstimatedInputTokens: 0,
|
|
488
|
+
estimatedInputTokens: 0,
|
|
489
|
+
maxInputTokens: 200_000,
|
|
490
|
+
thresholdTokens: 160_000,
|
|
491
|
+
compactedMessages: 0,
|
|
492
|
+
compactedPersistedMessages: 0,
|
|
493
|
+
summaryCalls: 0,
|
|
494
|
+
summaryInputTokens: 0,
|
|
495
|
+
summaryOutputTokens: 0,
|
|
496
|
+
summaryModel: "",
|
|
497
|
+
summaryText: "",
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const conversation = makeConversation(
|
|
501
|
+
collected,
|
|
502
|
+
"conv-compact-usage-noop",
|
|
503
|
+
// A no-op re-counts nothing, so the single count stands as before and
|
|
504
|
+
// after.
|
|
505
|
+
[56_000],
|
|
506
|
+
);
|
|
507
|
+
const result = await conversation.forceCompact();
|
|
508
|
+
|
|
509
|
+
expect(result.previousEstimatedInputTokens).toBe(56_000);
|
|
510
|
+
expect(result.estimatedInputTokens).toBe(56_000);
|
|
511
|
+
|
|
512
|
+
const usage = collected.filter((m) => m.type === "context_window_usage");
|
|
513
|
+
expect(usage.length).toBe(1);
|
|
514
|
+
const usageEvent = usage[0] as Extract<
|
|
515
|
+
AssistantEvent,
|
|
516
|
+
{ type: "context_window_usage" }
|
|
517
|
+
>;
|
|
518
|
+
expect(usageEvent.tokens).toBe(56_000);
|
|
519
|
+
expect(usageEvent.maxTokens).toBe(200_000);
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
|
|
386
523
|
describe("maybeCompact gate sizing", () => {
|
|
387
524
|
test("default sizing resolves mainAgent's window; wake sizing resolves the wake's call-site window", async () => {
|
|
388
525
|
// The auto-threshold gate derives its trip point from the
|
|
@@ -864,7 +864,7 @@ describe("loadConfig startup behavior", () => {
|
|
|
864
864
|
expect(effective["quality-optimized"]?.provider).toBe("openai");
|
|
865
865
|
expect(effective["quality-optimized"]?.model).toBe("gpt-5.4");
|
|
866
866
|
expect(effective["cost-optimized"]?.provider).toBe("openai");
|
|
867
|
-
expect(effective["cost-optimized"]?.model).toBe("gpt-5.
|
|
867
|
+
expect(effective["cost-optimized"]?.model).toBe("gpt-5.4-nano");
|
|
868
868
|
});
|
|
869
869
|
|
|
870
870
|
test("off-platform hatch with a provider outside the named matrix columns resolves through the shared BYOK templates", () => {
|
|
@@ -1471,7 +1471,7 @@ describe("seedInferenceProfiles BYOK-mode default profiles", () => {
|
|
|
1471
1471
|
);
|
|
1472
1472
|
expect(effective.balanced?.label).toBe("Balanced");
|
|
1473
1473
|
expect(effective["quality-optimized"]?.label).toBe("Quality");
|
|
1474
|
-
expect(effective["cost-optimized"]?.label).toBe("
|
|
1474
|
+
expect(effective["cost-optimized"]?.label).toBe("Cost");
|
|
1475
1475
|
expect("status" in (effective.balanced ?? {})).toBe(false);
|
|
1476
1476
|
expect("status" in (effective["quality-optimized"] ?? {})).toBe(false);
|
|
1477
1477
|
expect("status" in (effective["cost-optimized"] ?? {})).toBe(false);
|
|
@@ -1741,7 +1741,39 @@ describe("seedInferenceProfiles BYOK-mode default profiles", () => {
|
|
|
1741
1741
|
const effective = getEffectiveProfiles(raw.llm.profiles);
|
|
1742
1742
|
expect(effective.balanced?.label).toBe("Balanced");
|
|
1743
1743
|
expect(effective["quality-optimized"]?.label).toBe("Quality");
|
|
1744
|
-
expect(effective["cost-optimized"]?.label).toBe("
|
|
1744
|
+
expect(effective["cost-optimized"]?.label).toBe("Cost");
|
|
1745
|
+
});
|
|
1746
|
+
|
|
1747
|
+
test("a managed key missing from profileOrder lands beside its siblings", () => {
|
|
1748
|
+
// An install that predates a newly shipped managed profile, with a custom
|
|
1749
|
+
// profile already in its order. The new key belongs with the other
|
|
1750
|
+
// managed profiles, not after whatever the user has arranged below them.
|
|
1751
|
+
writeConfig({
|
|
1752
|
+
llm: {
|
|
1753
|
+
default: { provider: "anthropic", model: "claude-opus-4-7" },
|
|
1754
|
+
profiles: {
|
|
1755
|
+
"my-custom": { source: "user", provider: "anthropic", model: "x" },
|
|
1756
|
+
},
|
|
1757
|
+
profileOrder: [
|
|
1758
|
+
"balanced",
|
|
1759
|
+
"quality-optimized",
|
|
1760
|
+
"cost-optimized",
|
|
1761
|
+
"my-custom",
|
|
1762
|
+
],
|
|
1763
|
+
activeProfile: "balanced",
|
|
1764
|
+
},
|
|
1765
|
+
});
|
|
1766
|
+
|
|
1767
|
+
mergeDefaultConfigAndSeedInferenceProfiles();
|
|
1768
|
+
const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
1769
|
+
|
|
1770
|
+
expect(raw.llm.profileOrder).toEqual([
|
|
1771
|
+
"balanced",
|
|
1772
|
+
"quality-optimized",
|
|
1773
|
+
"cost-optimized",
|
|
1774
|
+
"latency-optimized",
|
|
1775
|
+
"my-custom",
|
|
1776
|
+
]);
|
|
1745
1777
|
});
|
|
1746
1778
|
|
|
1747
1779
|
test("boot leaves legacy bare labels on managed entries untouched", () => {
|
|
@@ -747,6 +747,7 @@ describe("AssistantConfigSchema", () => {
|
|
|
747
747
|
failureBackoffBaseMs: 2000,
|
|
748
748
|
failureBackoffMaxMs: 60000,
|
|
749
749
|
maxFileSizeBytes: 256000,
|
|
750
|
+
historyCompaction: { enabled: true },
|
|
750
751
|
interactiveGitTimeoutMs: 10000,
|
|
751
752
|
enrichmentQueueSize: 50,
|
|
752
753
|
enrichmentConcurrency: 1,
|
|
@@ -215,3 +215,33 @@ describe("runAgentLoopImpl fatal-failure cleanup (ATL-1009)", () => {
|
|
|
215
215
|
expect(drainQueue).toHaveBeenCalledWith("loop_complete");
|
|
216
216
|
});
|
|
217
217
|
});
|
|
218
|
+
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// Turn-scoped view state
|
|
221
|
+
//
|
|
222
|
+
// The app the client has on screen rides in on a message's transport. Nothing
|
|
223
|
+
// tells the daemon when the user closes it, so leaving the value on the
|
|
224
|
+
// conversation would let a later turn with no fresh client transport (a
|
|
225
|
+
// scheduled wake, a background follow-up) assert the user is looking at an app
|
|
226
|
+
// they closed. This suite reuses the loop harness above because the clear
|
|
227
|
+
// lives in the loop's per-turn teardown.
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
describe("runAgentLoopImpl per-turn view state", () => {
|
|
231
|
+
test("clears the reported visible app at turn end so a later turn cannot inherit it", async () => {
|
|
232
|
+
const events: AssistantEvent[] = [];
|
|
233
|
+
const ctx = makeCtx({
|
|
234
|
+
visibleAppId: "app-on-screen",
|
|
235
|
+
commitTurnChanges: mock(
|
|
236
|
+
async () => {},
|
|
237
|
+
) as unknown as Context["commitTurnChanges"],
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
await runAgentLoopImpl(ctx, "background task", "msg-visible-app", (event) =>
|
|
241
|
+
events.push(event),
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
expect(ctx.visibleAppId).toBeUndefined();
|
|
245
|
+
expect(ctx.currentTurnVisibleAppId).toBeUndefined();
|
|
246
|
+
});
|
|
247
|
+
});
|
|
@@ -349,6 +349,7 @@ mock.module("../persistence/conversation-crud.js", () => ({
|
|
|
349
349
|
finalizeMessageContent: finalizeMessageContentMock,
|
|
350
350
|
recordConversationPersistedSeq: () => {},
|
|
351
351
|
getConversationPersistedSeq: () => null,
|
|
352
|
+
PROVIDER_ERROR_MESSAGE_KIND: "provider_error",
|
|
352
353
|
// The real schema is a Zod object; tests don't exercise validation,
|
|
353
354
|
// so a passthrough is sufficient — the production code at
|
|
354
355
|
// `handleMessageComplete` only branches on `success` and reads two
|
|
@@ -379,6 +380,11 @@ mock.module("../runtime/sync/sync-publisher.js", () => ({
|
|
|
379
380
|
publishSyncInvalidation: publishSyncInvalidationMock,
|
|
380
381
|
}));
|
|
381
382
|
|
|
383
|
+
const emitAssistantReplyNotificationMock = mock(async () => {});
|
|
384
|
+
mock.module("../notifications/assistant-reply-producer.js", () => ({
|
|
385
|
+
emitAssistantReplyNotification: emitAssistantReplyNotificationMock,
|
|
386
|
+
}));
|
|
387
|
+
|
|
382
388
|
afterAll(() => {
|
|
383
389
|
mock.module(
|
|
384
390
|
"../persistence/conversation-crud.js",
|
|
@@ -895,6 +901,7 @@ beforeEach(() => {
|
|
|
895
901
|
setAgentLoopExitReasonOnLatestLogMock.mockClear();
|
|
896
902
|
syncMessageToDiskMock.mockClear();
|
|
897
903
|
rebuildConversationDiskViewFromDbStateMock.mockClear();
|
|
904
|
+
emitAssistantReplyNotificationMock.mockClear();
|
|
898
905
|
updateMessageMetadataMock.mockClear();
|
|
899
906
|
updateMessageMetadataMock.mockImplementation(() => {});
|
|
900
907
|
updateConversationSlackContextWatermarkMock.mockClear();
|
|
@@ -2227,6 +2234,49 @@ describe("session-agent-loop", () => {
|
|
|
2227
2234
|
expect(backfillCall[1]).toBe("mock-msg-id");
|
|
2228
2235
|
});
|
|
2229
2236
|
|
|
2237
|
+
test("stamps provider-error metadata onto the persisted synthetic assistant row", async () => {
|
|
2238
|
+
mockConversationErrorClassification = {
|
|
2239
|
+
code: "PROVIDER_BILLING",
|
|
2240
|
+
userMessage:
|
|
2241
|
+
"You're out of credits. Add credits in Settings → Billing to continue.",
|
|
2242
|
+
retryable: false,
|
|
2243
|
+
errorCategory: "credits_exhausted",
|
|
2244
|
+
};
|
|
2245
|
+
|
|
2246
|
+
const ctx = makeCtx({
|
|
2247
|
+
loopProvider: {
|
|
2248
|
+
name: "mock-provider",
|
|
2249
|
+
async sendMessage() {
|
|
2250
|
+
throw new Error("Payment Required");
|
|
2251
|
+
},
|
|
2252
|
+
} as unknown as Provider,
|
|
2253
|
+
});
|
|
2254
|
+
await runAgentLoopImpl(ctx, "hello", "msg-1", () => {});
|
|
2255
|
+
|
|
2256
|
+
expect(addMessageMock).toHaveBeenCalledTimes(1);
|
|
2257
|
+
const addCall = addMessageMock.mock.calls[0] as unknown as [
|
|
2258
|
+
string,
|
|
2259
|
+
string,
|
|
2260
|
+
string,
|
|
2261
|
+
{ metadata?: Record<string, unknown> },
|
|
2262
|
+
];
|
|
2263
|
+
expect(addCall[1]).toBe("assistant");
|
|
2264
|
+
// The persisted row swaps the context-neutral classification copy for
|
|
2265
|
+
// assistant-voice wording, since this turn truly ends with no reply.
|
|
2266
|
+
const persistedBlocks = JSON.parse(addCall[2]) as Array<{
|
|
2267
|
+
type: string;
|
|
2268
|
+
text: string;
|
|
2269
|
+
}>;
|
|
2270
|
+
expect(persistedBlocks[0]?.text).toBe(
|
|
2271
|
+
"I couldn't reply because you ran out of credits. Add credits in Settings → Billing and we can pick up where we left off.",
|
|
2272
|
+
);
|
|
2273
|
+
expect(addCall[3]?.metadata).toMatchObject({
|
|
2274
|
+
messageKind: "provider_error",
|
|
2275
|
+
providerErrorCode: "PROVIDER_BILLING",
|
|
2276
|
+
providerErrorCategory: "credits_exhausted",
|
|
2277
|
+
});
|
|
2278
|
+
});
|
|
2279
|
+
|
|
2230
2280
|
test("does not persist managed credential refresh failures as assistant text", async () => {
|
|
2231
2281
|
mockConversationErrorClassification = {
|
|
2232
2282
|
code: "MANAGED_KEY_INVALID",
|
|
@@ -2269,6 +2319,120 @@ describe("session-agent-loop", () => {
|
|
|
2269
2319
|
];
|
|
2270
2320
|
expect(deleteCall[0]).toBe("msg-reserve");
|
|
2271
2321
|
});
|
|
2322
|
+
|
|
2323
|
+
test("does not push the synthetic error row as a reply notification", async () => {
|
|
2324
|
+
// The synthetic row carries the provider's error text. It reaches the
|
|
2325
|
+
// `message_complete` branch like any other turn, so without the
|
|
2326
|
+
// provider-error guard the tail would send that text to the lock screen
|
|
2327
|
+
// as if it were the assistant's reply.
|
|
2328
|
+
const ctx = makeCtx({
|
|
2329
|
+
loopProvider: {
|
|
2330
|
+
name: "mock-provider",
|
|
2331
|
+
async sendMessage() {
|
|
2332
|
+
throw new Error("upstream 500");
|
|
2333
|
+
},
|
|
2334
|
+
} as unknown as Provider,
|
|
2335
|
+
});
|
|
2336
|
+
await runAgentLoopImpl(ctx, "hello", "msg-1", () => {});
|
|
2337
|
+
|
|
2338
|
+
expect(addMessageMock).toHaveBeenCalled();
|
|
2339
|
+
expect(emitAssistantReplyNotificationMock).not.toHaveBeenCalled();
|
|
2340
|
+
});
|
|
2341
|
+
});
|
|
2342
|
+
|
|
2343
|
+
describe("assistant-reply notification wiring", () => {
|
|
2344
|
+
test("a completed turn notifies with the row that opened it", async () => {
|
|
2345
|
+
mockMessageById = {
|
|
2346
|
+
id: "msg-reserve",
|
|
2347
|
+
conversationId: "test-conv",
|
|
2348
|
+
createdAt: 1234567,
|
|
2349
|
+
role: "assistant",
|
|
2350
|
+
content: "[]",
|
|
2351
|
+
metadata: null,
|
|
2352
|
+
};
|
|
2353
|
+
|
|
2354
|
+
const ctx = makeCtx({
|
|
2355
|
+
providerResponses: [textResponse("here you go")],
|
|
2356
|
+
});
|
|
2357
|
+
await runAgentLoopImpl(ctx, "hi", "msg-user-7", () => {});
|
|
2358
|
+
|
|
2359
|
+
expect(emitAssistantReplyNotificationMock).toHaveBeenCalledTimes(1);
|
|
2360
|
+
const notifyCall = emitAssistantReplyNotificationMock.mock
|
|
2361
|
+
.calls[0] as unknown as [
|
|
2362
|
+
{ conversationId: string; userMessageId: string | undefined },
|
|
2363
|
+
];
|
|
2364
|
+
expect(notifyCall[0]).toMatchObject({
|
|
2365
|
+
conversationId: "test-conv",
|
|
2366
|
+
userMessageId: "msg-user-7",
|
|
2367
|
+
});
|
|
2368
|
+
});
|
|
2369
|
+
|
|
2370
|
+
test("a caller-supplied notify row overrides the turn's user message id", async () => {
|
|
2371
|
+
mockMessageById = {
|
|
2372
|
+
id: "msg-reserve",
|
|
2373
|
+
conversationId: "test-conv",
|
|
2374
|
+
createdAt: 1234567,
|
|
2375
|
+
role: "assistant",
|
|
2376
|
+
content: "[]",
|
|
2377
|
+
metadata: null,
|
|
2378
|
+
};
|
|
2379
|
+
|
|
2380
|
+
const ctx = makeCtx({
|
|
2381
|
+
providerResponses: [textResponse("here you go")],
|
|
2382
|
+
});
|
|
2383
|
+
await runAgentLoopImpl(ctx, "hi", "msg-user-tail", () => {}, {
|
|
2384
|
+
notifyUserMessageId: "msg-user-prompt",
|
|
2385
|
+
});
|
|
2386
|
+
|
|
2387
|
+
expect(emitAssistantReplyNotificationMock).toHaveBeenCalledTimes(1);
|
|
2388
|
+
const notifyCall = emitAssistantReplyNotificationMock.mock
|
|
2389
|
+
.calls[0] as unknown as [{ userMessageId: string | undefined }];
|
|
2390
|
+
expect(notifyCall[0].userMessageId).toBe("msg-user-prompt");
|
|
2391
|
+
});
|
|
2392
|
+
|
|
2393
|
+
test("threads the app-only delivery marker to the producer", async () => {
|
|
2394
|
+
mockMessageById = {
|
|
2395
|
+
id: "msg-reserve",
|
|
2396
|
+
conversationId: "test-conv",
|
|
2397
|
+
createdAt: 1234567,
|
|
2398
|
+
role: "assistant",
|
|
2399
|
+
content: "[]",
|
|
2400
|
+
metadata: null,
|
|
2401
|
+
};
|
|
2402
|
+
|
|
2403
|
+
const ctx = makeCtx({
|
|
2404
|
+
providerResponses: [textResponse("here you go")],
|
|
2405
|
+
});
|
|
2406
|
+
await runAgentLoopImpl(ctx, "hi", "msg-user-8", () => {}, {
|
|
2407
|
+
replyDeliveredInAppOnly: true,
|
|
2408
|
+
});
|
|
2409
|
+
|
|
2410
|
+
expect(emitAssistantReplyNotificationMock).toHaveBeenCalledTimes(1);
|
|
2411
|
+
const notifyCall = emitAssistantReplyNotificationMock.mock
|
|
2412
|
+
.calls[0] as unknown as [{ replyDeliveredInAppOnly?: boolean }];
|
|
2413
|
+
expect(notifyCall[0].replyDeliveredInAppOnly).toBe(true);
|
|
2414
|
+
});
|
|
2415
|
+
|
|
2416
|
+
test("omits the app-only delivery marker for an ordinary turn", async () => {
|
|
2417
|
+
mockMessageById = {
|
|
2418
|
+
id: "msg-reserve",
|
|
2419
|
+
conversationId: "test-conv",
|
|
2420
|
+
createdAt: 1234567,
|
|
2421
|
+
role: "assistant",
|
|
2422
|
+
content: "[]",
|
|
2423
|
+
metadata: null,
|
|
2424
|
+
};
|
|
2425
|
+
|
|
2426
|
+
const ctx = makeCtx({
|
|
2427
|
+
providerResponses: [textResponse("here you go")],
|
|
2428
|
+
});
|
|
2429
|
+
await runAgentLoopImpl(ctx, "hi", "msg-user-9", () => {});
|
|
2430
|
+
|
|
2431
|
+
expect(emitAssistantReplyNotificationMock).toHaveBeenCalledTimes(1);
|
|
2432
|
+
const notifyCall = emitAssistantReplyNotificationMock.mock
|
|
2433
|
+
.calls[0] as unknown as [Record<string, unknown>];
|
|
2434
|
+
expect(notifyCall[0]).not.toHaveProperty("replyDeliveredInAppOnly");
|
|
2435
|
+
});
|
|
2272
2436
|
});
|
|
2273
2437
|
|
|
2274
2438
|
describe("B3 pre-allocation: indexing + cleanup", () => {
|
|
@@ -2858,12 +3022,20 @@ describe("session-agent-loop", () => {
|
|
|
2858
3022
|
}));
|
|
2859
3023
|
|
|
2860
3024
|
// GIVEN a real loop whose provider streams a delta — landing a debounced
|
|
2861
|
-
// partial flush on the reserved row — then rejects
|
|
2862
|
-
//
|
|
3025
|
+
// partial flush on the reserved row — then rejects. The loop resumes a
|
|
3026
|
+
// call that died after it had started streaming, so the second attempt
|
|
3027
|
+
// is scripted to fail before streaming anything: that keeps exactly one
|
|
3028
|
+
// row carrying partial content, and the turn then exits via
|
|
3029
|
+
// `provider_error` with no `message_complete`.
|
|
3030
|
+
let attempt = 0;
|
|
2863
3031
|
const ctx = makeCtx({
|
|
2864
3032
|
loopProvider: {
|
|
2865
3033
|
name: "mock-provider",
|
|
2866
3034
|
async sendMessage(_messages, options) {
|
|
3035
|
+
attempt++;
|
|
3036
|
+
if (attempt > 1) {
|
|
3037
|
+
throw new Error("upstream 500");
|
|
3038
|
+
}
|
|
2867
3039
|
options?.onEvent?.({ type: "text_delta", text: "hello world" });
|
|
2868
3040
|
await new Promise((resolve) => setTimeout(resolve, 1100));
|
|
2869
3041
|
throw new Error("upstream 500");
|
|
@@ -2887,11 +3059,12 @@ describe("session-agent-loop", () => {
|
|
|
2887
3059
|
.split("\n");
|
|
2888
3060
|
expect(orphanLines).toHaveLength(1);
|
|
2889
3061
|
expect(updateMessageContentMock).toHaveBeenCalledTimes(0);
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
3062
|
+
// Scoped to the row under test: the resumed attempt reserves a row of
|
|
3063
|
+
// its own, which its own cleanup deletes.
|
|
3064
|
+
const deletedIds = deleteMessageByIdMock.mock.calls.map(
|
|
3065
|
+
(call) => (call as unknown as [string])[0],
|
|
3066
|
+
);
|
|
3067
|
+
expect(deletedIds).toContain("msg-orphan-with-partial");
|
|
2895
3068
|
});
|
|
2896
3069
|
});
|
|
2897
3070
|
|