@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
|
@@ -87,18 +87,36 @@ mock.module("../notifications/emit-signal.js", () => ({
|
|
|
87
87
|
}));
|
|
88
88
|
|
|
89
89
|
import type { AssistantEvent } from "../api/index.js";
|
|
90
|
+
import { getConfig } from "../config/loader.js";
|
|
90
91
|
import type { Conversation } from "../daemon/conversation.js";
|
|
92
|
+
import {
|
|
93
|
+
type AbortContext,
|
|
94
|
+
abortScheduledRun,
|
|
95
|
+
} from "../daemon/conversation-lifecycle.js";
|
|
96
|
+
import {
|
|
97
|
+
MessageQueue,
|
|
98
|
+
type QueuedDispatch,
|
|
99
|
+
} from "../daemon/conversation-queue-manager.js";
|
|
91
100
|
import {
|
|
92
101
|
clearConversations,
|
|
93
102
|
setConversation,
|
|
103
|
+
setSubagentConversation,
|
|
94
104
|
} from "../daemon/conversation-registry.js";
|
|
95
105
|
import { addMessage } from "../persistence/conversation-crud.js";
|
|
96
106
|
import { getDb } from "../persistence/db-connection.js";
|
|
97
107
|
import { initializeDb } from "../persistence/db-init.js";
|
|
108
|
+
import { wakeAgentForOpportunity } from "../runtime/agent-wake.js";
|
|
98
109
|
import { createSchedule, getScheduleRuns } from "../schedule/schedule-store.js";
|
|
99
110
|
import { runDueSchedulesOnce } from "../schedule/scheduler.js";
|
|
100
111
|
import { getSubagentManager } from "../subagent/index.js";
|
|
101
112
|
import type { SubagentState } from "../subagent/types.js";
|
|
113
|
+
import {
|
|
114
|
+
_clearRegistryForTesting,
|
|
115
|
+
cancelBackgroundTool,
|
|
116
|
+
registerBackgroundTool,
|
|
117
|
+
removeBackgroundTool,
|
|
118
|
+
} from "../tools/background-tool-registry.js";
|
|
119
|
+
import { setConfig } from "./helpers/set-config.js";
|
|
102
120
|
|
|
103
121
|
await initializeDb();
|
|
104
122
|
|
|
@@ -123,7 +141,10 @@ function latestAssistantText(conversationId: string): string | undefined {
|
|
|
123
141
|
* run delegated. Returns a handle that settles it, mirroring what a real
|
|
124
142
|
* child's teardown does.
|
|
125
143
|
*/
|
|
126
|
-
function attachRunningChild(
|
|
144
|
+
function attachRunningChild(
|
|
145
|
+
parentConversationId: string,
|
|
146
|
+
cronRunId?: string,
|
|
147
|
+
): {
|
|
127
148
|
settle: () => void;
|
|
128
149
|
} {
|
|
129
150
|
const manager = getSubagentManager();
|
|
@@ -139,13 +160,14 @@ function attachRunningChild(parentConversationId: string): {
|
|
|
139
160
|
>;
|
|
140
161
|
parentToChildren: Map<string, Set<string>>;
|
|
141
162
|
};
|
|
142
|
-
const subagentId = `sub-${parentConversationId}`;
|
|
163
|
+
const subagentId = `sub-${parentConversationId}-${cronRunId ?? "default"}`;
|
|
143
164
|
const entry = {
|
|
144
165
|
conversation: null,
|
|
145
166
|
state: {
|
|
146
167
|
config: {
|
|
147
168
|
id: subagentId,
|
|
148
169
|
parentConversationId,
|
|
170
|
+
cronRunId,
|
|
149
171
|
label: "advisor",
|
|
150
172
|
objective: "Advise on the briefing",
|
|
151
173
|
role: "advisor",
|
|
@@ -160,7 +182,10 @@ function attachRunningChild(parentConversationId: string): {
|
|
|
160
182
|
runInFlight: true,
|
|
161
183
|
};
|
|
162
184
|
internals.subagents.set(subagentId, entry);
|
|
163
|
-
|
|
185
|
+
const children =
|
|
186
|
+
internals.parentToChildren.get(parentConversationId) ?? new Set<string>();
|
|
187
|
+
children.add(subagentId);
|
|
188
|
+
internals.parentToChildren.set(parentConversationId, children);
|
|
164
189
|
return {
|
|
165
190
|
settle: () => {
|
|
166
191
|
entry.state.status = "completed";
|
|
@@ -187,6 +212,7 @@ describe("schedule result notification wiring", () => {
|
|
|
187
212
|
producerSawText.length = 0;
|
|
188
213
|
runBackgroundJobShouldFail = false;
|
|
189
214
|
clearConversations();
|
|
215
|
+
_clearRegistryForTesting();
|
|
190
216
|
delegateOnRun = undefined;
|
|
191
217
|
});
|
|
192
218
|
|
|
@@ -252,12 +278,13 @@ describe("schedule result notification wiring", () => {
|
|
|
252
278
|
// The turn's own reply, written before the guidance exists.
|
|
253
279
|
addMessage(conversationId, "assistant", "Here is a first pass.");
|
|
254
280
|
|
|
255
|
-
const
|
|
281
|
+
const cronRunId = getScheduleRuns(schedule.id)[0].id;
|
|
282
|
+
const child = attachRunningChild(conversationId, cronRunId);
|
|
256
283
|
let continuationRunning = false;
|
|
257
284
|
setConversation(conversationId, {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
285
|
+
isProcessing: () => continuationRunning,
|
|
286
|
+
currentTurnCronRunId: cronRunId,
|
|
287
|
+
snapshotQueuedMessages: () => [],
|
|
261
288
|
} as unknown as Conversation);
|
|
262
289
|
|
|
263
290
|
// The advisor settles, then its notification starts the continuation
|
|
@@ -307,6 +334,493 @@ describe("schedule result notification wiring", () => {
|
|
|
307
334
|
expect(Date.now() - before).toBeLessThan(200);
|
|
308
335
|
});
|
|
309
336
|
|
|
337
|
+
for (const quiet of [false, true]) {
|
|
338
|
+
for (const status of ["completed", "failed", "cancelled"] as const) {
|
|
339
|
+
test(`waits for a ${status} background command and its final wake (quiet=${quiet})`, async () => {
|
|
340
|
+
const schedule = await createSchedule({
|
|
341
|
+
name: "Background report",
|
|
342
|
+
message: "Run the report in the background",
|
|
343
|
+
syntax: "cron",
|
|
344
|
+
expression: "0 9 * * *",
|
|
345
|
+
quiet,
|
|
346
|
+
});
|
|
347
|
+
forceScheduleDue(schedule.id);
|
|
348
|
+
const started = Promise.withResolvers<string>();
|
|
349
|
+
const finishWake = Promise.withResolvers<void>();
|
|
350
|
+
delegateOnRun = (conversationId) => {
|
|
351
|
+
addMessage(conversationId, "assistant", "The command is running.");
|
|
352
|
+
setConversation(conversationId, {
|
|
353
|
+
isProcessing: () => false,
|
|
354
|
+
snapshotQueuedMessages: () => [],
|
|
355
|
+
} as unknown as Conversation);
|
|
356
|
+
registerBackgroundTool({
|
|
357
|
+
id: "bg-scheduled",
|
|
358
|
+
cronRunId: getScheduleRuns(schedule.id)[0].id,
|
|
359
|
+
conversationId,
|
|
360
|
+
toolName: "bash",
|
|
361
|
+
command: "generate-report",
|
|
362
|
+
startedAt: Date.now(),
|
|
363
|
+
cancel: () => {},
|
|
364
|
+
});
|
|
365
|
+
if (status === "cancelled") {
|
|
366
|
+
cancelBackgroundTool("bg-scheduled");
|
|
367
|
+
}
|
|
368
|
+
started.resolve(conversationId);
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
let scheduleSettled = false;
|
|
372
|
+
const scheduledRun = runDueSchedulesOnce().then(() => {
|
|
373
|
+
scheduleSettled = true;
|
|
374
|
+
});
|
|
375
|
+
const conversationId = await started.promise;
|
|
376
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
377
|
+
expect(scheduleSettled).toBe(false);
|
|
378
|
+
expect(producerCalls).toHaveLength(0);
|
|
379
|
+
|
|
380
|
+
clearConversations();
|
|
381
|
+
removeBackgroundTool("bg-scheduled");
|
|
382
|
+
const wake = wakeAgentForOpportunity(
|
|
383
|
+
{
|
|
384
|
+
conversationId,
|
|
385
|
+
cronRunId: getScheduleRuns(schedule.id)[0].id,
|
|
386
|
+
hint: `Background command ${status}`,
|
|
387
|
+
source: "background-tool",
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
resolveTarget: async () => {
|
|
391
|
+
await finishWake.promise;
|
|
392
|
+
addMessage(
|
|
393
|
+
conversationId,
|
|
394
|
+
"assistant",
|
|
395
|
+
`Final result: ${status}.`,
|
|
396
|
+
);
|
|
397
|
+
return null;
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
);
|
|
401
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
402
|
+
expect(scheduleSettled).toBe(false);
|
|
403
|
+
expect(producerCalls).toHaveLength(0);
|
|
404
|
+
|
|
405
|
+
finishWake.resolve();
|
|
406
|
+
await wake;
|
|
407
|
+
await scheduledRun;
|
|
408
|
+
expect(getScheduleRuns(schedule.id)[0].status).toBe("ok");
|
|
409
|
+
expect(producerCalls).toHaveLength(quiet ? 0 : 1);
|
|
410
|
+
if (!quiet) {
|
|
411
|
+
expect(producerSawText).toEqual([`Final result: ${status}.`]);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
for (const quiet of [false, true]) {
|
|
418
|
+
for (const kind of ["bash", "host_bash", "wake", "subagent"] as const) {
|
|
419
|
+
test(`a timed-out ${kind} fails without a partial result (quiet=${quiet})`, async () => {
|
|
420
|
+
const timeouts = getConfig().timeouts;
|
|
421
|
+
setConfig("timeouts", { ...timeouts, scheduleTurnTimeoutSec: 1 });
|
|
422
|
+
const schedule = await createSchedule({
|
|
423
|
+
name: "Background report",
|
|
424
|
+
message: "Prepare the report",
|
|
425
|
+
syntax: "cron",
|
|
426
|
+
expression: "0 9 * * *",
|
|
427
|
+
maxRetries: 1,
|
|
428
|
+
quiet,
|
|
429
|
+
});
|
|
430
|
+
forceScheduleDue(schedule.id);
|
|
431
|
+
let cancelled = false;
|
|
432
|
+
let parentAborted = false;
|
|
433
|
+
let wake: ReturnType<typeof wakeAgentForOpportunity> | undefined;
|
|
434
|
+
const finishLookup = Promise.withResolvers<void>();
|
|
435
|
+
delegateOnRun = (conversationId) => {
|
|
436
|
+
addMessage(
|
|
437
|
+
conversationId,
|
|
438
|
+
"assistant",
|
|
439
|
+
"The report is still running.",
|
|
440
|
+
);
|
|
441
|
+
const cronRunId = getScheduleRuns(schedule.id)[0].id;
|
|
442
|
+
setConversation(conversationId, {
|
|
443
|
+
isProcessing: () => false,
|
|
444
|
+
snapshotQueuedMessages: () => [],
|
|
445
|
+
abortScheduledRun: (runId: string) => {
|
|
446
|
+
expect(runId).toBe(cronRunId);
|
|
447
|
+
parentAborted = true;
|
|
448
|
+
},
|
|
449
|
+
} as unknown as Conversation);
|
|
450
|
+
if (kind === "wake") {
|
|
451
|
+
wake = wakeAgentForOpportunity(
|
|
452
|
+
{
|
|
453
|
+
conversationId,
|
|
454
|
+
cronRunId: getScheduleRuns(schedule.id)[0].id,
|
|
455
|
+
source: "background-tool",
|
|
456
|
+
hint: "The command finished",
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
resolveTarget: async () => {
|
|
460
|
+
await finishLookup.promise;
|
|
461
|
+
return {
|
|
462
|
+
waitForIdle: async () => true,
|
|
463
|
+
isProcessing: () => false,
|
|
464
|
+
} as unknown as Conversation;
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
);
|
|
468
|
+
} else {
|
|
469
|
+
let toolConversationId = conversationId;
|
|
470
|
+
if (kind === "subagent") {
|
|
471
|
+
attachRunningChild(conversationId, cronRunId);
|
|
472
|
+
toolConversationId =
|
|
473
|
+
getSubagentManager().getChildrenOf(conversationId)[0]
|
|
474
|
+
.conversationId;
|
|
475
|
+
}
|
|
476
|
+
registerBackgroundTool({
|
|
477
|
+
id: "bg-timeout",
|
|
478
|
+
cronRunId,
|
|
479
|
+
conversationId: toolConversationId,
|
|
480
|
+
toolName: kind,
|
|
481
|
+
command: "generate-report",
|
|
482
|
+
startedAt: Date.now(),
|
|
483
|
+
cancel: () => {
|
|
484
|
+
cancelled = true;
|
|
485
|
+
removeBackgroundTool("bg-timeout");
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
try {
|
|
491
|
+
const result = await runDueSchedulesOnce();
|
|
492
|
+
expect(getScheduleRuns(schedule.id)[0]).toMatchObject({
|
|
493
|
+
status: "error",
|
|
494
|
+
error: "Scheduled work did not finish before its time limit",
|
|
495
|
+
});
|
|
496
|
+
expect(producerCalls).toHaveLength(0);
|
|
497
|
+
expect(parentAborted).toBe(true);
|
|
498
|
+
if (kind !== "wake") {
|
|
499
|
+
expect(cancelled).toBe(true);
|
|
500
|
+
}
|
|
501
|
+
finishLookup.resolve();
|
|
502
|
+
if (wake) {
|
|
503
|
+
expect(await wake).toMatchObject({
|
|
504
|
+
invoked: false,
|
|
505
|
+
reason: "timeout",
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
expect(result.failed).toBe(1);
|
|
509
|
+
} finally {
|
|
510
|
+
finishLookup.resolve();
|
|
511
|
+
await wake;
|
|
512
|
+
removeBackgroundTool("bg-timeout");
|
|
513
|
+
setConfig("timeouts", timeouts);
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
for (const userTurnActive of [true, false]) {
|
|
520
|
+
test(`timeout preserves unrelated work (user turn active=${userTurnActive})`, async () => {
|
|
521
|
+
const timeouts = getConfig().timeouts;
|
|
522
|
+
setConfig("timeouts", { ...timeouts, scheduleTurnTimeoutSec: 1 });
|
|
523
|
+
const schedule = await createSchedule({
|
|
524
|
+
name: "Report",
|
|
525
|
+
message: "Prepare a report",
|
|
526
|
+
syntax: "cron",
|
|
527
|
+
expression: "0 9 * * *",
|
|
528
|
+
maxRetries: 1,
|
|
529
|
+
});
|
|
530
|
+
forceScheduleDue(schedule.id);
|
|
531
|
+
const controller = new AbortController();
|
|
532
|
+
const queue = new MessageQueue();
|
|
533
|
+
const userEvents: string[] = [];
|
|
534
|
+
const cancelledTools: string[] = [];
|
|
535
|
+
let conversationId = "";
|
|
536
|
+
let runId = "";
|
|
537
|
+
let settleUserChild: (() => void) | undefined;
|
|
538
|
+
delegateOnRun = (id) => {
|
|
539
|
+
conversationId = id;
|
|
540
|
+
runId = getScheduleRuns(schedule.id)[0].id;
|
|
541
|
+
const ctx = {
|
|
542
|
+
conversationId: id,
|
|
543
|
+
currentTurnCronRunId: userTurnActive ? undefined : runId,
|
|
544
|
+
isProcessing: () => true,
|
|
545
|
+
setProcessing: () => {},
|
|
546
|
+
abortController: controller,
|
|
547
|
+
queue,
|
|
548
|
+
pendingInterruptRepair: false,
|
|
549
|
+
prompter: { dispose: () => {} },
|
|
550
|
+
secretPrompter: { dispose: () => {} },
|
|
551
|
+
pendingSurfaceActions: new Map(),
|
|
552
|
+
surfaceActionRequestIds: new Set(),
|
|
553
|
+
surfaceState: new Map(),
|
|
554
|
+
accumulatedSurfaceState: new Map(),
|
|
555
|
+
kickDrainQueue: async () => {},
|
|
556
|
+
} as unknown as AbortContext;
|
|
557
|
+
setConversation(id, {
|
|
558
|
+
...ctx,
|
|
559
|
+
snapshotQueuedMessages: () => queue.snapshot(),
|
|
560
|
+
abortScheduledRun: (run: string) => abortScheduledRun(ctx, run),
|
|
561
|
+
} as unknown as Conversation);
|
|
562
|
+
for (const owner of [undefined, runId, "run-other"]) {
|
|
563
|
+
queue.push({
|
|
564
|
+
content: "Follow-up",
|
|
565
|
+
attachments: [],
|
|
566
|
+
requestId: `queued-${owner ?? "user"}`,
|
|
567
|
+
sentAt: Date.now(),
|
|
568
|
+
cronRunId: owner,
|
|
569
|
+
onEvent: (event) => {
|
|
570
|
+
if (owner !== runId) {
|
|
571
|
+
userEvents.push(event.type);
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
});
|
|
575
|
+
const id = `tool-${owner ?? "user"}`;
|
|
576
|
+
registerBackgroundTool({
|
|
577
|
+
id,
|
|
578
|
+
cronRunId: owner,
|
|
579
|
+
conversationId,
|
|
580
|
+
toolName: "bash",
|
|
581
|
+
command: "example-command",
|
|
582
|
+
startedAt: Date.now(),
|
|
583
|
+
cancel: () => {
|
|
584
|
+
cancelledTools.push(id);
|
|
585
|
+
removeBackgroundTool(id);
|
|
586
|
+
},
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
attachRunningChild(id, runId);
|
|
590
|
+
settleUserChild = attachRunningChild(id).settle;
|
|
591
|
+
};
|
|
592
|
+
try {
|
|
593
|
+
await runDueSchedulesOnce();
|
|
594
|
+
expect(controller.signal.aborted).toBe(!userTurnActive);
|
|
595
|
+
expect(queue.snapshot().map((message) => message.requestId)).toEqual([
|
|
596
|
+
"queued-user",
|
|
597
|
+
"queued-run-other",
|
|
598
|
+
]);
|
|
599
|
+
expect(userEvents).toEqual([]);
|
|
600
|
+
expect(cancelledTools).toEqual([`tool-${runId}`]);
|
|
601
|
+
const children = getSubagentManager().getChildrenOf(conversationId);
|
|
602
|
+
expect(
|
|
603
|
+
children.find((child) => child.config.cronRunId === runId)?.status,
|
|
604
|
+
).toBe("aborted");
|
|
605
|
+
expect(
|
|
606
|
+
children.find((child) => child.config.cronRunId === undefined)
|
|
607
|
+
?.status,
|
|
608
|
+
).toBe("running");
|
|
609
|
+
expect(producerCalls).toHaveLength(0);
|
|
610
|
+
} finally {
|
|
611
|
+
settleUserChild?.();
|
|
612
|
+
_clearRegistryForTesting();
|
|
613
|
+
setConfig("timeouts", timeouts);
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
for (const kind of [
|
|
619
|
+
"turn",
|
|
620
|
+
"queue",
|
|
621
|
+
"command",
|
|
622
|
+
"child",
|
|
623
|
+
"wake",
|
|
624
|
+
"dispatch",
|
|
625
|
+
] as const) {
|
|
626
|
+
test(`unrelated ${kind} does not keep a settled firing open`, async () => {
|
|
627
|
+
const timeouts = getConfig().timeouts;
|
|
628
|
+
setConfig("timeouts", { ...timeouts, scheduleTurnTimeoutSec: 1 });
|
|
629
|
+
const schedule = await createSchedule({
|
|
630
|
+
name: "Report",
|
|
631
|
+
message: "Prepare a report",
|
|
632
|
+
syntax: "cron",
|
|
633
|
+
expression: "0 9 * * *",
|
|
634
|
+
});
|
|
635
|
+
forceScheduleDue(schedule.id);
|
|
636
|
+
const delegated = Promise.withResolvers<void>();
|
|
637
|
+
const releaseWake = Promise.withResolvers<void>();
|
|
638
|
+
let wake: ReturnType<typeof wakeAgentForOpportunity> | undefined;
|
|
639
|
+
let userChild: ReturnType<typeof attachRunningChild> | undefined;
|
|
640
|
+
let conversationId = "";
|
|
641
|
+
delegateOnRun = (id) => {
|
|
642
|
+
conversationId = id;
|
|
643
|
+
const runId = getScheduleRuns(schedule.id)[0].id;
|
|
644
|
+
const queue = new MessageQueue();
|
|
645
|
+
if (kind === "queue") {
|
|
646
|
+
queue.push({
|
|
647
|
+
content: "User follow-up",
|
|
648
|
+
attachments: [],
|
|
649
|
+
requestId: "queued-user",
|
|
650
|
+
sentAt: Date.now(),
|
|
651
|
+
onEvent: () => {},
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
setConversation(id, {
|
|
655
|
+
isProcessing: () => kind === "turn",
|
|
656
|
+
currentTurnCronRunId: undefined,
|
|
657
|
+
pendingQueuedDispatches:
|
|
658
|
+
kind === "dispatch"
|
|
659
|
+
? new Map<string | null, Set<QueuedDispatch>>([
|
|
660
|
+
[
|
|
661
|
+
"run-other",
|
|
662
|
+
new Set([
|
|
663
|
+
{ controller: new AbortController(), messages: [] },
|
|
664
|
+
]),
|
|
665
|
+
],
|
|
666
|
+
[
|
|
667
|
+
null,
|
|
668
|
+
new Set([
|
|
669
|
+
{ controller: new AbortController(), messages: [] },
|
|
670
|
+
]),
|
|
671
|
+
],
|
|
672
|
+
])
|
|
673
|
+
: new Map(),
|
|
674
|
+
snapshotQueuedMessages: () => queue.snapshot(),
|
|
675
|
+
} as unknown as Conversation);
|
|
676
|
+
registerBackgroundTool({
|
|
677
|
+
id: "owned-command",
|
|
678
|
+
conversationId: id,
|
|
679
|
+
cronRunId: runId,
|
|
680
|
+
toolName: "bash",
|
|
681
|
+
command: "report",
|
|
682
|
+
startedAt: Date.now(),
|
|
683
|
+
cancel: () => {},
|
|
684
|
+
});
|
|
685
|
+
if (kind === "command") {
|
|
686
|
+
registerBackgroundTool({
|
|
687
|
+
id: "user-command",
|
|
688
|
+
conversationId: id,
|
|
689
|
+
toolName: "bash",
|
|
690
|
+
command: "user-work",
|
|
691
|
+
startedAt: Date.now(),
|
|
692
|
+
cancel: () => {},
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
if (kind === "child") {
|
|
696
|
+
userChild = attachRunningChild(id);
|
|
697
|
+
}
|
|
698
|
+
if (kind === "wake") {
|
|
699
|
+
wake = wakeAgentForOpportunity(
|
|
700
|
+
{
|
|
701
|
+
conversationId: id,
|
|
702
|
+
source: "background-tool",
|
|
703
|
+
hint: "Unrelated result",
|
|
704
|
+
cronRunId: "run-other",
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
resolveTarget: async () => {
|
|
708
|
+
await releaseWake.promise;
|
|
709
|
+
return null;
|
|
710
|
+
},
|
|
711
|
+
},
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
delegated.resolve();
|
|
715
|
+
};
|
|
716
|
+
try {
|
|
717
|
+
const scheduledRun = runDueSchedulesOnce();
|
|
718
|
+
await delegated.promise;
|
|
719
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
720
|
+
expect(producerCalls).toHaveLength(0);
|
|
721
|
+
addMessage(conversationId, "assistant", "Report finished.");
|
|
722
|
+
removeBackgroundTool("owned-command");
|
|
723
|
+
await scheduledRun;
|
|
724
|
+
expect(getScheduleRuns(schedule.id)[0].status).toBe("ok");
|
|
725
|
+
expect(producerSawText).toEqual(["Report finished."]);
|
|
726
|
+
} finally {
|
|
727
|
+
releaseWake.resolve();
|
|
728
|
+
await wake;
|
|
729
|
+
userChild?.settle();
|
|
730
|
+
_clearRegistryForTesting();
|
|
731
|
+
setConfig("timeouts", timeouts);
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
for (const location of [
|
|
737
|
+
"parent",
|
|
738
|
+
"completed-child",
|
|
739
|
+
"reused-child",
|
|
740
|
+
] as const) {
|
|
741
|
+
test(`waits for owned queued work in a ${location}`, async () => {
|
|
742
|
+
const schedule = await createSchedule({
|
|
743
|
+
name: "Report",
|
|
744
|
+
message: "Prepare a report",
|
|
745
|
+
syntax: "cron",
|
|
746
|
+
expression: "0 9 * * *",
|
|
747
|
+
});
|
|
748
|
+
forceScheduleDue(schedule.id);
|
|
749
|
+
const delegated = Promise.withResolvers<string>();
|
|
750
|
+
const queue = new MessageQueue();
|
|
751
|
+
delegateOnRun = (id) => {
|
|
752
|
+
const runId = getScheduleRuns(schedule.id)[0].id;
|
|
753
|
+
queue.push({
|
|
754
|
+
content: "Continue report",
|
|
755
|
+
attachments: [],
|
|
756
|
+
requestId: "queued-schedule",
|
|
757
|
+
sentAt: Date.now(),
|
|
758
|
+
cronRunId: runId,
|
|
759
|
+
onEvent: () => {},
|
|
760
|
+
});
|
|
761
|
+
const live = {
|
|
762
|
+
isProcessing: () => false,
|
|
763
|
+
snapshotQueuedMessages: () => queue.snapshot(),
|
|
764
|
+
} as unknown as Conversation;
|
|
765
|
+
if (location === "parent") {
|
|
766
|
+
setConversation(id, live);
|
|
767
|
+
} else {
|
|
768
|
+
attachRunningChild(
|
|
769
|
+
id,
|
|
770
|
+
location === "reused-child" ? undefined : runId,
|
|
771
|
+
).settle();
|
|
772
|
+
setSubagentConversation(
|
|
773
|
+
getSubagentManager().getChildrenOf(id)[0].conversationId,
|
|
774
|
+
live,
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
delegated.resolve(id);
|
|
778
|
+
};
|
|
779
|
+
const scheduledRun = runDueSchedulesOnce();
|
|
780
|
+
const id = await delegated.promise;
|
|
781
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
782
|
+
expect(producerCalls).toHaveLength(0);
|
|
783
|
+
addMessage(id, "assistant", "Final report.");
|
|
784
|
+
queue.clear();
|
|
785
|
+
await scheduledRun;
|
|
786
|
+
expect(getScheduleRuns(schedule.id)[0].status).toBe("ok");
|
|
787
|
+
expect(producerSawText).toEqual(["Final report."]);
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
test("waits for a dequeued dispatch before its processing claim", async () => {
|
|
792
|
+
const schedule = await createSchedule({
|
|
793
|
+
name: "Report",
|
|
794
|
+
message: "Prepare report",
|
|
795
|
+
syntax: "cron",
|
|
796
|
+
expression: "0 9 * * *",
|
|
797
|
+
});
|
|
798
|
+
forceScheduleDue(schedule.id);
|
|
799
|
+
const entered = Promise.withResolvers<string>();
|
|
800
|
+
const dispatches = new Map<string, Set<QueuedDispatch>>();
|
|
801
|
+
delegateOnRun = (id) => {
|
|
802
|
+
dispatches.set(
|
|
803
|
+
getScheduleRuns(schedule.id)[0].id,
|
|
804
|
+
new Set([{ controller: new AbortController(), messages: [] }]),
|
|
805
|
+
);
|
|
806
|
+
setConversation(id, {
|
|
807
|
+
isProcessing: () => false,
|
|
808
|
+
snapshotQueuedMessages: () => [],
|
|
809
|
+
pendingQueuedDispatches: dispatches,
|
|
810
|
+
} as unknown as Conversation);
|
|
811
|
+
entered.resolve(id);
|
|
812
|
+
};
|
|
813
|
+
const scheduledRun = runDueSchedulesOnce();
|
|
814
|
+
const id = await entered.promise;
|
|
815
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
816
|
+
expect(producerCalls).toHaveLength(0);
|
|
817
|
+
addMessage(id, "assistant", "Final report.");
|
|
818
|
+
dispatches.clear();
|
|
819
|
+
await scheduledRun;
|
|
820
|
+
expect(getScheduleRuns(schedule.id)[0].status).toBe("ok");
|
|
821
|
+
expect(producerSawText).toEqual(["Final report."]);
|
|
822
|
+
});
|
|
823
|
+
|
|
310
824
|
test("captures runStartedAt before the run, not after", async () => {
|
|
311
825
|
const before = Date.now();
|
|
312
826
|
const schedule = await createSchedule({
|
|
@@ -8,6 +8,7 @@ const mockWakeAgentForOpportunity = mock(
|
|
|
8
8
|
}> => Promise.resolve({ invoked: true, producedToolCalls: false }),
|
|
9
9
|
);
|
|
10
10
|
mock.module("../runtime/agent-wake.js", () => ({
|
|
11
|
+
hasPendingAgentWake: () => false,
|
|
11
12
|
wakeAgentForOpportunity: mockWakeAgentForOpportunity,
|
|
12
13
|
}));
|
|
13
14
|
|
|
@@ -109,6 +109,41 @@ async function executeSkillLoad(
|
|
|
109
109
|
// ── Tests ─────────────────────────────────────────────────────────────────
|
|
110
110
|
|
|
111
111
|
describe("skill_load inline command expansion", () => {
|
|
112
|
+
test("read-only preloading returns instructions without running inline commands", async () => {
|
|
113
|
+
writeSkill(
|
|
114
|
+
"readonly-example",
|
|
115
|
+
"readonly-example",
|
|
116
|
+
"Example skill",
|
|
117
|
+
"Context: !`echo example`",
|
|
118
|
+
);
|
|
119
|
+
const result = await skillLoadTool.execute(
|
|
120
|
+
{ skill: "readonly-example" },
|
|
121
|
+
{
|
|
122
|
+
conversationId: "conversation-1",
|
|
123
|
+
workingDir: TEST_DIR,
|
|
124
|
+
trustClass: "guardian",
|
|
125
|
+
},
|
|
126
|
+
{ readOnly: true },
|
|
127
|
+
);
|
|
128
|
+
expect(result.isError).toBe(false);
|
|
129
|
+
expect(result.content).toContain("[inline command not executed]");
|
|
130
|
+
expect(runInlineCommandCalls).toHaveLength(0);
|
|
131
|
+
expect(mockAutoInstall).not.toHaveBeenCalled();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("read-only preloading does not install missing skills", async () => {
|
|
135
|
+
const result = await skillLoadTool.execute(
|
|
136
|
+
{ skill: "missing-readonly-skill" },
|
|
137
|
+
{
|
|
138
|
+
conversationId: "conversation-1",
|
|
139
|
+
workingDir: TEST_DIR,
|
|
140
|
+
trustClass: "guardian",
|
|
141
|
+
},
|
|
142
|
+
{ readOnly: true },
|
|
143
|
+
);
|
|
144
|
+
expect(result.isError).toBe(true);
|
|
145
|
+
expect(mockAutoInstall).not.toHaveBeenCalled();
|
|
146
|
+
});
|
|
112
147
|
beforeEach(() => {
|
|
113
148
|
mkdirSync(join(TEST_DIR, "skills"), { recursive: true });
|
|
114
149
|
runInlineCommandCalls.length = 0;
|