@oh-my-pi/pi-coding-agent 17.0.8 → 17.0.9
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/CHANGELOG.md +33 -1
- package/dist/cli.js +4850 -4774
- package/dist/types/config/__tests__/model-registry.test.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +27 -0
- package/dist/types/config/settings-schema.d.ts +34 -3
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +10 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +1 -1
- package/dist/types/internal-urls/registry-helpers.d.ts +11 -0
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +6 -2
- package/dist/types/mcp/render.d.ts +2 -2
- package/dist/types/mcp/tool-bridge.d.ts +7 -3
- package/dist/types/mcp/types.d.ts +6 -0
- package/dist/types/modes/components/oauth-selector.d.ts +8 -0
- package/dist/types/modes/components/settings-defs.d.ts +1 -0
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +3 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
- package/dist/types/modes/rpc/rpc-frame.d.ts +16 -0
- package/dist/types/modes/rpc/rpc-messages.d.ts +26 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +40 -0
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +9 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +8 -1
- package/dist/types/session/session-loader.d.ts +6 -4
- package/dist/types/task/types.d.ts +14 -0
- package/dist/types/tools/hub/jobs.d.ts +1 -1
- package/dist/types/tools/index.d.ts +3 -0
- package/dist/types/tools/report-tool-issue.d.ts +24 -9
- package/dist/types/web/search/providers/firecrawl.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +13 -12
- package/scripts/legacy-pi-virtual-module.ts +1 -1
- package/src/cli/grievances-cli.ts +2 -2
- package/src/cli/usage-cli.ts +1 -1
- package/src/config/__tests__/model-registry.test.ts +147 -0
- package/src/config/model-registry.ts +40 -0
- package/src/config/model-resolver.ts +0 -1
- package/src/config/settings-schema.ts +41 -3
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +1 -0
- package/src/extensibility/legacy-pi-tui-shim.ts +10 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +851 -310
- package/src/internal-urls/registry-helpers.ts +40 -0
- package/src/lsp/client.ts +23 -0
- package/src/lsp/clients/biome-client.ts +3 -4
- package/src/lsp/index.ts +25 -7
- package/src/main.ts +1 -0
- package/src/mcp/manager.ts +39 -8
- package/src/mcp/render.ts +94 -35
- package/src/mcp/tool-bridge.ts +107 -11
- package/src/mcp/types.ts +7 -0
- package/src/modes/components/agent-hub.ts +26 -9
- package/src/modes/components/oauth-selector.ts +24 -7
- package/src/modes/components/settings-defs.ts +5 -2
- package/src/modes/components/settings-selector.ts +9 -5
- package/src/modes/components/tool-execution.test.ts +63 -2
- package/src/modes/controllers/command-controller.ts +14 -7
- package/src/modes/controllers/mcp-command-controller.ts +70 -40
- package/src/modes/interactive-mode.ts +3 -0
- package/src/modes/rpc/rpc-client.ts +94 -3
- package/src/modes/rpc/rpc-frame.ts +164 -4
- package/src/modes/rpc/rpc-messages.ts +127 -0
- package/src/modes/rpc/rpc-mode.ts +79 -7
- package/src/modes/rpc/rpc-types.ts +34 -2
- package/src/modes/setup-wizard/scenes/model.ts +5 -7
- package/src/modes/setup-wizard/scenes/providers.ts +3 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +8 -2
- package/src/modes/setup-wizard/scenes/theme.ts +13 -3
- package/src/modes/setup-wizard/scenes/types.ts +9 -1
- package/src/modes/setup-wizard/scenes/web-search.ts +6 -1
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- package/src/prompts/goals/guided-goal-system.md +24 -3
- package/src/prompts/tools/task.md +12 -2
- package/src/sdk.ts +45 -3
- package/src/session/agent-session.ts +63 -32
- package/src/session/session-context.test.ts +224 -1
- package/src/session/session-context.ts +41 -2
- package/src/session/session-loader.ts +10 -5
- package/src/slash-commands/helpers/usage-report.ts +3 -1
- package/src/task/executor.ts +3 -0
- package/src/task/index.ts +52 -8
- package/src/task/structured-subagent.ts +3 -1
- package/src/task/types.ts +16 -0
- package/src/tools/hub/index.ts +1 -1
- package/src/tools/hub/jobs.ts +67 -8
- package/src/tools/index.ts +3 -0
- package/src/tools/report-tool-issue.ts +79 -28
- package/src/web/search/providers/firecrawl.ts +46 -13
- package/src/web/search/types.ts +5 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
3
|
+
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
3
4
|
import * as snapcompact from "@oh-my-pi/snapcompact";
|
|
4
|
-
import type
|
|
5
|
+
import { type CompactionSummaryMessage, INTERRUPTED_THINKING_MESSAGE_TYPE } from "./messages";
|
|
5
6
|
import { buildSessionContext, type StrippedToolCallsMarker } from "./session-context";
|
|
6
7
|
import type { SessionEntry } from "./session-entries";
|
|
7
8
|
|
|
@@ -159,3 +160,225 @@ describe("buildSessionContext dangling toolCalls", () => {
|
|
|
159
160
|
expect(context.messages.some(message => message.role === "assistant")).toBe(false);
|
|
160
161
|
});
|
|
161
162
|
});
|
|
163
|
+
|
|
164
|
+
const assistantUsage: AssistantMessage["usage"] = {
|
|
165
|
+
input: 0,
|
|
166
|
+
output: 0,
|
|
167
|
+
cacheRead: 0,
|
|
168
|
+
cacheWrite: 0,
|
|
169
|
+
totalTokens: 0,
|
|
170
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
function userEntry(id: string, parentId: string | null, content: string, messageTimestamp: number): SessionEntry {
|
|
174
|
+
return {
|
|
175
|
+
type: "message",
|
|
176
|
+
id,
|
|
177
|
+
parentId,
|
|
178
|
+
timestamp,
|
|
179
|
+
message: { role: "user", content, timestamp: messageTimestamp } as AgentMessage,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function assistantEntry(
|
|
184
|
+
id: string,
|
|
185
|
+
parentId: string | null,
|
|
186
|
+
stopReason: AssistantMessage["stopReason"],
|
|
187
|
+
text: string,
|
|
188
|
+
messageTimestamp: number,
|
|
189
|
+
): SessionEntry {
|
|
190
|
+
return {
|
|
191
|
+
type: "message",
|
|
192
|
+
id,
|
|
193
|
+
parentId,
|
|
194
|
+
timestamp,
|
|
195
|
+
message: {
|
|
196
|
+
role: "assistant",
|
|
197
|
+
content: [{ type: "text", text }],
|
|
198
|
+
api: "anthropic-messages",
|
|
199
|
+
provider: "anthropic",
|
|
200
|
+
model: "claude-sonnet-4-5",
|
|
201
|
+
usage: assistantUsage,
|
|
202
|
+
stopReason,
|
|
203
|
+
timestamp: messageTimestamp,
|
|
204
|
+
} satisfies AssistantMessage,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function toolCallAssistantEntry(
|
|
209
|
+
id: string,
|
|
210
|
+
parentId: string | null,
|
|
211
|
+
stopReason: AssistantMessage["stopReason"],
|
|
212
|
+
toolCallId: string,
|
|
213
|
+
messageTimestamp: number,
|
|
214
|
+
): SessionEntry {
|
|
215
|
+
return {
|
|
216
|
+
type: "message",
|
|
217
|
+
id,
|
|
218
|
+
parentId,
|
|
219
|
+
timestamp,
|
|
220
|
+
message: {
|
|
221
|
+
role: "assistant",
|
|
222
|
+
content: [{ type: "toolCall", id: toolCallId, name: "write", arguments: { path: "plan.md", content: "x" } }],
|
|
223
|
+
api: "anthropic-messages",
|
|
224
|
+
provider: "anthropic",
|
|
225
|
+
model: "claude-sonnet-4-5",
|
|
226
|
+
usage: assistantUsage,
|
|
227
|
+
stopReason,
|
|
228
|
+
timestamp: messageTimestamp,
|
|
229
|
+
} satisfies AssistantMessage,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function syntheticToolResultEntry(
|
|
234
|
+
id: string,
|
|
235
|
+
parentId: string | null,
|
|
236
|
+
toolCallId: string,
|
|
237
|
+
messageTimestamp: number,
|
|
238
|
+
): SessionEntry {
|
|
239
|
+
return {
|
|
240
|
+
type: "message",
|
|
241
|
+
id,
|
|
242
|
+
parentId,
|
|
243
|
+
timestamp,
|
|
244
|
+
message: {
|
|
245
|
+
role: "toolResult",
|
|
246
|
+
toolCallId,
|
|
247
|
+
toolName: "write",
|
|
248
|
+
content: [
|
|
249
|
+
{ type: "text", text: "Tool call was not executed because the provider stream ended with an error." },
|
|
250
|
+
],
|
|
251
|
+
details: { __synthetic: true, source: "assistant_stop_error", executed: false },
|
|
252
|
+
isError: true,
|
|
253
|
+
timestamp: messageTimestamp,
|
|
254
|
+
} as AgentMessage,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function hiddenContinuityEntry(id: string, parentId: string | null): SessionEntry {
|
|
259
|
+
return {
|
|
260
|
+
type: "custom_message",
|
|
261
|
+
id,
|
|
262
|
+
parentId,
|
|
263
|
+
timestamp,
|
|
264
|
+
customType: INTERRUPTED_THINKING_MESSAGE_TYPE,
|
|
265
|
+
content: "preserved interrupted thinking",
|
|
266
|
+
display: false,
|
|
267
|
+
attribution: "agent",
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function expectUserTail(messages: AgentMessage[], content: string): void {
|
|
272
|
+
const tail = messages.at(-1);
|
|
273
|
+
expect(tail?.role).toBe("user");
|
|
274
|
+
if (tail?.role !== "user") {
|
|
275
|
+
throw new Error(`Expected user tail, received ${tail?.role ?? "none"}`);
|
|
276
|
+
}
|
|
277
|
+
expect(tail.content).toBe(content);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
describe("buildSessionContext failed replay tails", () => {
|
|
281
|
+
it("terminates on cyclic parent links and includes each reachable message once", () => {
|
|
282
|
+
const entries = [userEntry("A", "B", "from A", 1), userEntry("B", "A", "from B", 2)];
|
|
283
|
+
|
|
284
|
+
const context = buildSessionContext(entries, "A");
|
|
285
|
+
|
|
286
|
+
expect(context.messages.map(message => (message.role === "user" ? message.content : message.role))).toEqual([
|
|
287
|
+
"from B",
|
|
288
|
+
"from A",
|
|
289
|
+
]);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("omits a terminal aborted assistant from normal context", () => {
|
|
293
|
+
const context = buildSessionContext([
|
|
294
|
+
userEntry("user", null, "continue", 1),
|
|
295
|
+
assistantEntry("assistant", "user", "aborted", "partial unsafe replay", 2),
|
|
296
|
+
]);
|
|
297
|
+
|
|
298
|
+
expect(context.messages.some(message => message.role === "assistant")).toBe(false);
|
|
299
|
+
expectUserTail(context.messages, "continue");
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("omits an earlier aborted assistant before a later user from normal context", () => {
|
|
303
|
+
const context = buildSessionContext([
|
|
304
|
+
userEntry("user-1", null, "first prompt", 1),
|
|
305
|
+
assistantEntry("assistant", "user-1", "aborted", "partial unsafe replay", 2),
|
|
306
|
+
userEntry("user-2", "assistant", "retry", 3),
|
|
307
|
+
]);
|
|
308
|
+
|
|
309
|
+
expect(context.messages.some(message => message.role === "assistant")).toBe(false);
|
|
310
|
+
expectUserTail(context.messages, "retry");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("preserves a terminal aborted assistant in transcript mode", () => {
|
|
314
|
+
const context = buildSessionContext(
|
|
315
|
+
[
|
|
316
|
+
userEntry("user", null, "continue", 1),
|
|
317
|
+
assistantEntry("assistant", "user", "aborted", "visible transcript error", 2),
|
|
318
|
+
],
|
|
319
|
+
undefined,
|
|
320
|
+
undefined,
|
|
321
|
+
{ transcript: true },
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
const assistant = context.messages.find(message => message.role === "assistant");
|
|
325
|
+
expect(assistant?.role).toBe("assistant");
|
|
326
|
+
if (assistant?.role !== "assistant") {
|
|
327
|
+
throw new Error(`Expected transcript assistant, received ${assistant?.role ?? "none"}`);
|
|
328
|
+
}
|
|
329
|
+
expect(assistant.stopReason).toBe("aborted");
|
|
330
|
+
expect(assistant.content).toEqual([{ type: "text", text: "visible transcript error" }]);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("omits a terminal error assistant from normal context", () => {
|
|
334
|
+
const context = buildSessionContext([
|
|
335
|
+
userEntry("user", null, "retry with smaller input", 1),
|
|
336
|
+
assistantEntry("assistant", "user", "error", "provider rejected the request", 2),
|
|
337
|
+
]);
|
|
338
|
+
|
|
339
|
+
expect(context.messages.some(message => message.role === "assistant")).toBe(false);
|
|
340
|
+
expectUserTail(context.messages, "retry with smaller input");
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("keeps an aborted assistant when hidden interrupted-thinking continuity follows it", () => {
|
|
344
|
+
const context = buildSessionContext([
|
|
345
|
+
userEntry("user", null, "keep reasoning continuity", 1),
|
|
346
|
+
assistantEntry("assistant", "user", "aborted", "partial answer before interrupt", 2),
|
|
347
|
+
hiddenContinuityEntry("continuity", "assistant"),
|
|
348
|
+
]);
|
|
349
|
+
|
|
350
|
+
const assistant = context.messages.find(message => message.role === "assistant");
|
|
351
|
+
expect(assistant?.role).toBe("assistant");
|
|
352
|
+
if (assistant?.role !== "assistant") {
|
|
353
|
+
throw new Error(`Expected assistant before continuity, received ${assistant?.role ?? "none"}`);
|
|
354
|
+
}
|
|
355
|
+
expect(assistant.stopReason).toBe("aborted");
|
|
356
|
+
expect(context.messages.at(-1)?.role).toBe("custom");
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("drops synthetic tool results paired with a dropped failed tool-call turn", () => {
|
|
360
|
+
const context = buildSessionContext([
|
|
361
|
+
userEntry("user", null, "write the plan", 1),
|
|
362
|
+
toolCallAssistantEntry("assistant", "user", "error", "call-1", 2),
|
|
363
|
+
syntheticToolResultEntry("result", "assistant", "call-1", 3),
|
|
364
|
+
]);
|
|
365
|
+
|
|
366
|
+
expect(context.messages.map(message => message.role)).toEqual(["user"]);
|
|
367
|
+
expectUserTail(context.messages, "write the plan");
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("keeps the failed tool-call turn and its result in transcript mode", () => {
|
|
371
|
+
const context = buildSessionContext(
|
|
372
|
+
[
|
|
373
|
+
userEntry("user", null, "write the plan", 1),
|
|
374
|
+
toolCallAssistantEntry("assistant", "user", "error", "call-1", 2),
|
|
375
|
+
syntheticToolResultEntry("result", "assistant", "call-1", 3),
|
|
376
|
+
],
|
|
377
|
+
undefined,
|
|
378
|
+
undefined,
|
|
379
|
+
{ transcript: true, keepDanglingToolCalls: true },
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
expect(context.messages.map(message => message.role)).toEqual(["user", "assistant", "toolResult"]);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
createBranchSummaryMessage,
|
|
6
6
|
createCompactionSummaryMessage,
|
|
7
7
|
createCustomMessage,
|
|
8
|
+
INTERRUPTED_THINKING_MESSAGE_TYPE,
|
|
8
9
|
isCustomMessageContent,
|
|
9
10
|
normalizeCustomMessagePayload,
|
|
10
11
|
} from "./messages";
|
|
@@ -197,10 +198,13 @@ export function buildSessionContext(
|
|
|
197
198
|
};
|
|
198
199
|
}
|
|
199
200
|
|
|
200
|
-
// Walk from leaf to root, collecting path
|
|
201
|
+
// Walk from leaf to root, collecting path. Corrupt/pre-fix files can contain
|
|
202
|
+
// parent cycles; stop at the first repeat so session load is bounded.
|
|
201
203
|
const path: SessionEntry[] = [];
|
|
204
|
+
const seenPathIds = new Set<string>();
|
|
202
205
|
let current: SessionEntry | undefined = leaf;
|
|
203
|
-
while (current) {
|
|
206
|
+
while (current && !seenPathIds.has(current.id)) {
|
|
207
|
+
seenPathIds.add(current.id);
|
|
204
208
|
path.push(current);
|
|
205
209
|
current = current.parentId ? byId.get(current.parentId) : undefined;
|
|
206
210
|
}
|
|
@@ -498,6 +502,41 @@ export function buildSessionContext(
|
|
|
498
502
|
}
|
|
499
503
|
}
|
|
500
504
|
|
|
505
|
+
// Error/abort assistant turns are transcript events, not safe assistant
|
|
506
|
+
// turns to replay into the next provider request. Drop them even when a
|
|
507
|
+
// later user message follows through non-context entries (`session_exit`,
|
|
508
|
+
// labels, etc.); otherwise a resumed session replays a dead partial turn
|
|
509
|
+
// and can spend minutes reprocessing old context before the new prompt.
|
|
510
|
+
// Keep the interrupted-thinking continuity pair: convertToLlm strips the
|
|
511
|
+
// unsafe trailing thinking from that assistant and sends the hidden
|
|
512
|
+
// continuity note instead.
|
|
513
|
+
if (!options?.transcript) {
|
|
514
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
515
|
+
const message = messages[i];
|
|
516
|
+
if (message?.role !== "assistant") continue;
|
|
517
|
+
if (message.stopReason !== "aborted" && message.stopReason !== "error") continue;
|
|
518
|
+
const next = messages[i + 1];
|
|
519
|
+
if (next?.role === "custom" && next.customType === INTERRUPTED_THINKING_MESSAGE_TYPE) continue;
|
|
520
|
+
// A failed turn that emitted tool calls persists paired synthetic
|
|
521
|
+
// tool_result placeholders after it. Dropping only the assistant would
|
|
522
|
+
// strand those results with no preceding tool_use — a shape providers
|
|
523
|
+
// reject — so remove the paired results alongside the turn.
|
|
524
|
+
const droppedToolCallIds = new Set<string>();
|
|
525
|
+
for (const block of message.content) {
|
|
526
|
+
if (block.type === "toolCall") droppedToolCallIds.add(block.id);
|
|
527
|
+
}
|
|
528
|
+
messages.splice(i, 1);
|
|
529
|
+
if (droppedToolCallIds.size > 0) {
|
|
530
|
+
for (let j = messages.length - 1; j >= i; j--) {
|
|
531
|
+
const candidate = messages[j];
|
|
532
|
+
if (candidate?.role === "toolResult" && droppedToolCallIds.has(candidate.toolCallId)) {
|
|
533
|
+
messages.splice(j, 1);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
501
540
|
return {
|
|
502
541
|
messages,
|
|
503
542
|
cacheMissExplainedAt: options?.transcript ? cacheMissExplainedAt : undefined,
|
|
@@ -306,10 +306,12 @@ export async function resolveBlobRefsInEntries(entries: FileEntry[], blobStore:
|
|
|
306
306
|
}
|
|
307
307
|
|
|
308
308
|
/**
|
|
309
|
-
* Read-only
|
|
310
|
-
* current version, resolve blob refs, and build the
|
|
311
|
-
* persisted leaf path (last entry).
|
|
312
|
-
*
|
|
309
|
+
* Read-only transcript view of a session file: load entries, migrate to the
|
|
310
|
+
* current version, resolve blob refs, and build the display transcript along
|
|
311
|
+
* the persisted leaf path (last entry). Uses transcript mode (collapsed to the
|
|
312
|
+
* latest compaction) so failed/aborted tail turns stay visible, unlike the
|
|
313
|
+
* provider-context builder which drops them. Does NOT create a writer or take
|
|
314
|
+
* the session lock — safe to call against a file another session is writing.
|
|
313
315
|
*/
|
|
314
316
|
export async function loadSessionMessagesReadOnly(filePath: string): Promise<AgentMessage[]> {
|
|
315
317
|
const entries = await loadEntriesFromFile(filePath);
|
|
@@ -317,5 +319,8 @@ export async function loadSessionMessagesReadOnly(filePath: string): Promise<Age
|
|
|
317
319
|
migrateToCurrentVersion(entries);
|
|
318
320
|
await resolveBlobRefsInEntries(entries, new BlobStore(getBlobsDir()));
|
|
319
321
|
const sessionEntries = entries.filter((e): e is SessionEntry => e.type !== "session");
|
|
320
|
-
return buildSessionContext(sessionEntries
|
|
322
|
+
return buildSessionContext(sessionEntries, undefined, undefined, {
|
|
323
|
+
transcript: true,
|
|
324
|
+
collapseCompactedHistory: true,
|
|
325
|
+
}).messages;
|
|
321
326
|
}
|
|
@@ -124,7 +124,9 @@ function renderUsageReports(
|
|
|
124
124
|
);
|
|
125
125
|
lines.push(` ${renderAsciiBar(limit.amount.usedFraction)}`);
|
|
126
126
|
if (limit.window?.resetsAt && limit.window.resetsAt > nowMs) {
|
|
127
|
-
lines.push(
|
|
127
|
+
lines.push(
|
|
128
|
+
` ${limit.window.resetLabel ?? "resets"} in ${formatDuration(limit.window.resetsAt - nowMs)}`,
|
|
129
|
+
);
|
|
128
130
|
}
|
|
129
131
|
if (limit.notes && limit.notes.length > 0)
|
|
130
132
|
lines.push(
|
package/src/task/executor.ts
CHANGED
|
@@ -1578,11 +1578,13 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
|
1578
1578
|
}
|
|
1579
1579
|
if (event.type === "retry_fallback_applied") {
|
|
1580
1580
|
progress.resolvedModel = event.to;
|
|
1581
|
+
progress.resolvedModelIsFallback = true;
|
|
1581
1582
|
scheduleProgress(true);
|
|
1582
1583
|
return;
|
|
1583
1584
|
}
|
|
1584
1585
|
if (event.type === "retry_fallback_succeeded") {
|
|
1585
1586
|
progress.resolvedModel = event.model;
|
|
1587
|
+
progress.resolvedModelIsFallback = true;
|
|
1586
1588
|
scheduleProgress(true);
|
|
1587
1589
|
return;
|
|
1588
1590
|
}
|
|
@@ -1989,6 +1991,7 @@ async function finalizeRunResult(args: FinalizeRunArgs): Promise<SingleResult> {
|
|
|
1989
1991
|
contextWindow: progress.contextWindow,
|
|
1990
1992
|
modelOverride,
|
|
1991
1993
|
resolvedModel: progress.resolvedModel,
|
|
1994
|
+
resolvedModelIsFallback: progress.resolvedModelIsFallback,
|
|
1992
1995
|
error: exitCode !== 0 && stderr ? stderr : undefined,
|
|
1993
1996
|
aborted: wasAborted,
|
|
1994
1997
|
abortReason: finalAbortReason,
|
package/src/task/index.ts
CHANGED
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
// Import review tools for side effects (registers subagent tool handlers)
|
|
42
42
|
import "../tools/review";
|
|
43
43
|
import type { AsyncJobManager } from "../async";
|
|
44
|
+
import { hasResolvableTranscript } from "../internal-urls/registry-helpers";
|
|
44
45
|
import { AgentRegistry } from "../registry/agent-registry";
|
|
45
46
|
import { type DiscoveryResult, discoverAgents } from "./discovery";
|
|
46
47
|
import { generateTaskName } from "./name-generator";
|
|
@@ -160,6 +161,7 @@ export function formatResultOutputFallback(result: Pick<SingleResult, "output" |
|
|
|
160
161
|
function renderDescription(
|
|
161
162
|
agents: AgentDefinition[],
|
|
162
163
|
isolationEnabled: boolean,
|
|
164
|
+
applyIsolatedChanges: boolean,
|
|
163
165
|
disabledAgents: string[],
|
|
164
166
|
batchEnabled: boolean,
|
|
165
167
|
asyncEnabled: boolean,
|
|
@@ -187,6 +189,7 @@ function renderDescription(
|
|
|
187
189
|
defaultAgent: spawnPolicy.defaultAgent,
|
|
188
190
|
allowedAgentsText: spawnPolicy.allowedPromptText,
|
|
189
191
|
isolationEnabled,
|
|
192
|
+
applyIsolatedChanges,
|
|
190
193
|
batchEnabled,
|
|
191
194
|
asyncEnabled,
|
|
192
195
|
hasBlockingAgents: renderedAgents.some(agent => agent.blocking),
|
|
@@ -229,6 +232,19 @@ function validateShapeParams(batchEnabled: boolean, params: TaskParams): string
|
|
|
229
232
|
* policy later, in `spawnParamsFor`. Returns a problem description, or
|
|
230
233
|
* undefined when valid.
|
|
231
234
|
*/
|
|
235
|
+
function hasInvalidModelSelector(model: unknown): boolean {
|
|
236
|
+
if (model === undefined) return false;
|
|
237
|
+
const selectors = typeof model === "string" ? [model] : Array.isArray(model) ? model : undefined;
|
|
238
|
+
const materializedSelectors = selectors ? Array.from(selectors) : [];
|
|
239
|
+
return (
|
|
240
|
+
!selectors ||
|
|
241
|
+
materializedSelectors.length === 0 ||
|
|
242
|
+
materializedSelectors.some(
|
|
243
|
+
selector => typeof selector !== "string" || !selector.split(",").some(pattern => pattern.trim()),
|
|
244
|
+
)
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
232
248
|
function validateSpawnParams(params: TaskParams, batchEnabled: boolean): string | undefined {
|
|
233
249
|
const hasTask = typeof params.task === "string" && params.task.trim() !== "";
|
|
234
250
|
const tasks = params.tasks;
|
|
@@ -244,6 +260,9 @@ function validateSpawnParams(params: TaskParams, batchEnabled: boolean): string
|
|
|
244
260
|
if (!item || typeof item.task !== "string" || item.task.trim() === "") {
|
|
245
261
|
return `Task ${i + 1}${item?.name ? ` (\`${item.name}\`)` : ""} is missing \`task\`. Every task needs complete, self-contained instructions.`;
|
|
246
262
|
}
|
|
263
|
+
if (hasInvalidModelSelector(item.model)) {
|
|
264
|
+
return `Task ${i + 1}${item.name ? ` (\`${item.name}\`)` : ""} has an invalid \`model\`. Provide a non-empty selector or a non-empty array of non-empty selectors.`;
|
|
265
|
+
}
|
|
247
266
|
}
|
|
248
267
|
const seen = new Map<string, string>();
|
|
249
268
|
for (const item of tasks) {
|
|
@@ -266,6 +285,9 @@ function validateSpawnParams(params: TaskParams, batchEnabled: boolean): string
|
|
|
266
285
|
? "Missing `tasks`. Provide a `tasks` array (one subagent per item) with a shared `context`."
|
|
267
286
|
: "Missing `task`. Provide complete, self-contained instructions for the agent.";
|
|
268
287
|
}
|
|
288
|
+
if (hasInvalidModelSelector(params.model)) {
|
|
289
|
+
return "Invalid `model`. Provide a non-empty selector or a non-empty array of non-empty selectors.";
|
|
290
|
+
}
|
|
269
291
|
return undefined;
|
|
270
292
|
}
|
|
271
293
|
|
|
@@ -279,7 +301,7 @@ function resolveSpawnItems(params: TaskParams): TaskItem[] {
|
|
|
279
301
|
if (Array.isArray(params.tasks) && params.tasks.length > 0) {
|
|
280
302
|
return params.tasks;
|
|
281
303
|
}
|
|
282
|
-
const item: TaskItem = { name: params.name, agent: params.agent, task: params.task };
|
|
304
|
+
const item: TaskItem = { name: params.name, agent: params.agent, task: params.task, model: params.model };
|
|
283
305
|
if ("outputSchema" in params) item.outputSchema = params.outputSchema;
|
|
284
306
|
if ("schemaMode" in params) item.schemaMode = params.schemaMode;
|
|
285
307
|
if ("isolated" in params) item.isolated = params.isolated;
|
|
@@ -299,6 +321,7 @@ function spawnParamsFor(params: TaskParams, item: TaskItem, defaultAgent: string
|
|
|
299
321
|
const spawn: TaskParams = { agent: item.agent?.trim() || defaultAgent };
|
|
300
322
|
if (item.name !== undefined) spawn.name = item.name;
|
|
301
323
|
if (item.task !== undefined) spawn.task = item.task;
|
|
324
|
+
if (item.model !== undefined) spawn.model = item.model;
|
|
302
325
|
if (params.context !== undefined) spawn.context = params.context;
|
|
303
326
|
if ("outputSchema" in item) spawn.outputSchema = item.outputSchema;
|
|
304
327
|
if ("schemaMode" in item) spawn.schemaMode = item.schemaMode;
|
|
@@ -469,6 +492,14 @@ function discoverAgentsForCreate(cwd: string): Promise<DiscoveryResult> {
|
|
|
469
492
|
return pending;
|
|
470
493
|
}
|
|
471
494
|
|
|
495
|
+
function formatModelForApproval(model: unknown): string | undefined {
|
|
496
|
+
const selectors = typeof model === "string" ? [model] : Array.isArray(model) ? model : [];
|
|
497
|
+
const normalized = selectors.filter(
|
|
498
|
+
(selector): selector is string => typeof selector === "string" && !!selector.trim(),
|
|
499
|
+
);
|
|
500
|
+
return normalized.length > 0 ? truncateForPrompt(normalized.join(" → ")) : undefined;
|
|
501
|
+
}
|
|
502
|
+
|
|
472
503
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
473
504
|
// Tool Class
|
|
474
505
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -492,6 +523,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
492
523
|
if (typeof params.name === "string" && params.name.trim()) {
|
|
493
524
|
lines.push(`Name: ${truncateForPrompt(params.name)}`);
|
|
494
525
|
}
|
|
526
|
+
const model = formatModelForApproval(params.model);
|
|
527
|
+
if (model) lines.push(`Model: ${model}`);
|
|
495
528
|
if (typeof params.task === "string") {
|
|
496
529
|
lines.push(`Task:\n${truncateForPrompt(params.task)}`);
|
|
497
530
|
}
|
|
@@ -507,6 +540,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
507
540
|
if (typeof firstTask.agent === "string" && firstTask.agent.trim()) {
|
|
508
541
|
lines.push(`Agent: ${truncateForPrompt(firstTask.agent)}`);
|
|
509
542
|
}
|
|
543
|
+
const itemModel = formatModelForApproval(firstTask.model);
|
|
544
|
+
if (itemModel) lines.push(`Model: ${itemModel}`);
|
|
510
545
|
if (typeof firstTask.task === "string") {
|
|
511
546
|
lines.push(`Task:\n${truncateForPrompt(firstTask.task)}`);
|
|
512
547
|
}
|
|
@@ -566,6 +601,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
566
601
|
return renderDescription(
|
|
567
602
|
this.#discoveredAgents,
|
|
568
603
|
!planMode && isolationMode !== "none",
|
|
604
|
+
this.session.settings.get("task.isolation.apply"),
|
|
569
605
|
disabledAgents,
|
|
570
606
|
this.#isBatchEnabled(),
|
|
571
607
|
this.session.settings.get("async.enabled"),
|
|
@@ -612,6 +648,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
612
648
|
assignment: (params.task ?? "").trim(),
|
|
613
649
|
context: this.#isBatchEnabled() ? params.context?.trim() || undefined : undefined,
|
|
614
650
|
agent: params.agent,
|
|
651
|
+
model: params.model,
|
|
615
652
|
...(Object.hasOwn(params, "outputSchema") ? { outputSchema: params.outputSchema } : {}),
|
|
616
653
|
...(Object.hasOwn(params, "schemaMode") ? { schemaMode: params.schemaMode } : {}),
|
|
617
654
|
...("isolated" in params ? { isolation: { requested: params.isolated } } : {}),
|
|
@@ -1043,14 +1080,17 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1043
1080
|
}): string {
|
|
1044
1081
|
const { manager, toolCallId, spawnParams, agentId, progress, ircEnabled, buildDetails, onUpdate, onSettled } =
|
|
1045
1082
|
options;
|
|
1046
|
-
const buildFollowUpHint = (aborted: boolean): string => {
|
|
1083
|
+
const buildFollowUpHint = async (aborted: boolean): Promise<string> => {
|
|
1047
1084
|
if (aborted) {
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1085
|
+
const ref = AgentRegistry.global().get(agentId);
|
|
1086
|
+
const transcript = (await hasResolvableTranscript(agentId))
|
|
1087
|
+
? `transcript at history://${agentId}`
|
|
1088
|
+
: "transcript unavailable";
|
|
1089
|
+
if (ref?.status === "idle" || ref?.status === "parked") {
|
|
1050
1090
|
const followUp = ircEnabled ? "message it via `hub` to resume; " : "";
|
|
1051
|
-
return `\n\n${agentId} was stopped but is still resumable — ${followUp}transcript
|
|
1091
|
+
return `\n\n${agentId} was stopped but is still resumable — ${followUp}${transcript}`;
|
|
1052
1092
|
}
|
|
1053
|
-
return `\n\n${agentId} was aborted — transcript
|
|
1093
|
+
return `\n\n${agentId} was aborted — ${transcript}`;
|
|
1054
1094
|
}
|
|
1055
1095
|
const followUp = ircEnabled ? "message it via `hub` to follow up; " : "";
|
|
1056
1096
|
return `\n\n${agentId} is now idle — ${followUp}transcript at history://${agentId}`;
|
|
@@ -1105,6 +1145,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1105
1145
|
// and running counters without reverting the "running"
|
|
1106
1146
|
// status back to the subagent's initial "pending" snapshot.
|
|
1107
1147
|
progress.resolvedModel = nextProgress.resolvedModel;
|
|
1148
|
+
progress.resolvedModelIsFallback = nextProgress.resolvedModelIsFallback;
|
|
1108
1149
|
progress.tokens = nextProgress.tokens;
|
|
1109
1150
|
progress.requests = nextProgress.requests;
|
|
1110
1151
|
progress.contextTokens = nextProgress.contextTokens;
|
|
@@ -1149,15 +1190,17 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1149
1190
|
progress.retryState = undefined;
|
|
1150
1191
|
if (singleResult?.resolvedModel) {
|
|
1151
1192
|
progress.resolvedModel = singleResult.resolvedModel;
|
|
1193
|
+
progress.resolvedModelIsFallback = singleResult.resolvedModelIsFallback;
|
|
1152
1194
|
} else {
|
|
1153
1195
|
delete progress.resolvedModel;
|
|
1196
|
+
delete progress.resolvedModelIsFallback;
|
|
1154
1197
|
}
|
|
1155
1198
|
onSettled?.(resultFailed);
|
|
1156
1199
|
const statusText = resultFailed
|
|
1157
1200
|
? `Background task ${agentId} failed.`
|
|
1158
1201
|
: `Background task ${agentId} complete.`;
|
|
1159
1202
|
await reportProgress(statusText, buildDetails() as unknown as Record<string, unknown>);
|
|
1160
|
-
const deliveryText = `${finalText}${buildFollowUpHint(singleResult?.aborted === true)}`;
|
|
1203
|
+
const deliveryText = `${finalText}${await buildFollowUpHint(singleResult?.aborted === true)}`;
|
|
1161
1204
|
if (resultFailed) {
|
|
1162
1205
|
// Mark the job itself failed; the failed agent stays interrogable.
|
|
1163
1206
|
throw new TaskJobError(deliveryText);
|
|
@@ -1173,7 +1216,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1173
1216
|
const statusText = `Background task ${agentId} failed.`;
|
|
1174
1217
|
await reportProgress(statusText, buildDetails() as unknown as Record<string, unknown>);
|
|
1175
1218
|
const message = error instanceof Error ? error.message : String(error);
|
|
1176
|
-
const hint = AgentRegistry.global().get(agentId) ? buildFollowUpHint(false) : "";
|
|
1219
|
+
const hint = AgentRegistry.global().get(agentId) ? await buildFollowUpHint(false) : "";
|
|
1177
1220
|
throw new TaskJobError(`${message}${hint}`);
|
|
1178
1221
|
} finally {
|
|
1179
1222
|
releasePermit();
|
|
@@ -1383,6 +1426,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1383
1426
|
assignment,
|
|
1384
1427
|
context,
|
|
1385
1428
|
agent: params.agent,
|
|
1429
|
+
model: params.model,
|
|
1386
1430
|
...(Object.hasOwn(params, "outputSchema") ? { outputSchema: params.outputSchema } : {}),
|
|
1387
1431
|
...(Object.hasOwn(params, "schemaMode") ? { schemaMode: params.schemaMode } : {}),
|
|
1388
1432
|
identity: { id: preAllocatedId, label: params.name },
|
|
@@ -300,7 +300,9 @@ export async function resolveEffectiveSubagentPolicy(
|
|
|
300
300
|
planMode,
|
|
301
301
|
isIsolated,
|
|
302
302
|
mergeMode: request.isolation?.merge ?? request.session.settings.get("task.isolation.merge"),
|
|
303
|
-
applyChanges:
|
|
303
|
+
applyChanges:
|
|
304
|
+
request.isolation?.apply ??
|
|
305
|
+
(request.invocationKind === "task" ? request.session.settings.get("task.isolation.apply") : true),
|
|
304
306
|
enableLsp:
|
|
305
307
|
!planMode &&
|
|
306
308
|
(request.enableLsp ?? ((request.session.enableLsp ?? true) && request.session.settings.get("task.enableLsp"))),
|
package/src/task/types.ts
CHANGED
|
@@ -113,6 +113,7 @@ export const taskItemSchema = type({
|
|
|
113
113
|
"name?": "string",
|
|
114
114
|
agent: "string = 'task'",
|
|
115
115
|
task: "string",
|
|
116
|
+
"model?": "string | string[]",
|
|
116
117
|
"outputSchema?": outputSchemaInputSchema,
|
|
117
118
|
"schemaMode?": '"permissive" | "strict"',
|
|
118
119
|
"+": "delete",
|
|
@@ -121,6 +122,7 @@ const taskItemSchemaIsolated = type({
|
|
|
121
122
|
"name?": "string",
|
|
122
123
|
agent: "string = 'task'",
|
|
123
124
|
task: "string",
|
|
125
|
+
"model?": "string | string[]",
|
|
124
126
|
"outputSchema?": outputSchemaInputSchema,
|
|
125
127
|
"schemaMode?": '"permissive" | "strict"',
|
|
126
128
|
"isolated?": "boolean",
|
|
@@ -135,6 +137,8 @@ export interface TaskItem {
|
|
|
135
137
|
agent?: string;
|
|
136
138
|
/** The work; required by the schema. */
|
|
137
139
|
task?: string;
|
|
140
|
+
/** Explicit model selector or fallback chain for this spawn, including optional reasoning suffixes. */
|
|
141
|
+
model?: string | string[];
|
|
138
142
|
/** Caller-provided output schema; its presence overrides the selected agent's schema. */
|
|
139
143
|
outputSchema?: unknown;
|
|
140
144
|
/** Validation behavior for a caller-provided or inherited output schema. */
|
|
@@ -147,6 +151,7 @@ export const taskSchema = type({
|
|
|
147
151
|
"name?": "string",
|
|
148
152
|
agent: "string = 'task'",
|
|
149
153
|
task: "string",
|
|
154
|
+
"model?": "string | string[]",
|
|
150
155
|
"outputSchema?": outputSchemaInputSchema,
|
|
151
156
|
"schemaMode?": '"permissive" | "strict"',
|
|
152
157
|
"isolated?": "boolean",
|
|
@@ -156,6 +161,7 @@ const taskSchemaNoIsolation = type({
|
|
|
156
161
|
"name?": "string",
|
|
157
162
|
agent: "string = 'task'",
|
|
158
163
|
task: "string",
|
|
164
|
+
"model?": "string | string[]",
|
|
159
165
|
"outputSchema?": outputSchemaInputSchema,
|
|
160
166
|
"schemaMode?": '"permissive" | "strict"',
|
|
161
167
|
"+": "delete",
|
|
@@ -200,6 +206,7 @@ function createTaskSchema(options: {
|
|
|
200
206
|
"name?": "string",
|
|
201
207
|
agent,
|
|
202
208
|
task: "string",
|
|
209
|
+
"model?": "string | string[]",
|
|
203
210
|
"outputSchema?": outputSchemaInputSchema,
|
|
204
211
|
"schemaMode?": '"permissive" | "strict"',
|
|
205
212
|
"isolated?": "boolean",
|
|
@@ -215,6 +222,7 @@ function createTaskSchema(options: {
|
|
|
215
222
|
"name?": "string",
|
|
216
223
|
agent,
|
|
217
224
|
task: "string",
|
|
225
|
+
"model?": "string | string[]",
|
|
218
226
|
"outputSchema?": outputSchemaInputSchema,
|
|
219
227
|
"schemaMode?": '"permissive" | "strict"',
|
|
220
228
|
"+": "delete",
|
|
@@ -230,6 +238,7 @@ function createTaskSchema(options: {
|
|
|
230
238
|
"name?": "string",
|
|
231
239
|
agent,
|
|
232
240
|
task: "string",
|
|
241
|
+
"model?": "string | string[]",
|
|
233
242
|
"outputSchema?": outputSchemaInputSchema,
|
|
234
243
|
"schemaMode?": '"permissive" | "strict"',
|
|
235
244
|
"isolated?": "boolean",
|
|
@@ -240,6 +249,7 @@ function createTaskSchema(options: {
|
|
|
240
249
|
"name?": "string",
|
|
241
250
|
agent,
|
|
242
251
|
task: "string",
|
|
252
|
+
"model?": "string | string[]",
|
|
243
253
|
"outputSchema?": outputSchemaInputSchema,
|
|
244
254
|
"schemaMode?": '"permissive" | "strict"',
|
|
245
255
|
"+": "delete",
|
|
@@ -283,6 +293,8 @@ export interface TaskParams {
|
|
|
283
293
|
agent?: string;
|
|
284
294
|
/** The work (flat form). */
|
|
285
295
|
task?: string;
|
|
296
|
+
/** Explicit model selector or fallback chain for the spawn, including optional reasoning suffixes. */
|
|
297
|
+
model?: string | string[];
|
|
286
298
|
/** Caller-provided output schema; its presence overrides the selected agent's schema. */
|
|
287
299
|
outputSchema?: unknown;
|
|
288
300
|
/** Validation behavior for a caller-provided or inherited output schema. */
|
|
@@ -419,6 +431,8 @@ export interface AgentProgress {
|
|
|
419
431
|
modelOverride?: string | string[];
|
|
420
432
|
/** Resolved model display string in the form `<provider>/<id>`, optionally suffixed with `:<thinkingLevel>` when the level was set explicitly. Undefined when the model could not be resolved. */
|
|
421
433
|
resolvedModel?: string;
|
|
434
|
+
/** True when {@link resolvedModel} is the target of an active retry fallback (not the originally configured model). Lets observer-only UIs (collab guests, Agent Hub rows with no live session) flag the fallback and keep the provider. */
|
|
435
|
+
resolvedModelIsFallback?: boolean;
|
|
422
436
|
/** Data extracted by registered subprocess tool handlers (keyed by tool name) */
|
|
423
437
|
extractedToolData?: Record<string, unknown[]>;
|
|
424
438
|
/**
|
|
@@ -486,6 +500,8 @@ export interface SingleResult {
|
|
|
486
500
|
modelOverride?: string | string[];
|
|
487
501
|
/** Resolved model display string in the form `<provider>/<id>`, optionally suffixed with `:<thinkingLevel>` when the level was set explicitly. Omitted from tool-result JSON when undefined to keep wire payloads small. */
|
|
488
502
|
resolvedModel?: string;
|
|
503
|
+
/** True when {@link resolvedModel} is the target of an active retry fallback. Mirrors {@link AgentProgress.resolvedModelIsFallback} onto the settled result. */
|
|
504
|
+
resolvedModelIsFallback?: boolean;
|
|
489
505
|
error?: string;
|
|
490
506
|
aborted?: boolean;
|
|
491
507
|
abortReason?: string;
|
package/src/tools/hub/index.ts
CHANGED
|
@@ -283,7 +283,7 @@ export class HubTool implements AgentTool<typeof hubSchema, HubDetails> {
|
|
|
283
283
|
if (!params.ids?.length) {
|
|
284
284
|
return hubErrorResult('`ids` is required for op="cancel".', { op: "cancel", jobs: [] });
|
|
285
285
|
}
|
|
286
|
-
return executeCancel(this.session, manager, this.#ownerId(), params.ids);
|
|
286
|
+
return await executeCancel(this.session, manager, this.#ownerId(), params.ids);
|
|
287
287
|
}
|
|
288
288
|
case "jobs": {
|
|
289
289
|
const manager = this.session.asyncJobManager;
|