@vellumai/assistant 0.12.4 → 0.12.5-dev.202609251017.e467a6c
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/docs/architecture/memory.md +9 -1
- package/docs/architecture/scheduling.md +13 -5
- package/docs/desktop-browser-cli.md +3 -2
- package/docs/guardian-request-flow.md +1 -1
- package/node_modules/@vellumai/avatar-manifest/src/__tests__/notification-avatar.test.ts +48 -4
- package/node_modules/@vellumai/avatar-manifest/src/notification-avatar.ts +12 -8
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/guardian-requests.test.ts +26 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +77 -3
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/guardian-requests.test.ts +26 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +77 -3
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +2 -4
- package/node_modules/@vellumai/service-contracts/src/__tests__/guardian-requests.test.ts +26 -0
- package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +77 -3
- package/openapi.yaml +152 -3
- package/package.json +1 -1
- package/src/__tests__/agent-loop.test.ts +76 -0
- package/src/__tests__/anthropic-error-formatting.test.ts +9 -0
- package/src/__tests__/background-shell-bash.test.ts +34 -0
- package/src/__tests__/background-shell-host-bash.test.ts +98 -1
- package/src/__tests__/background-tool-registry.test.ts +39 -0
- package/src/__tests__/channel-readiness-discord.test.ts +8 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +8 -0
- package/src/__tests__/client-os-metadata-persistence.test.ts +4 -20
- package/src/__tests__/conversation-agent-loop.test.ts +529 -1
- package/src/__tests__/conversation-error.test.ts +18 -0
- package/src/__tests__/conversation-pairing.test.ts +69 -0
- package/src/__tests__/conversation-queue.test.ts +257 -4
- package/src/__tests__/conversation-runtime-assembly.test.ts +27 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +62 -0
- package/src/__tests__/evict-conversations-for-reload.test.ts +21 -0
- package/src/__tests__/gemini-provider.test.ts +9 -0
- package/src/__tests__/host-cu-proxy.test.ts +79 -0
- package/src/__tests__/host-cu-routes-targeted.test.ts +14 -5
- package/src/__tests__/introduction-card-resolver.test.ts +53 -8
- package/src/__tests__/job-handler-registration-guard.test.ts +1 -0
- package/src/__tests__/non-member-access-request.test.ts +48 -0
- package/src/__tests__/notification-events-source-context-probe.test.ts +48 -1
- package/src/__tests__/notification-platform-adapter.test.ts +97 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +103 -3
- package/src/__tests__/platform-callback-registration.test.ts +46 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +116 -66
- package/src/__tests__/scheduler-disk-pressure.test.ts +1 -0
- package/src/__tests__/scheduler-result-notification.test.ts +521 -7
- package/src/__tests__/scheduler-wake.test.ts +1 -0
- package/src/__tests__/skill-load-inline-command.test.ts +35 -0
- package/src/__tests__/subagent-cron-run-attribution.test.ts +41 -2
- package/src/__tests__/subagent-manager-notify.test.ts +48 -0
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +6 -6
- package/src/__tests__/usage-attribution.test.ts +31 -0
- package/src/__tests__/wake-schedule-escalation.test.ts +1 -0
- package/src/agent/loop.ts +23 -0
- package/src/api/ask-question-tool.ts +50 -0
- package/src/api/events/generation-handoff.ts +4 -0
- package/src/api/events/message-complete.ts +4 -0
- package/src/api/events/tool-result.ts +11 -0
- package/src/api/index.ts +11 -0
- package/src/api/requests/guardian-actions.test.ts +45 -0
- package/src/api/requests/guardian-actions.ts +20 -0
- package/src/api/responses/conversation-message.ts +4 -0
- package/src/api/responses/home.ts +27 -0
- package/src/approvals/guardian-decision-primitive.ts +6 -4
- package/src/approvals/guardian-request-resolvers.ts +35 -30
- package/src/avatar/notification-avatar.test.ts +42 -34
- package/src/avatar/notification-avatar.ts +1 -0
- package/src/calls/__tests__/call-controller.test.ts +18 -0
- package/src/calls/__tests__/voice-control-protocol.test.ts +6 -1
- package/src/calls/__tests__/voice-leg-coordinator.test.ts +70 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +116 -4
- package/src/calls/__tests__/voice-triage-escalate.test.ts +77 -11
- package/src/calls/call-controller.ts +3 -1
- package/src/calls/voice-control-protocol.ts +12 -4
- package/src/calls/voice-escalation-judge.ts +2 -2
- package/src/calls/voice-leg-coordinator.ts +23 -5
- package/src/calls/voice-screen-annotation.ts +44 -0
- package/src/calls/voice-session-bridge.ts +63 -20
- package/src/calls/voice-triage-escalate.ts +82 -27
- package/src/cli/commands/__tests__/conversations-clear.test.ts +156 -0
- package/src/cli/commands/conversations.help.ts +7 -2
- package/src/cli/commands/conversations.ts +36 -30
- package/src/cli/commands/oauth/index.help.ts +8 -2
- package/src/cli/commands/oauth/request.test.ts +32 -0
- package/src/cli/commands/platform/index.help.ts +2 -1
- package/src/cli/commands/platform/index.ts +8 -2
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +60 -0
- package/src/cli/lib/__tests__/plugin-catalog-platform.test.ts +66 -2
- package/src/cli/lib/bundled-marketplace.json +35 -0
- package/src/cli/lib/bundled-plugin-packages.json +21 -0
- package/src/cli/lib/plugin-catalog-cache.ts +17 -2
- package/src/cli/lib/plugin-catalog-platform.ts +48 -9
- package/src/config/__tests__/default-profile-catalog.test.ts +28 -0
- package/src/config/__tests__/platform-identity.test.ts +59 -0
- package/src/config/__tests__/profile-order.test.ts +19 -0
- package/src/config/__tests__/sync-gated-profiles.test.ts +71 -0
- package/src/config/bundled-skills/computer-use/SKILL.md +20 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +11 -11
- package/src/config/bundled-skills/screen-annotation/SKILL.md +41 -29
- package/src/config/bundled-skills/screen-annotation/TOOLS.json +5 -5
- package/src/config/call-site-defaults.ts +6 -0
- package/src/config/default-profile-catalog.ts +30 -1
- package/src/config/default-profile-names.ts +36 -2
- package/src/config/feature-flag-registry.json +9 -1
- package/src/config/platform-identity.ts +22 -5
- package/src/config/profile-order.ts +7 -1
- package/src/config/schemas/call-site-catalog.ts +7 -0
- package/src/config/schemas/llm.ts +21 -5
- package/src/config/sync-gated-profiles.ts +96 -44
- package/src/daemon/__tests__/abort-null-controller.test.ts +135 -1
- package/src/daemon/__tests__/auto-profile-router.test.ts +380 -0
- package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +16 -0
- package/src/daemon/activity-trail.test.ts +206 -0
- package/src/daemon/activity-trail.ts +190 -0
- package/src/daemon/auto-profile-router.ts +431 -0
- package/src/daemon/computer-use-observation.test.ts +81 -0
- package/src/daemon/computer-use-observation.ts +23 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +11 -0
- package/src/daemon/conversation-agent-loop.ts +157 -32
- package/src/daemon/conversation-error.ts +19 -0
- package/src/daemon/conversation-lifecycle.ts +70 -7
- package/src/daemon/conversation-media-retry.ts +1 -2
- package/src/daemon/conversation-messaging.ts +31 -2
- package/src/daemon/conversation-process.ts +107 -16
- package/src/daemon/conversation-queue-manager.ts +7 -0
- package/src/daemon/conversation-runtime-assembly.ts +1 -0
- package/src/daemon/conversation-turn-finalize.ts +14 -0
- package/src/daemon/conversation.ts +25 -6
- package/src/daemon/event-loop-watchdog.test.ts +201 -2
- package/src/daemon/event-loop-watchdog.ts +182 -6
- package/src/daemon/host-cu-proxy.ts +34 -3
- package/src/daemon/pkb-reminder-builder.test.ts +10 -3
- package/src/daemon/pkb-reminder-builder.ts +1 -1
- package/src/daemon/wake-conversation-ops.ts +4 -2
- package/src/desktop/desktop-computer-use.test.ts +14 -4
- package/src/home/feed-types.ts +2 -0
- package/src/inbound/platform-callback-registration.ts +19 -6
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +27 -0
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +83 -0
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +48 -1
- package/src/live-voice/__tests__/live-voice-vad.test.ts +34 -2
- package/src/live-voice/__tests__/protocol.test.ts +22 -0
- package/src/live-voice/continuation-judge.ts +1 -1
- package/src/live-voice/live-voice-session.ts +25 -19
- package/src/live-voice/protocol.ts +16 -5
- package/src/messaging/providers/discord/withdraw.ts +4 -5
- package/src/messaging/providers/gmail/client.ts +4 -0
- package/src/messaging/providers/slack/withdraw.ts +5 -5
- package/src/messaging/providers/telegram-bot/withdraw.ts +4 -5
- package/src/monitoring/__tests__/proc-fixtures.ts +28 -0
- package/src/monitoring/__tests__/proc-wait-state.test.ts +232 -0
- package/src/monitoring/__tests__/process-usage.test.ts +243 -0
- package/src/monitoring/proc-wait-state.ts +366 -0
- package/src/monitoring/process-memory.ts +4 -3
- package/src/monitoring/process-usage.ts +215 -0
- package/src/monitoring/resource-sample-types.ts +8 -0
- package/src/monitoring/resource-sampler.ts +17 -2
- package/src/monitoring/stall-capture.ts +12 -0
- package/src/notifications/README.md +88 -9
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +390 -119
- package/src/notifications/__tests__/background-result-producer.test.ts +1272 -0
- package/src/notifications/__tests__/broadcaster.test.ts +292 -1
- package/src/notifications/__tests__/completion-policy.test.ts +243 -0
- package/src/notifications/__tests__/decision-engine.test.ts +51 -4
- package/src/notifications/__tests__/has-pending-background-work.test.ts +281 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +161 -3
- package/src/notifications/__tests__/resolve-visible-in-source.test.ts +110 -1
- package/src/notifications/__tests__/schedule-result-producer.test.ts +11 -4
- package/src/notifications/adapters/macos.ts +24 -12
- package/src/notifications/adapters/platform.ts +16 -5
- package/src/notifications/assistant-reply-producer.ts +96 -71
- package/src/notifications/background-result-producer.ts +369 -0
- package/src/notifications/broadcaster.ts +28 -8
- package/src/notifications/completion-policy.ts +115 -0
- package/src/notifications/completion-work.ts +46 -0
- package/src/notifications/conversation-pairing.ts +32 -13
- package/src/notifications/decision-engine.ts +18 -24
- package/src/notifications/emit-signal.ts +16 -1
- package/src/notifications/events-store.ts +4 -0
- package/src/notifications/has-pending-background-work.ts +91 -0
- package/src/notifications/home-feed-mirror.ts +44 -0
- package/src/notifications/home-feed-side-effect.ts +54 -28
- package/src/notifications/resolve-visible-in-source.ts +56 -2
- package/src/notifications/result-output.ts +154 -0
- package/src/notifications/schedule-result-producer.ts +6 -187
- package/src/notifications/types.ts +2 -0
- package/src/oauth/AGENTS.md +9 -1
- package/src/oauth/byo-connection.test.ts +28 -0
- package/src/persistence/__tests__/recent-conversation-messages.test.ts +41 -0
- package/src/persistence/conversation-crud.ts +26 -0
- package/src/persistence/conversation-types.ts +15 -37
- package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +85 -14
- package/src/persistence/jobs-store.ts +229 -0
- package/src/persistence/migrations/383-normalize-opencode-host-connections.ts +44 -0
- package/src/persistence/migrations/__tests__/383-normalize-opencode-host-connections.test.ts +102 -0
- package/src/persistence/steps.ts +12 -0
- package/src/platform/sync-avatar.test.ts +29 -0
- package/src/plugins/defaults/memory/AGENTS.md +5 -3
- package/src/plugins/defaults/memory/__tests__/skill-update-receipt-boundary-guard.test.ts +45 -0
- package/src/plugins/defaults/memory/__tests__/skill-update-receipt-job.test.ts +580 -0
- package/src/plugins/defaults/memory/__tests__/turn-retrieval.test.ts +21 -0
- package/src/plugins/defaults/memory/graph/tools.ts +9 -5
- package/src/plugins/defaults/memory/hooks/conversation-deleted.ts +4 -0
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +7 -3
- package/src/plugins/defaults/memory/job-handlers.ts +8 -2
- package/src/plugins/defaults/memory/skill-update-receipt-job.ts +334 -0
- package/src/plugins/defaults/memory/tools.test.ts +17 -0
- package/src/plugins/defaults/memory/turn-retrieval.ts +8 -0
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +24 -0
- package/src/plugins/defaults/memory/v3/injector.ts +4 -4
- package/src/plugins/types.ts +2 -0
- package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
- package/src/providers/anthropic/client.ts +4 -0
- package/src/providers/gemini/client.ts +7 -1
- package/src/providers/inference/auth.ts +25 -0
- package/src/providers/inference/profile-probe.ts +1 -0
- package/src/{calls/__tests__/typesafe-noul.test.ts → providers/jev/ask.test.ts} +4 -4
- package/src/{calls/typesafe-noul.ts → providers/jev/ask.ts} +67 -34
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +12 -0
- package/src/providers/openai/api-error-normalization.ts +4 -0
- package/src/providers/retry.ts +2 -0
- package/src/providers/search-provider-catalog.ts +10 -0
- package/src/providers/types.ts +7 -0
- package/src/providers/usage-tracking.ts +5 -3
- package/src/runtime/__tests__/agent-wake.test.ts +308 -0
- package/src/runtime/access-request-helper.ts +16 -9
- package/src/runtime/agent-wake-queue.ts +52 -0
- package/src/runtime/agent-wake.ts +111 -18
- package/src/runtime/channel-approval-types.ts +6 -38
- package/src/runtime/channel-approvals.ts +4 -5
- package/src/runtime/http-server.ts +4 -1
- package/src/runtime/introduction-policy.test.ts +17 -1
- package/src/runtime/introduction-policy.ts +25 -7
- package/src/runtime/migrations/vbundle-builder.ts +9 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +21 -1
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +60 -0
- package/src/runtime/routes/__tests__/oauth-proxy-routes.test.ts +61 -3
- package/src/runtime/routes/auto-profile-routes.ts +83 -0
- package/src/runtime/routes/conversation-management-routes.ts +1 -3
- package/src/runtime/routes/conversation-routes.ts +9 -0
- package/src/runtime/routes/guardian-action-routes.ts +8 -23
- package/src/runtime/routes/host-cu-routes.ts +3 -1
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-profiles-routes.ts +5 -2
- package/src/runtime/routes/inference-provider-connection-routes.ts +25 -8
- package/src/runtime/routes/migration-routes.ts +8 -2
- package/src/runtime/routes/oauth-proxy-passthrough.test.ts +32 -0
- package/src/runtime/routes/platform-routes.ts +6 -1
- package/src/runtime/routes/user-route-dispatcher.ts +1 -2
- package/src/runtime/web-presence.ts +3 -4
- package/src/schedule/schedule-store.ts +10 -0
- package/src/schedule/scheduler.ts +92 -24
- package/src/subagent/manager.ts +32 -12
- package/src/telegram/__tests__/webhook-health.test.ts +8 -0
- package/src/telemetry/telemetry-wire-source.json +1 -1
- package/src/telemetry/telemetry-wire.generated.ts +1 -0
- package/src/tools/background-tool-registry.ts +30 -3
- package/src/tools/browser/cdp-client/local-cdp-client.ts +1 -2
- package/src/tools/host-terminal/host-shell.ts +7 -0
- package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +28 -1
- package/src/tools/network/__tests__/web-fetch-metadata.test.ts +34 -0
- package/src/tools/network/__tests__/web-fetch-tinyfish.test.ts +35 -1
- package/src/tools/network/__tests__/web-search.test.ts +96 -0
- package/src/tools/network/web-fetch.ts +30 -9
- package/src/tools/network/web-search.ts +235 -4
- package/src/tools/skills/load.ts +13 -17
- package/src/tools/skills/scaffold-managed.ts +86 -71
- package/src/tools/terminal/shell-launch.test.ts +27 -1
- package/src/tools/terminal/shell.ts +4 -0
- package/src/tools/workspace-tools/loader.ts +1 -2
- package/src/usage/attribution.ts +18 -1
- package/src/util/abort-reasons.ts +2 -0
- package/src/util/errors.ts +1 -0
- package/src/util/oauth-request-body.test.ts +5 -1
- package/src/util/provider-error-patterns.ts +11 -0
- package/src/util/single-flight.test.ts +6 -0
- package/src/util/single-flight.ts +2 -0
- package/src/util/worker-pipe.test.ts +56 -10
- package/src/util/worker-pipe.ts +9 -4
- package/src/workspace/git-service.ts +6 -1
- package/src/workspace/migrations/158-repoint-opencode-host-provider-fragments.ts +163 -0
- package/src/workspace/migrations/__tests__/158-repoint-opencode-host-provider-fragments.test.ts +188 -0
- package/src/workspace/migrations/registry.ts +2 -0
|
@@ -15,11 +15,16 @@ import type { LoopToolExecutor } from "../agent/loop.js";
|
|
|
15
15
|
import type { AssistantEvent } from "../api/index.js";
|
|
16
16
|
import { stripInjectionsForCompaction } from "../context/strip-injections.js";
|
|
17
17
|
import type { AttachmentResolutionResult } from "../daemon/conversation-attachments.js";
|
|
18
|
+
import { abortScheduledRun } from "../daemon/conversation-lifecycle.js";
|
|
18
19
|
import {
|
|
19
20
|
queueConversationNotice,
|
|
20
21
|
resetConversationNoticesForTests,
|
|
21
22
|
} from "../daemon/conversation-notices.js";
|
|
23
|
+
import { MessageQueue } from "../daemon/conversation-queue-manager.js";
|
|
22
24
|
import { desktopAutomationLease } from "../desktop/desktop-automation-lease.js";
|
|
25
|
+
import type { EmitSignalParams } from "../notifications/emit-signal.js";
|
|
26
|
+
import type { AttentionState } from "../persistence/conversation-attention-store.js";
|
|
27
|
+
import type { MessageRow } from "../persistence/conversation-crud.js";
|
|
23
28
|
import { getConversationDirName } from "../persistence/conversation-directories.js";
|
|
24
29
|
import type { UserPromptSubmitContext } from "../plugin-api/types.js";
|
|
25
30
|
import { resetPluginRegistryAndRegisterDefaults } from "../plugins/defaults/index.js";
|
|
@@ -36,6 +41,7 @@ import {
|
|
|
36
41
|
type UsageAttributionInput,
|
|
37
42
|
} from "../usage/attribution.js";
|
|
38
43
|
import { createAbortReason } from "../util/abort-reasons.js";
|
|
44
|
+
import { getLogger } from "../util/logger.js";
|
|
39
45
|
import { getWorkspaceDir } from "../util/platform.js";
|
|
40
46
|
import { setConfig } from "./helpers/set-config.js";
|
|
41
47
|
|
|
@@ -98,6 +104,14 @@ function seedLlmConfig(options?: {
|
|
|
98
104
|
|
|
99
105
|
// ── Module mocks (must precede imports of the module under test) ─────
|
|
100
106
|
|
|
107
|
+
let routeAutoProfileForTest: (() => Promise<void>) | undefined;
|
|
108
|
+
mock.module("../daemon/auto-profile-router.js", () => ({
|
|
109
|
+
routeAutoProfile: async () => {
|
|
110
|
+
await routeAutoProfileForTest?.();
|
|
111
|
+
return { profile: "balanced", outcome: "fallback", latencyMs: 0 };
|
|
112
|
+
},
|
|
113
|
+
}));
|
|
114
|
+
|
|
101
115
|
// The real AgentLoop resolves the per-conversation ContextWindowManager from
|
|
102
116
|
// the compaction store keyed by conversationId. These orchestrator tests build
|
|
103
117
|
// fake conversations whose manager is a canned stub, so register each stub in a
|
|
@@ -309,6 +323,13 @@ let mockConversationRow: MockConversationRow = {
|
|
|
309
323
|
title: null,
|
|
310
324
|
};
|
|
311
325
|
let mockMessageById: Record<string, unknown> | null = null;
|
|
326
|
+
let backgroundRecoveryRows: MessageRow[] | undefined;
|
|
327
|
+
let backgroundRecoveryAttention: AttentionState | undefined;
|
|
328
|
+
let backgroundRecoveryPending = () => false;
|
|
329
|
+
const backgroundRecoverySignals: EmitSignalParams[] = [];
|
|
330
|
+
const emitBackgroundSignalMock = mock(async (signal: EmitSignalParams) => {
|
|
331
|
+
backgroundRecoverySignals.push(signal);
|
|
332
|
+
});
|
|
312
333
|
|
|
313
334
|
// The in-flight delta files the writers create for the (unmocked-path)
|
|
314
335
|
// test conversation. Files are uuid-named at reserve time, so tests locate
|
|
@@ -384,7 +405,38 @@ mock.module("../persistence/conversation-crud.js", () => ({
|
|
|
384
405
|
updateConversationSlackContextWatermarkMock,
|
|
385
406
|
updateConversationTitle: () => {},
|
|
386
407
|
getConversationOriginChannel: () => null,
|
|
387
|
-
getMessageById: () =>
|
|
408
|
+
getMessageById: (id: string) =>
|
|
409
|
+
backgroundRecoveryRows
|
|
410
|
+
? (backgroundRecoveryRows.find((row) => row.id === id) ?? null)
|
|
411
|
+
: mockMessageById,
|
|
412
|
+
getRecentConversationMessages: (
|
|
413
|
+
_conversationId: string,
|
|
414
|
+
limit: number,
|
|
415
|
+
beforeMessageId?: string,
|
|
416
|
+
) => {
|
|
417
|
+
const rows = backgroundRecoveryRows ?? [];
|
|
418
|
+
const end = beforeMessageId
|
|
419
|
+
? rows.findIndex((row) => row.id === beforeMessageId)
|
|
420
|
+
: rows.length;
|
|
421
|
+
return rows.slice(Math.max(0, end - limit), end);
|
|
422
|
+
},
|
|
423
|
+
getAssistantMessageIdsInTurn: (id: string) => {
|
|
424
|
+
const rows = backgroundRecoveryRows ?? [];
|
|
425
|
+
const ids: string[] = [];
|
|
426
|
+
for (
|
|
427
|
+
let index = rows.findIndex((row) => row.id === id);
|
|
428
|
+
index >= 0;
|
|
429
|
+
index--
|
|
430
|
+
) {
|
|
431
|
+
if (rows[index].role === "user") {
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
if (rows[index].role === "assistant") {
|
|
435
|
+
ids.unshift(rows[index].id);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return ids;
|
|
439
|
+
},
|
|
388
440
|
getLastUserTimestampBefore: () => 0,
|
|
389
441
|
reserveMessage: reserveMessageMock,
|
|
390
442
|
updateMessageContent: updateMessageContentMock,
|
|
@@ -417,11 +469,46 @@ mock.module("../plugins/defaults/memory/indexer.js", () => ({
|
|
|
417
469
|
}));
|
|
418
470
|
mock.module("../persistence/conversation-attention-store.js", () => ({
|
|
419
471
|
projectAssistantMessage: projectAssistantMessageMock,
|
|
472
|
+
getAttentionStateByConversationIds: () =>
|
|
473
|
+
new Map(
|
|
474
|
+
backgroundRecoveryAttention
|
|
475
|
+
? [["test-conv", backgroundRecoveryAttention]]
|
|
476
|
+
: [],
|
|
477
|
+
),
|
|
420
478
|
}));
|
|
421
479
|
mock.module("../runtime/sync/sync-publisher.js", () => ({
|
|
422
480
|
publishSyncInvalidation: publishSyncInvalidationMock,
|
|
423
481
|
}));
|
|
424
482
|
|
|
483
|
+
mock.module("../notifications/has-pending-background-work.js", () => ({
|
|
484
|
+
hasPendingBackgroundWork: () => backgroundRecoveryPending(),
|
|
485
|
+
}));
|
|
486
|
+
mock.module("../notifications/events-store.js", () => ({
|
|
487
|
+
hasNotifiedSourceContextSince: () => false,
|
|
488
|
+
}));
|
|
489
|
+
mock.module("../notifications/emit-signal.js", () => ({
|
|
490
|
+
emitNotificationSignal: emitBackgroundSignalMock,
|
|
491
|
+
}));
|
|
492
|
+
mock.module("../notifications/resolve-visible-in-source.js", () => ({
|
|
493
|
+
resolveCompletionRecipientPrincipalId: async () => "principal-owner",
|
|
494
|
+
resolveCompletionVisibleInSourceNow: async () => false,
|
|
495
|
+
}));
|
|
496
|
+
const {
|
|
497
|
+
emitBackgroundResultNotification: emitBackgroundResultNotificationReal,
|
|
498
|
+
} = await import("../notifications/background-result-producer.js");
|
|
499
|
+
const emitBackgroundResultNotificationMock = mock(
|
|
500
|
+
async (
|
|
501
|
+
params: Parameters<typeof emitBackgroundResultNotificationReal>[0],
|
|
502
|
+
) => {
|
|
503
|
+
if (backgroundRecoveryRows) {
|
|
504
|
+
await emitBackgroundResultNotificationReal(params);
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
);
|
|
508
|
+
mock.module("../notifications/background-result-producer.js", () => ({
|
|
509
|
+
emitBackgroundResultNotification: emitBackgroundResultNotificationMock,
|
|
510
|
+
}));
|
|
511
|
+
|
|
425
512
|
const emitAssistantReplyNotificationMock = mock(async () => {});
|
|
426
513
|
mock.module("../notifications/assistant-reply-producer.js", () => ({
|
|
427
514
|
emitAssistantReplyNotification: emitAssistantReplyNotificationMock,
|
|
@@ -1044,6 +1131,7 @@ function overflowAfterToolTurnScenario(): NonNullable<
|
|
|
1044
1131
|
// ── Tests ────────────────────────────────────────────────────────────
|
|
1045
1132
|
|
|
1046
1133
|
beforeEach(() => {
|
|
1134
|
+
routeAutoProfileForTest = undefined;
|
|
1047
1135
|
setConfig("ui", {});
|
|
1048
1136
|
seedLlmConfig();
|
|
1049
1137
|
raceWithTimeoutOutcome = "completed";
|
|
@@ -1063,6 +1151,12 @@ beforeEach(() => {
|
|
|
1063
1151
|
mockTurnReplyMessageId = undefined;
|
|
1064
1152
|
resolveTurnReplyMessageIdMock.mockClear();
|
|
1065
1153
|
emitAssistantReplyNotificationMock.mockClear();
|
|
1154
|
+
emitBackgroundResultNotificationMock.mockClear();
|
|
1155
|
+
backgroundRecoveryRows = undefined;
|
|
1156
|
+
backgroundRecoveryAttention = undefined;
|
|
1157
|
+
backgroundRecoveryPending = () => false;
|
|
1158
|
+
backgroundRecoverySignals.length = 0;
|
|
1159
|
+
emitBackgroundSignalMock.mockClear();
|
|
1066
1160
|
updateMessageMetadataMock.mockClear();
|
|
1067
1161
|
updateMessageMetadataMock.mockImplementation(() => {});
|
|
1068
1162
|
updateConversationSlackContextWatermarkMock.mockClear();
|
|
@@ -1508,6 +1602,27 @@ describe("session-agent-loop", () => {
|
|
|
1508
1602
|
});
|
|
1509
1603
|
|
|
1510
1604
|
describe("user-prompt-submit hook failures", () => {
|
|
1605
|
+
test.each([true, false, undefined])(
|
|
1606
|
+
"fresh memory policy is scoped to the turn (%s)",
|
|
1607
|
+
async (skipMemoryRetrieval) => {
|
|
1608
|
+
const ctx = makeCtx({ providerResponses: [textResponse("ok")] });
|
|
1609
|
+
ctx.currentTurnSkipMemoryRetrieval = true;
|
|
1610
|
+
const seen: Array<boolean | undefined> = [];
|
|
1611
|
+
registerPlugin({
|
|
1612
|
+
manifest: { name: "test-memory-turn-policy", version: "1.0.0" },
|
|
1613
|
+
hooks: {
|
|
1614
|
+
"user-prompt-submit": async () => {
|
|
1615
|
+
seen.push(ctx.currentTurnSkipMemoryRetrieval);
|
|
1616
|
+
},
|
|
1617
|
+
},
|
|
1618
|
+
});
|
|
1619
|
+
await runAgentLoopImpl(ctx, "circle that", "msg-1", () => {}, {
|
|
1620
|
+
skipMemoryRetrieval,
|
|
1621
|
+
});
|
|
1622
|
+
expect(seen).toEqual([skipMemoryRetrieval === true]);
|
|
1623
|
+
expect(ctx.currentTurnSkipMemoryRetrieval).toBeUndefined();
|
|
1624
|
+
},
|
|
1625
|
+
);
|
|
1511
1626
|
test("passes the effective profile to hooks even when it was already announced", async () => {
|
|
1512
1627
|
// Both profiles are complete (provider + model) so each is a usable
|
|
1513
1628
|
// winner: the conversation's pinned "balanced" must win selection over
|
|
@@ -2919,6 +3034,51 @@ describe("session-agent-loop", () => {
|
|
|
2919
3034
|
});
|
|
2920
3035
|
|
|
2921
3036
|
describe("user cancellation", () => {
|
|
3037
|
+
test("schedule timeout aborts a continuation during Auto profile setup", async () => {
|
|
3038
|
+
seedLlmConfig({
|
|
3039
|
+
profiles: {
|
|
3040
|
+
auto: {
|
|
3041
|
+
source: "managed",
|
|
3042
|
+
provider: "anthropic",
|
|
3043
|
+
model: "test-model",
|
|
3044
|
+
},
|
|
3045
|
+
},
|
|
3046
|
+
activeProfile: "auto",
|
|
3047
|
+
});
|
|
3048
|
+
const enteredRouter = Promise.withResolvers<void>();
|
|
3049
|
+
const releaseRouter = Promise.withResolvers<void>();
|
|
3050
|
+
routeAutoProfileForTest = async () => {
|
|
3051
|
+
enteredRouter.resolve();
|
|
3052
|
+
await releaseRouter.promise;
|
|
3053
|
+
};
|
|
3054
|
+
const controller = new AbortController();
|
|
3055
|
+
const provider = createMockProvider([textResponse("Should not run")]);
|
|
3056
|
+
const ctx = makeCtx({
|
|
3057
|
+
abortController: controller,
|
|
3058
|
+
loopProvider: provider.provider,
|
|
3059
|
+
queue: new MessageQueue(),
|
|
3060
|
+
prompter: { dispose: () => {} } as Conversation["prompter"],
|
|
3061
|
+
secretPrompter: { dispose: () => {} } as Conversation["secretPrompter"],
|
|
3062
|
+
accumulatedSurfaceState: new Map(),
|
|
3063
|
+
});
|
|
3064
|
+
const turn = runAgentLoopImpl(ctx, "Continue report", "msg-1", () => {}, {
|
|
3065
|
+
cronRunId: "run-scheduled",
|
|
3066
|
+
overrideProfile: "auto",
|
|
3067
|
+
});
|
|
3068
|
+
try {
|
|
3069
|
+
await enteredRouter.promise;
|
|
3070
|
+
expect(ctx.currentTurnCronRunId).toBe("run-scheduled");
|
|
3071
|
+
abortScheduledRun(ctx, "run-scheduled");
|
|
3072
|
+
expect(controller.signal.aborted).toBe(true);
|
|
3073
|
+
} finally {
|
|
3074
|
+
releaseRouter.resolve();
|
|
3075
|
+
await turn;
|
|
3076
|
+
}
|
|
3077
|
+
expect(provider.calls).toHaveLength(0);
|
|
3078
|
+
expect(ctx.isProcessing()).toBe(false);
|
|
3079
|
+
expect(ctx.currentTurnCronRunId).toBeUndefined();
|
|
3080
|
+
});
|
|
3081
|
+
|
|
2922
3082
|
test("emits generation_cancelled when abort signal fires", async () => {
|
|
2923
3083
|
const events: AssistantEvent[] = [];
|
|
2924
3084
|
const abortController = new AbortController();
|
|
@@ -3958,10 +4118,378 @@ describe("session-agent-loop", () => {
|
|
|
3958
4118
|
|
|
3959
4119
|
expect(addMessageMock).toHaveBeenCalled();
|
|
3960
4120
|
expect(emitAssistantReplyNotificationMock).not.toHaveBeenCalled();
|
|
4121
|
+
expect(emitBackgroundResultNotificationMock.mock.calls).toMatchObject([
|
|
4122
|
+
[{ recoverOnly: true, assistantMessageId: undefined }],
|
|
4123
|
+
]);
|
|
3961
4124
|
});
|
|
3962
4125
|
});
|
|
3963
4126
|
|
|
3964
4127
|
describe("assistant-reply notification wiring", () => {
|
|
4128
|
+
function seedDeferredCommandResult(options?: {
|
|
4129
|
+
privateResult?: boolean;
|
|
4130
|
+
noEarlierSuccess?: boolean;
|
|
4131
|
+
userTrigger?: boolean;
|
|
4132
|
+
}): void {
|
|
4133
|
+
const startedAt = 1_700_000_000_000;
|
|
4134
|
+
const row = (
|
|
4135
|
+
id: string,
|
|
4136
|
+
role: "user" | "assistant",
|
|
4137
|
+
text: string,
|
|
4138
|
+
offset: number,
|
|
4139
|
+
metadata?: Record<string, unknown>,
|
|
4140
|
+
): MessageRow => ({
|
|
4141
|
+
id,
|
|
4142
|
+
conversationId: "test-conv",
|
|
4143
|
+
role,
|
|
4144
|
+
content: [{ type: "text", text }],
|
|
4145
|
+
createdAt: startedAt + offset,
|
|
4146
|
+
metadata: metadata ? JSON.stringify(metadata) : null,
|
|
4147
|
+
clientMessageId: null,
|
|
4148
|
+
finalized: 1,
|
|
4149
|
+
});
|
|
4150
|
+
backgroundRecoveryRows = [
|
|
4151
|
+
row("prior-trigger", "user", "INTERNAL COMMAND RESULT", 1, {
|
|
4152
|
+
backgroundEventSource: "background-tool",
|
|
4153
|
+
backgroundToolCompletion: {
|
|
4154
|
+
id: "tool-success",
|
|
4155
|
+
toolName: "bash",
|
|
4156
|
+
conversationId: "test-conv",
|
|
4157
|
+
command: "example-command",
|
|
4158
|
+
startedAt,
|
|
4159
|
+
completedAt: startedAt + 1,
|
|
4160
|
+
status: options?.noEarlierSuccess ? "failed" : "completed",
|
|
4161
|
+
exitCode: options?.noEarlierSuccess ? 1 : 0,
|
|
4162
|
+
output: "raw command output",
|
|
4163
|
+
},
|
|
4164
|
+
}),
|
|
4165
|
+
row(
|
|
4166
|
+
"prior-result",
|
|
4167
|
+
"assistant",
|
|
4168
|
+
"The requested report is ready.",
|
|
4169
|
+
2,
|
|
4170
|
+
options?.privateResult
|
|
4171
|
+
? { assistantTextVisibility: "private" }
|
|
4172
|
+
: undefined,
|
|
4173
|
+
),
|
|
4174
|
+
row(
|
|
4175
|
+
"final-trigger",
|
|
4176
|
+
"user",
|
|
4177
|
+
"INTERNAL SIBLING RESULT",
|
|
4178
|
+
3,
|
|
4179
|
+
options?.userTrigger
|
|
4180
|
+
? undefined
|
|
4181
|
+
: {
|
|
4182
|
+
scripted: true,
|
|
4183
|
+
subagentNotification: {
|
|
4184
|
+
subagentId: "task-final",
|
|
4185
|
+
conversationId: "conv-final",
|
|
4186
|
+
label: "Research",
|
|
4187
|
+
status: "completed",
|
|
4188
|
+
},
|
|
4189
|
+
},
|
|
4190
|
+
),
|
|
4191
|
+
];
|
|
4192
|
+
backgroundRecoveryAttention = {
|
|
4193
|
+
conversationId: "test-conv",
|
|
4194
|
+
latestAssistantMessageId: "prior-result",
|
|
4195
|
+
latestAssistantMessageAt: startedAt + 2,
|
|
4196
|
+
lastSeenAssistantMessageAt: null,
|
|
4197
|
+
} as AttentionState;
|
|
4198
|
+
mockConversationRow = {
|
|
4199
|
+
...mockConversationRow,
|
|
4200
|
+
id: "test-conv",
|
|
4201
|
+
source: "web",
|
|
4202
|
+
conversationType: "standard",
|
|
4203
|
+
};
|
|
4204
|
+
updateMessageMetadataMock.mockImplementation((id, updates) => {
|
|
4205
|
+
const persisted = backgroundRecoveryRows?.find((row) => row.id === id);
|
|
4206
|
+
if (persisted) {
|
|
4207
|
+
persisted.metadata = JSON.stringify({
|
|
4208
|
+
...JSON.parse(persisted.metadata ?? "{}"),
|
|
4209
|
+
...updates,
|
|
4210
|
+
});
|
|
4211
|
+
}
|
|
4212
|
+
});
|
|
4213
|
+
}
|
|
4214
|
+
|
|
4215
|
+
test.each(["failed", "cancelled"] as const)(
|
|
4216
|
+
"an escaping %s continuation recovers an earlier result after release and queue settlement",
|
|
4217
|
+
async (outcome) => {
|
|
4218
|
+
seedDeferredCommandResult();
|
|
4219
|
+
const { promise: queueDrain, resolve: releaseQueue } =
|
|
4220
|
+
Promise.withResolvers<void>();
|
|
4221
|
+
let queued = true;
|
|
4222
|
+
const ctx = makeCtx({
|
|
4223
|
+
hasQueuedMessages: () => queued,
|
|
4224
|
+
drainQueue: async () => {
|
|
4225
|
+
await queueDrain;
|
|
4226
|
+
queued = false;
|
|
4227
|
+
},
|
|
4228
|
+
});
|
|
4229
|
+
backgroundRecoveryPending = () =>
|
|
4230
|
+
ctx.isProcessing() || ctx.hasQueuedMessages();
|
|
4231
|
+
await emitBackgroundResultNotificationReal({
|
|
4232
|
+
conversationId: "test-conv",
|
|
4233
|
+
assistantMessageId: "prior-result",
|
|
4234
|
+
userMessageId: "prior-trigger",
|
|
4235
|
+
rlog: getLogger("failed-continuation-test"),
|
|
4236
|
+
});
|
|
4237
|
+
expect(backgroundRecoverySignals).toHaveLength(0);
|
|
4238
|
+
const abortController = ctx.abortController!;
|
|
4239
|
+
const run = spyOn(ctx.agentLoop, "run").mockImplementationOnce(
|
|
4240
|
+
async () => {
|
|
4241
|
+
if (outcome === "cancelled") {
|
|
4242
|
+
abortController.abort();
|
|
4243
|
+
throw new DOMException("Cancelled", "AbortError");
|
|
4244
|
+
}
|
|
4245
|
+
throw new Error("runtime failure outside the provider retry loop");
|
|
4246
|
+
},
|
|
4247
|
+
);
|
|
4248
|
+
const events: AssistantEvent[] = [];
|
|
4249
|
+
await runAgentLoopImpl(ctx, "continue", "final-trigger", (event) =>
|
|
4250
|
+
events.push(event),
|
|
4251
|
+
);
|
|
4252
|
+
run.mockRestore();
|
|
4253
|
+
|
|
4254
|
+
expect(ctx.isProcessing()).toBe(false);
|
|
4255
|
+
expect(emitBackgroundResultNotificationMock).not.toHaveBeenCalled();
|
|
4256
|
+
expect(backgroundRecoverySignals).toHaveLength(0);
|
|
4257
|
+
expect(events.some((event) => event.type === "message_complete")).toBe(
|
|
4258
|
+
false,
|
|
4259
|
+
);
|
|
4260
|
+
expect(
|
|
4261
|
+
events.some(
|
|
4262
|
+
(event) =>
|
|
4263
|
+
event.type ===
|
|
4264
|
+
(outcome === "cancelled" ? "generation_cancelled" : "error"),
|
|
4265
|
+
),
|
|
4266
|
+
).toBe(true);
|
|
4267
|
+
releaseQueue();
|
|
4268
|
+
await settleTurnTail(ctx.conversationId);
|
|
4269
|
+
await Promise.all(
|
|
4270
|
+
emitBackgroundResultNotificationMock.mock.results.map(
|
|
4271
|
+
(result) => result.value,
|
|
4272
|
+
),
|
|
4273
|
+
);
|
|
4274
|
+
|
|
4275
|
+
expect(emitAssistantReplyNotificationMock).not.toHaveBeenCalled();
|
|
4276
|
+
expect(emitBackgroundResultNotificationMock.mock.calls).toMatchObject([
|
|
4277
|
+
[
|
|
4278
|
+
{
|
|
4279
|
+
userMessageId: "final-trigger",
|
|
4280
|
+
recoverOnly: true,
|
|
4281
|
+
assistantMessageId: undefined,
|
|
4282
|
+
},
|
|
4283
|
+
],
|
|
4284
|
+
]);
|
|
4285
|
+
expect(backgroundRecoverySignals).toMatchObject([
|
|
4286
|
+
{
|
|
4287
|
+
sourceEventName: "activity.complete",
|
|
4288
|
+
dedupeKey: "activity.complete:test-conv:tool:tool-success",
|
|
4289
|
+
contextPayload: {
|
|
4290
|
+
requestedMessage: "The requested report is ready.",
|
|
4291
|
+
},
|
|
4292
|
+
},
|
|
4293
|
+
]);
|
|
4294
|
+
expect(updateMessageMetadataMock).toHaveBeenCalledWith(
|
|
4295
|
+
"final-trigger",
|
|
4296
|
+
expect.objectContaining({ turnOutcome: outcome }),
|
|
4297
|
+
);
|
|
4298
|
+
},
|
|
4299
|
+
);
|
|
4300
|
+
|
|
4301
|
+
test.each([
|
|
4302
|
+
{ name: "a user turn", userTrigger: true },
|
|
4303
|
+
{ name: "private earlier output", privateResult: true },
|
|
4304
|
+
{ name: "failure-only work", noEarlierSuccess: true },
|
|
4305
|
+
{ name: "scheduled work", cronRunId: "run-schedule" },
|
|
4306
|
+
])("an escaping exception stays silent for $name", async (scenario) => {
|
|
4307
|
+
seedDeferredCommandResult(scenario);
|
|
4308
|
+
const ctx = makeCtx();
|
|
4309
|
+
backgroundRecoveryPending = () => ctx.isProcessing();
|
|
4310
|
+
const run = spyOn(ctx.agentLoop, "run").mockRejectedValueOnce(
|
|
4311
|
+
new Error("runtime failure outside the provider retry loop"),
|
|
4312
|
+
);
|
|
4313
|
+
await runAgentLoopImpl(ctx, "continue", "final-trigger", () => {}, {
|
|
4314
|
+
cronRunId: scenario.cronRunId,
|
|
4315
|
+
});
|
|
4316
|
+
run.mockRestore();
|
|
4317
|
+
await settleTurnTail(ctx.conversationId);
|
|
4318
|
+
await Promise.all(
|
|
4319
|
+
emitBackgroundResultNotificationMock.mock.results.map(
|
|
4320
|
+
(result) => result.value,
|
|
4321
|
+
),
|
|
4322
|
+
);
|
|
4323
|
+
|
|
4324
|
+
expect(emitBackgroundResultNotificationMock.mock.calls).toMatchObject([
|
|
4325
|
+
[
|
|
4326
|
+
{
|
|
4327
|
+
userMessageId: "final-trigger",
|
|
4328
|
+
recoverOnly: true,
|
|
4329
|
+
cronRunId: scenario.cronRunId ?? null,
|
|
4330
|
+
},
|
|
4331
|
+
],
|
|
4332
|
+
]);
|
|
4333
|
+
expect(emitAssistantReplyNotificationMock).not.toHaveBeenCalled();
|
|
4334
|
+
expect(backgroundRecoverySignals).toHaveLength(0);
|
|
4335
|
+
});
|
|
4336
|
+
|
|
4337
|
+
test("a failed continuation draining a newer public result emits only that result", async () => {
|
|
4338
|
+
seedDeferredCommandResult();
|
|
4339
|
+
const rows = backgroundRecoveryRows!;
|
|
4340
|
+
const startedAt = rows[0].createdAt;
|
|
4341
|
+
const { promise: queuedTurnReady, resolve: releaseQueuedTurn } =
|
|
4342
|
+
Promise.withResolvers<void>();
|
|
4343
|
+
const { promise: projectionReady, resolve: releaseProjection } =
|
|
4344
|
+
Promise.withResolvers<void>();
|
|
4345
|
+
const { promise: indexingStarted, resolve: markIndexingStarted } =
|
|
4346
|
+
Promise.withResolvers<void>();
|
|
4347
|
+
const { promise: deliveryReady, resolve: releaseDelivery } =
|
|
4348
|
+
Promise.withResolvers<void>();
|
|
4349
|
+
const { promise: deliveryStarted, resolve: markDeliveryStarted } =
|
|
4350
|
+
Promise.withResolvers<void>();
|
|
4351
|
+
let queued = true;
|
|
4352
|
+
const drainQueue = mock(async () => {
|
|
4353
|
+
if (!queued) {
|
|
4354
|
+
return;
|
|
4355
|
+
}
|
|
4356
|
+
await queuedTurnReady;
|
|
4357
|
+
queued = false;
|
|
4358
|
+
const metadata = JSON.parse(rows[0].metadata!);
|
|
4359
|
+
rows.push({
|
|
4360
|
+
...rows[0],
|
|
4361
|
+
id: "queued-trigger",
|
|
4362
|
+
createdAt: startedAt + 10,
|
|
4363
|
+
metadata: JSON.stringify({
|
|
4364
|
+
...metadata,
|
|
4365
|
+
backgroundToolCompletion: {
|
|
4366
|
+
...metadata.backgroundToolCompletion,
|
|
4367
|
+
id: "tool-queued",
|
|
4368
|
+
completedAt: startedAt + 10,
|
|
4369
|
+
},
|
|
4370
|
+
}),
|
|
4371
|
+
});
|
|
4372
|
+
ctx.abortController = new AbortController();
|
|
4373
|
+
ctx.setProcessing(true);
|
|
4374
|
+
await runAgentLoopImpl(
|
|
4375
|
+
ctx,
|
|
4376
|
+
"queued result",
|
|
4377
|
+
"queued-trigger",
|
|
4378
|
+
() => {},
|
|
4379
|
+
);
|
|
4380
|
+
});
|
|
4381
|
+
const ctx = makeCtx({
|
|
4382
|
+
providerResponses: [textResponse("The complete report is ready.")],
|
|
4383
|
+
hasQueuedMessages: () => queued,
|
|
4384
|
+
drainQueue,
|
|
4385
|
+
});
|
|
4386
|
+
backgroundRecoveryPending = () =>
|
|
4387
|
+
ctx.isProcessing() || ctx.hasQueuedMessages();
|
|
4388
|
+
reserveMessageMock.mockImplementationOnce(async () => {
|
|
4389
|
+
rows.push({
|
|
4390
|
+
...rows[1],
|
|
4391
|
+
id: "queued-result",
|
|
4392
|
+
createdAt: startedAt + 11,
|
|
4393
|
+
content: [],
|
|
4394
|
+
finalized: 0,
|
|
4395
|
+
});
|
|
4396
|
+
return { id: "queued-result", createdAt: startedAt + 11 };
|
|
4397
|
+
});
|
|
4398
|
+
finalizeMessageContentMock.mockImplementationOnce(
|
|
4399
|
+
(...args: unknown[]) => {
|
|
4400
|
+
const [id, content] = args as [string, string];
|
|
4401
|
+
const row = rows.find((candidate) => candidate.id === id)!;
|
|
4402
|
+
row.content = JSON.parse(content);
|
|
4403
|
+
row.finalized = 1;
|
|
4404
|
+
},
|
|
4405
|
+
);
|
|
4406
|
+
indexMessageNowMock.mockImplementationOnce(async () => {
|
|
4407
|
+
markIndexingStarted();
|
|
4408
|
+
await projectionReady;
|
|
4409
|
+
return { indexedSegments: 0, enqueuedJobs: 0 };
|
|
4410
|
+
});
|
|
4411
|
+
projectAssistantMessageMock.mockImplementationOnce(
|
|
4412
|
+
(...args: unknown[]) => {
|
|
4413
|
+
const [projection] = args as [
|
|
4414
|
+
{ messageId: string; messageAt: number },
|
|
4415
|
+
];
|
|
4416
|
+
backgroundRecoveryAttention!.latestAssistantMessageId =
|
|
4417
|
+
projection.messageId;
|
|
4418
|
+
backgroundRecoveryAttention!.latestAssistantMessageAt =
|
|
4419
|
+
projection.messageAt;
|
|
4420
|
+
return true;
|
|
4421
|
+
},
|
|
4422
|
+
);
|
|
4423
|
+
emitBackgroundSignalMock.mockImplementationOnce(async (signal) => {
|
|
4424
|
+
markDeliveryStarted();
|
|
4425
|
+
await deliveryReady;
|
|
4426
|
+
backgroundRecoverySignals.push(signal);
|
|
4427
|
+
});
|
|
4428
|
+
const run = spyOn(ctx.agentLoop, "run").mockRejectedValueOnce(
|
|
4429
|
+
new Error("runtime failure outside the provider retry loop"),
|
|
4430
|
+
);
|
|
4431
|
+
await runAgentLoopImpl(ctx, "continue", "final-trigger", () => {});
|
|
4432
|
+
run.mockRestore();
|
|
4433
|
+
expect(backgroundRecoverySignals).toHaveLength(0);
|
|
4434
|
+
releaseQueuedTurn();
|
|
4435
|
+
await indexingStarted;
|
|
4436
|
+
await drainQueue.mock.results[0].value;
|
|
4437
|
+
expect(ctx.isProcessing()).toBe(false);
|
|
4438
|
+
expect(backgroundRecoverySignals).toHaveLength(0);
|
|
4439
|
+
releaseProjection();
|
|
4440
|
+
await deliveryStarted;
|
|
4441
|
+
await settleTurnTail(ctx.conversationId);
|
|
4442
|
+
expect(emitBackgroundSignalMock).toHaveBeenCalledTimes(1);
|
|
4443
|
+
expect(backgroundRecoverySignals).toHaveLength(0);
|
|
4444
|
+
releaseDelivery();
|
|
4445
|
+
await Promise.all(
|
|
4446
|
+
emitBackgroundResultNotificationMock.mock.results.map(
|
|
4447
|
+
(result) => result.value,
|
|
4448
|
+
),
|
|
4449
|
+
);
|
|
4450
|
+
|
|
4451
|
+
expect(emitBackgroundResultNotificationMock).toHaveBeenCalledTimes(2);
|
|
4452
|
+
expect(backgroundRecoverySignals).toMatchObject([
|
|
4453
|
+
{
|
|
4454
|
+
dedupeKey: "activity.complete:test-conv:tool:tool-queued",
|
|
4455
|
+
contextPayload: {
|
|
4456
|
+
requestedMessage: "The complete report is ready.",
|
|
4457
|
+
},
|
|
4458
|
+
},
|
|
4459
|
+
]);
|
|
4460
|
+
});
|
|
4461
|
+
|
|
4462
|
+
test("finalized continuations pass their persisted rows and scheduled owner to the background producer", async () => {
|
|
4463
|
+
mockMessageById = {
|
|
4464
|
+
id: "msg-reserve",
|
|
4465
|
+
conversationId: "test-conv",
|
|
4466
|
+
createdAt: 1234567,
|
|
4467
|
+
role: "assistant",
|
|
4468
|
+
content: "[]",
|
|
4469
|
+
metadata: null,
|
|
4470
|
+
};
|
|
4471
|
+
const ctx = makeCtx({
|
|
4472
|
+
providerResponses: [textResponse("The delegated result is ready.")],
|
|
4473
|
+
});
|
|
4474
|
+
await runAgentLoopImpl(
|
|
4475
|
+
ctx,
|
|
4476
|
+
"internal task completion",
|
|
4477
|
+
"msg-completion",
|
|
4478
|
+
() => {},
|
|
4479
|
+
{ cronRunId: "run-schedule" },
|
|
4480
|
+
);
|
|
4481
|
+
expect(projectAssistantMessageMock).toHaveBeenCalled();
|
|
4482
|
+
expect(emitBackgroundResultNotificationMock).toHaveBeenCalledTimes(1);
|
|
4483
|
+
expect(emitBackgroundResultNotificationMock.mock.calls[0]).toMatchObject([
|
|
4484
|
+
{
|
|
4485
|
+
conversationId: "test-conv",
|
|
4486
|
+
assistantMessageId: "msg-reserve",
|
|
4487
|
+
userMessageId: "msg-completion",
|
|
4488
|
+
cronRunId: "run-schedule",
|
|
4489
|
+
},
|
|
4490
|
+
]);
|
|
4491
|
+
});
|
|
4492
|
+
|
|
3965
4493
|
test("a completed turn notifies with the row that opened it", async () => {
|
|
3966
4494
|
mockMessageById = {
|
|
3967
4495
|
id: "msg-reserve",
|
|
@@ -1356,6 +1356,24 @@ describe("classifyConversationError", () => {
|
|
|
1356
1356
|
expect(result.userMessage).toContain("Billing settings");
|
|
1357
1357
|
});
|
|
1358
1358
|
|
|
1359
|
+
it("routes reason=free_tier_daily_limit_reached to PROVIDER_BILLING/free_tier_daily_limit_reached", () => {
|
|
1360
|
+
providerRoutingSources.openai = "user-key";
|
|
1361
|
+
const err = new ProviderError(
|
|
1362
|
+
'OpenAI API error (402): {"code":"free_tier_daily_limit_reached","detail":"free usage"}',
|
|
1363
|
+
"openai",
|
|
1364
|
+
402,
|
|
1365
|
+
{ reason: "free_tier_daily_limit_reached" },
|
|
1366
|
+
);
|
|
1367
|
+
|
|
1368
|
+
const result = classifyConversationError(err, baseCtx);
|
|
1369
|
+
|
|
1370
|
+
expect(result.code).toBe("PROVIDER_BILLING");
|
|
1371
|
+
expect(result.errorCategory).toBe("free_tier_daily_limit_reached");
|
|
1372
|
+
expect(result.retryable).toBe(false);
|
|
1373
|
+
expect(result.userMessage).toContain("today's free usage");
|
|
1374
|
+
expect(result.userMessage).toContain("midnight UTC");
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1359
1377
|
it("classifies reason=daily_limit_reached as daily_limit_reached even when the routing map says user-key", () => {
|
|
1360
1378
|
// Per-connection platform-auth routes can leave the global routing map
|
|
1361
1379
|
// at user-key; the stamped reason comes only from the platform proxy's
|