@oh-my-pi/pi-coding-agent 17.0.7 → 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 +91 -0
- package/dist/cli.js +5025 -4930
- package/dist/types/config/__tests__/model-registry.test.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +28 -1
- package/dist/types/config/model-resolver.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +50 -3
- package/dist/types/dap/client.d.ts +19 -0
- package/dist/types/extensibility/extensions/runner.d.ts +4 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- 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/extensibility/typebox.d.ts +4 -0
- package/dist/types/hindsight/client.d.ts +10 -0
- package/dist/types/hindsight/config.d.ts +8 -0
- 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 +16 -3
- package/dist/types/mcp/types.d.ts +6 -0
- package/dist/types/mnemopi/state.d.ts +2 -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/components/user-message.d.ts +25 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +8 -0
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -1
- 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/modes/types.d.ts +14 -1
- package/dist/types/modes/utils/ui-helpers.d.ts +12 -8
- package/dist/types/sdk.d.ts +1 -0
- package/dist/types/session/agent-session-error-log.test.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +86 -3
- package/dist/types/session/session-loader.d.ts +6 -4
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/task/index.d.ts +1 -0
- package/dist/types/task/prewalk.d.ts +3 -0
- package/dist/types/task/types.d.ts +14 -0
- package/dist/types/tools/ask.d.ts +10 -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/tools/tool-timeouts.d.ts +8 -2
- package/dist/types/utils/changelog.d.ts +4 -6
- 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 -13
- package/scripts/legacy-pi-virtual-module.ts +1 -1
- package/src/cli/grievances-cli.ts +2 -2
- package/src/cli/models-cli.ts +37 -17
- package/src/cli/update-cli.ts +14 -1
- package/src/cli/usage-cli.ts +1 -1
- package/src/config/__tests__/model-registry.test.ts +147 -0
- package/src/config/model-registry.ts +87 -24
- package/src/config/model-resolver.ts +56 -32
- package/src/config/settings-schema.ts +46 -3
- package/src/config/settings.ts +68 -5
- package/src/dap/client.ts +86 -7
- package/src/edit/diff.ts +4 -4
- package/src/eval/py/prelude.py +5 -5
- package/src/export/html/template.js +23 -9
- package/src/extensibility/extensions/runner.ts +9 -4
- package/src/extensibility/extensions/types.ts +3 -0
- 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/extensibility/typebox.ts +22 -9
- package/src/hindsight/client.test.ts +42 -0
- package/src/hindsight/client.ts +40 -3
- package/src/hindsight/config.ts +18 -0
- package/src/internal-urls/registry-helpers.ts +40 -0
- package/src/launch/broker.ts +31 -5
- package/src/lsp/client.ts +23 -0
- package/src/lsp/clients/biome-client.ts +3 -4
- package/src/lsp/index.ts +26 -8
- package/src/main.ts +16 -5
- package/src/mcp/manager.ts +39 -8
- package/src/mcp/render.ts +94 -35
- package/src/mcp/tool-bridge.ts +116 -11
- package/src/mcp/types.ts +7 -0
- package/src/mnemopi/state.ts +70 -3
- package/src/modes/components/agent-dashboard.ts +6 -2
- package/src/modes/components/agent-hub.ts +26 -9
- package/src/modes/components/chat-transcript-builder.ts +13 -2
- package/src/modes/components/diff.ts +2 -2
- package/src/modes/components/oauth-selector.ts +24 -7
- package/src/modes/components/plan-review-overlay.ts +20 -4
- 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/components/user-message.ts +100 -1
- package/src/modes/controllers/command-controller.ts +36 -12
- package/src/modes/controllers/event-controller.ts +4 -1
- package/src/modes/controllers/extension-ui-controller.ts +18 -0
- package/src/modes/controllers/input-controller.ts +47 -12
- package/src/modes/controllers/mcp-command-controller.ts +70 -40
- package/src/modes/controllers/selector-controller.ts +82 -6
- package/src/modes/interactive-mode.ts +39 -4
- 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/modes/types.ts +16 -3
- package/src/modes/utils/ui-helpers.ts +40 -20
- package/src/prompts/goals/guided-goal-system.md +24 -3
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/task.md +12 -2
- package/src/sdk.ts +179 -51
- package/src/session/agent-session-error-log.test.ts +59 -0
- package/src/session/agent-session.ts +351 -46
- 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/system-prompt.ts +1 -2
- package/src/task/executor.ts +44 -27
- package/src/task/index.ts +63 -8
- package/src/task/prewalk.ts +6 -0
- package/src/task/structured-subagent.ts +3 -1
- package/src/task/types.ts +16 -0
- package/src/task/worktree.ts +25 -11
- package/src/tools/ask.ts +15 -0
- package/src/tools/bash.ts +14 -6
- package/src/tools/browser.ts +1 -1
- package/src/tools/debug.ts +1 -1
- package/src/tools/eval.ts +4 -5
- package/src/tools/fetch.ts +1 -1
- package/src/tools/hub/index.ts +1 -1
- package/src/tools/hub/jobs.ts +67 -8
- package/src/tools/hub/launch.ts +7 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/report-tool-issue.ts +79 -28
- package/src/tools/tool-timeouts.ts +10 -3
- package/src/tools/write.ts +31 -0
- package/src/utils/changelog.ts +54 -58
- package/src/utils/git.ts +57 -49
- package/src/web/search/providers/firecrawl.ts +46 -13
- package/src/web/search/types.ts +5 -1
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
|
}
|
|
@@ -520,6 +555,17 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
520
555
|
readonly summary = "Spawn subagents to complete delegated tasks";
|
|
521
556
|
readonly strict = false;
|
|
522
557
|
readonly loadMode = "essential";
|
|
558
|
+
// Arktype validates model calls against the active wire schema, but the flat
|
|
559
|
+
// single-spawn schema carries `"+": "delete"`: a batch `{ context, tasks[] }`
|
|
560
|
+
// payload has those keys stripped, then fails on the now-missing `task` with
|
|
561
|
+
// the misleading `task must be a string (was missing)`. That preempts the
|
|
562
|
+
// tool's own actionable shape checks (`validateShapeParams` /
|
|
563
|
+
// `validateSpawnParams`), which never run. Lenient validation forwards the
|
|
564
|
+
// raw args to `execute()` on any arktype failure so those checks surface the
|
|
565
|
+
// real reason ("enable task.batch, or use the flat `task` shape"). Valid
|
|
566
|
+
// calls still normalize through arktype; `execute()` resolves `agent`
|
|
567
|
+
// defaults independently, so the success path is unchanged.
|
|
568
|
+
readonly lenientArgValidation = true;
|
|
523
569
|
readonly renderResult = renderResult;
|
|
524
570
|
// Suppress the streaming call preview once a (partial or final) result exists
|
|
525
571
|
// so the task renders as ONE block that transitions in place — not a pending
|
|
@@ -555,6 +601,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
555
601
|
return renderDescription(
|
|
556
602
|
this.#discoveredAgents,
|
|
557
603
|
!planMode && isolationMode !== "none",
|
|
604
|
+
this.session.settings.get("task.isolation.apply"),
|
|
558
605
|
disabledAgents,
|
|
559
606
|
this.#isBatchEnabled(),
|
|
560
607
|
this.session.settings.get("async.enabled"),
|
|
@@ -601,6 +648,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
601
648
|
assignment: (params.task ?? "").trim(),
|
|
602
649
|
context: this.#isBatchEnabled() ? params.context?.trim() || undefined : undefined,
|
|
603
650
|
agent: params.agent,
|
|
651
|
+
model: params.model,
|
|
604
652
|
...(Object.hasOwn(params, "outputSchema") ? { outputSchema: params.outputSchema } : {}),
|
|
605
653
|
...(Object.hasOwn(params, "schemaMode") ? { schemaMode: params.schemaMode } : {}),
|
|
606
654
|
...("isolated" in params ? { isolation: { requested: params.isolated } } : {}),
|
|
@@ -1032,14 +1080,17 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1032
1080
|
}): string {
|
|
1033
1081
|
const { manager, toolCallId, spawnParams, agentId, progress, ircEnabled, buildDetails, onUpdate, onSettled } =
|
|
1034
1082
|
options;
|
|
1035
|
-
const buildFollowUpHint = (aborted: boolean): string => {
|
|
1083
|
+
const buildFollowUpHint = async (aborted: boolean): Promise<string> => {
|
|
1036
1084
|
if (aborted) {
|
|
1037
|
-
const
|
|
1038
|
-
|
|
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") {
|
|
1039
1090
|
const followUp = ircEnabled ? "message it via `hub` to resume; " : "";
|
|
1040
|
-
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}`;
|
|
1041
1092
|
}
|
|
1042
|
-
return `\n\n${agentId} was aborted — transcript
|
|
1093
|
+
return `\n\n${agentId} was aborted — ${transcript}`;
|
|
1043
1094
|
}
|
|
1044
1095
|
const followUp = ircEnabled ? "message it via `hub` to follow up; " : "";
|
|
1045
1096
|
return `\n\n${agentId} is now idle — ${followUp}transcript at history://${agentId}`;
|
|
@@ -1094,6 +1145,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1094
1145
|
// and running counters without reverting the "running"
|
|
1095
1146
|
// status back to the subagent's initial "pending" snapshot.
|
|
1096
1147
|
progress.resolvedModel = nextProgress.resolvedModel;
|
|
1148
|
+
progress.resolvedModelIsFallback = nextProgress.resolvedModelIsFallback;
|
|
1097
1149
|
progress.tokens = nextProgress.tokens;
|
|
1098
1150
|
progress.requests = nextProgress.requests;
|
|
1099
1151
|
progress.contextTokens = nextProgress.contextTokens;
|
|
@@ -1138,15 +1190,17 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1138
1190
|
progress.retryState = undefined;
|
|
1139
1191
|
if (singleResult?.resolvedModel) {
|
|
1140
1192
|
progress.resolvedModel = singleResult.resolvedModel;
|
|
1193
|
+
progress.resolvedModelIsFallback = singleResult.resolvedModelIsFallback;
|
|
1141
1194
|
} else {
|
|
1142
1195
|
delete progress.resolvedModel;
|
|
1196
|
+
delete progress.resolvedModelIsFallback;
|
|
1143
1197
|
}
|
|
1144
1198
|
onSettled?.(resultFailed);
|
|
1145
1199
|
const statusText = resultFailed
|
|
1146
1200
|
? `Background task ${agentId} failed.`
|
|
1147
1201
|
: `Background task ${agentId} complete.`;
|
|
1148
1202
|
await reportProgress(statusText, buildDetails() as unknown as Record<string, unknown>);
|
|
1149
|
-
const deliveryText = `${finalText}${buildFollowUpHint(singleResult?.aborted === true)}`;
|
|
1203
|
+
const deliveryText = `${finalText}${await buildFollowUpHint(singleResult?.aborted === true)}`;
|
|
1150
1204
|
if (resultFailed) {
|
|
1151
1205
|
// Mark the job itself failed; the failed agent stays interrogable.
|
|
1152
1206
|
throw new TaskJobError(deliveryText);
|
|
@@ -1162,7 +1216,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1162
1216
|
const statusText = `Background task ${agentId} failed.`;
|
|
1163
1217
|
await reportProgress(statusText, buildDetails() as unknown as Record<string, unknown>);
|
|
1164
1218
|
const message = error instanceof Error ? error.message : String(error);
|
|
1165
|
-
const hint = AgentRegistry.global().get(agentId) ? buildFollowUpHint(false) : "";
|
|
1219
|
+
const hint = AgentRegistry.global().get(agentId) ? await buildFollowUpHint(false) : "";
|
|
1166
1220
|
throw new TaskJobError(`${message}${hint}`);
|
|
1167
1221
|
} finally {
|
|
1168
1222
|
releasePermit();
|
|
@@ -1372,6 +1426,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1372
1426
|
assignment,
|
|
1373
1427
|
context,
|
|
1374
1428
|
agent: params.agent,
|
|
1429
|
+
model: params.model,
|
|
1375
1430
|
...(Object.hasOwn(params, "outputSchema") ? { outputSchema: params.outputSchema } : {}),
|
|
1376
1431
|
...(Object.hasOwn(params, "schemaMode") ? { schemaMode: params.schemaMode } : {}),
|
|
1377
1432
|
identity: { id: preAllocatedId, label: params.name },
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AgentDefinition } from "./types";
|
|
2
|
+
|
|
3
|
+
/** Resolve an agent's prewalk default, including the bundled task opt-in. */
|
|
4
|
+
export function resolveAgentPrewalkDefault(agent: AgentDefinition, taskPrewalk: boolean): boolean | string | undefined {
|
|
5
|
+
return agent.prewalk ?? (taskPrewalk && agent.source === "bundled" && agent.name === "task" ? true : undefined);
|
|
6
|
+
}
|
|
@@ -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/task/worktree.ts
CHANGED
|
@@ -116,7 +116,16 @@ async function captureRepoBaseline(repoRoot: string): Promise<RepoBaseline> {
|
|
|
116
116
|
return { repoRoot, headCommit, staged, unstaged, untracked, untrackedPatch };
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
interface SyntheticTreeOptions {
|
|
120
|
+
readonly threeWay?: boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function writeSyntheticTree(
|
|
124
|
+
repoDir: string,
|
|
125
|
+
baseTreeish: string,
|
|
126
|
+
patches: readonly string[],
|
|
127
|
+
options: SyntheticTreeOptions = {},
|
|
128
|
+
): Promise<string> {
|
|
120
129
|
const tempIndex = path.join(os.tmpdir(), `omp-task-index-${Snowflake.next()}`);
|
|
121
130
|
try {
|
|
122
131
|
await git.readTree(repoDir, baseTreeish, {
|
|
@@ -127,6 +136,7 @@ async function writeSyntheticTree(repoDir: string, baseTreeish: string, patches:
|
|
|
127
136
|
await git.patch.applyText(repoDir, patch, {
|
|
128
137
|
cached: true,
|
|
129
138
|
env: { GIT_INDEX_FILE: tempIndex },
|
|
139
|
+
threeWay: options.threeWay,
|
|
130
140
|
});
|
|
131
141
|
}
|
|
132
142
|
return await git.writeTree(repoDir, {
|
|
@@ -653,11 +663,12 @@ async function replayFilteredAgentCommits(opts: FilteredAgentReplayOptions): Pro
|
|
|
653
663
|
try {
|
|
654
664
|
await git.worktree.add(opts.repoRoot, tmpDir, opts.branchName);
|
|
655
665
|
const agentCommits = await git.revList.range(opts.isolationDir, baselineSha, opts.isolationHead);
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
666
|
+
const baselineWip = [opts.baseline.root.staged, opts.baseline.root.unstaged, opts.baseline.root.untrackedPatch];
|
|
667
|
+
// Seed the parent ODB with the dirty-side blobs needed by `git apply
|
|
668
|
+
// --3way`. Isolation repositories can read parent objects, but the parent
|
|
669
|
+
// cannot read objects created only inside isolation.
|
|
670
|
+
await writeSyntheticTree(opts.repoRoot, baselineSha, baselineWip);
|
|
671
|
+
const dirtyBaselineTree = await writeSyntheticTree(opts.isolationDir, baselineSha, baselineWip);
|
|
661
672
|
let previousFilteredTree = baselineSha;
|
|
662
673
|
let filteredCommitsApplied = 0;
|
|
663
674
|
|
|
@@ -666,7 +677,9 @@ async function replayFilteredAgentCommits(opts: FilteredAgentReplayOptions): Pro
|
|
|
666
677
|
allowFailure: true,
|
|
667
678
|
binary: true,
|
|
668
679
|
});
|
|
669
|
-
const currentFilteredTree = await writeSyntheticTree(opts.repoRoot, baselineSha, [taskStatePatch]
|
|
680
|
+
const currentFilteredTree = await writeSyntheticTree(opts.repoRoot, baselineSha, [taskStatePatch], {
|
|
681
|
+
threeWay: true,
|
|
682
|
+
});
|
|
670
683
|
const commitPatch = await git.diff.tree(opts.repoRoot, previousFilteredTree, currentFilteredTree, {
|
|
671
684
|
allowFailure: true,
|
|
672
685
|
binary: true,
|
|
@@ -699,10 +712,11 @@ async function replayFilteredAgentCommits(opts: FilteredAgentReplayOptions): Pro
|
|
|
699
712
|
await commitPatchToBranchWorktree(tmpDir, opts.taskId, opts.rootPatch, msg, undefined, opts.baseline.root);
|
|
700
713
|
}
|
|
701
714
|
} else {
|
|
702
|
-
// A filtered commit landed;
|
|
703
|
-
//
|
|
704
|
-
|
|
705
|
-
|
|
715
|
+
// A filtered commit landed; reconstruct the final HEAD-derived tree
|
|
716
|
+
// with the same dirty-side blobs and 3-way synthesis used above.
|
|
717
|
+
const finalFilteredTree = await writeSyntheticTree(opts.repoRoot, baselineSha, [opts.rootPatch], {
|
|
718
|
+
threeWay: true,
|
|
719
|
+
});
|
|
706
720
|
const leftoverPatch = await git.diff.tree(opts.repoRoot, previousFilteredTree, finalFilteredTree, {
|
|
707
721
|
allowFailure: true,
|
|
708
722
|
binary: true,
|
package/src/tools/ask.ts
CHANGED
|
@@ -81,6 +81,21 @@ const askSchema = arkType({
|
|
|
81
81
|
|
|
82
82
|
export type AskToolInput = typeof askSchema.infer;
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Recover a validated `questions` payload from a persisted `ask` toolCall's
|
|
86
|
+
* `arguments`. Used by `/tree` re-answer (issue #5642): selecting a past
|
|
87
|
+
* `ask` toolResult re-opens the picker with the *original* questions, so the
|
|
88
|
+
* new answer branches as a sibling instead of mutating the old one. Runs the
|
|
89
|
+
* same schema the live tool call validated against — legacy/corrupted
|
|
90
|
+
* persisted args fail closed (`undefined`) rather than feeding malformed
|
|
91
|
+
* data back into the picker.
|
|
92
|
+
*/
|
|
93
|
+
export function recoverAskQuestions(toolCallArguments: unknown): AskToolInput["questions"] | undefined {
|
|
94
|
+
const parsed = askSchema(toolCallArguments);
|
|
95
|
+
if (parsed instanceof arkType.errors) return undefined;
|
|
96
|
+
return parsed.questions;
|
|
97
|
+
}
|
|
98
|
+
|
|
84
99
|
/** Result for a single question */
|
|
85
100
|
export interface QuestionResult {
|
|
86
101
|
id: string;
|
package/src/tools/bash.ts
CHANGED
|
@@ -312,10 +312,17 @@ function extractPartialBashEnv(partialJson: string | undefined): Record<string,
|
|
|
312
312
|
return Object.keys(env).length > 0 ? env : undefined;
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
function formatTimeoutClampNotice(
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
315
|
+
function formatTimeoutClampNotice(
|
|
316
|
+
requestedTimeoutSec: number,
|
|
317
|
+
effectiveTimeoutSec: number,
|
|
318
|
+
maxTimeout: number,
|
|
319
|
+
): string | undefined {
|
|
320
|
+
if (requestedTimeoutSec === effectiveTimeoutSec) return undefined;
|
|
321
|
+
const cappedByGlobal = maxTimeout > 0 && effectiveTimeoutSec === maxTimeout && maxTimeout < TOOL_TIMEOUTS.bash.max;
|
|
322
|
+
const limit = cappedByGlobal
|
|
323
|
+
? `global tools.maxTimeout ceiling ${maxTimeout}s`
|
|
324
|
+
: `allowed range ${TOOL_TIMEOUTS.bash.min}-${TOOL_TIMEOUTS.bash.max}s`;
|
|
325
|
+
return `Timeout clamped to ${effectiveTimeoutSec}s (requested ${requestedTimeoutSec}s; ${limit}).`;
|
|
319
326
|
}
|
|
320
327
|
|
|
321
328
|
function formatWallTimeSeconds(wallTimeMs: number): string {
|
|
@@ -831,11 +838,12 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
831
838
|
// must still cancel the call or job, but OMP does not impose a deadline.
|
|
832
839
|
const requestedTimeoutSec = rawTimeout;
|
|
833
840
|
const timeoutDisabled = requestedTimeoutSec === 0;
|
|
834
|
-
const
|
|
841
|
+
const maxTimeout = this.session.settings.get("tools.maxTimeout");
|
|
842
|
+
const timeoutSec = timeoutDisabled ? undefined : clampTimeout("bash", requestedTimeoutSec, maxTimeout);
|
|
835
843
|
const timeoutMs = timeoutSec === undefined ? undefined : timeoutSec * 1000;
|
|
836
844
|
const pendingNotices: string[] = [];
|
|
837
845
|
if (timeoutSec !== undefined) {
|
|
838
|
-
const timeoutClampNotice = formatTimeoutClampNotice(requestedTimeoutSec, timeoutSec);
|
|
846
|
+
const timeoutClampNotice = formatTimeoutClampNotice(requestedTimeoutSec, timeoutSec, maxTimeout);
|
|
839
847
|
if (timeoutClampNotice) pendingNotices.push(timeoutClampNotice);
|
|
840
848
|
}
|
|
841
849
|
|
package/src/tools/browser.ts
CHANGED
|
@@ -190,7 +190,7 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
190
190
|
): Promise<AgentToolResult<BrowserToolDetails>> {
|
|
191
191
|
try {
|
|
192
192
|
throwIfAborted(signal);
|
|
193
|
-
const timeoutSeconds = clampTimeout("browser", params.timeout);
|
|
193
|
+
const timeoutSeconds = clampTimeout("browser", params.timeout, this.session.settings.get("tools.maxTimeout"));
|
|
194
194
|
const timeoutMs = timeoutSeconds * 1000;
|
|
195
195
|
const name = params.name ?? DEFAULT_TAB_NAME;
|
|
196
196
|
const details: BrowserToolDetails = { action: params.action, name };
|
package/src/tools/debug.ts
CHANGED
|
@@ -729,7 +729,7 @@ export class DebugTool implements AgentTool<typeof debugSchema, DebugToolDetails
|
|
|
729
729
|
_onUpdate?: AgentToolUpdateCallback<DebugToolDetails>,
|
|
730
730
|
_context?: AgentToolContext,
|
|
731
731
|
): Promise<AgentToolResult<DebugToolDetails>> {
|
|
732
|
-
const timeoutSec = clampTimeout("debug", params.timeout);
|
|
732
|
+
const timeoutSec = clampTimeout("debug", params.timeout, this.session.settings.get("tools.maxTimeout"));
|
|
733
733
|
const timeoutSignal = AbortSignal.timeout(timeoutSec * 1000);
|
|
734
734
|
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
735
735
|
const details: DebugToolDetails = { action: params.action, success: true };
|
package/src/tools/eval.ts
CHANGED
|
@@ -217,10 +217,6 @@ function detailsNotice(cells: ResolvedEvalCell[]): string | undefined {
|
|
|
217
217
|
return notices.length > 0 ? notices.join(" ") : undefined;
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
function timeoutSecondsFromMs(timeoutMs: number): number {
|
|
221
|
-
return clampTimeout("eval", timeoutMs / 1000);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
220
|
async function resolveBackend(session: ToolSession, language: EvalLanguage): Promise<ResolvedBackend> {
|
|
225
221
|
const backends = resolveEvalBackends(session);
|
|
226
222
|
const allowPy = backends.python;
|
|
@@ -538,7 +534,10 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
538
534
|
// ordinary tool calls all count against the budget. The watchdog drives
|
|
539
535
|
// `combinedSignal`; we pass no wall-clock deadline downstream so the
|
|
540
536
|
// backends never arm a competing fixed timer.
|
|
541
|
-
const idleTimeoutMs =
|
|
537
|
+
const idleTimeoutMs =
|
|
538
|
+
cell.timeoutMs === 0
|
|
539
|
+
? undefined
|
|
540
|
+
: clampTimeout("eval", cell.timeoutMs / 1000, session.settings.get("tools.maxTimeout")) * 1000;
|
|
542
541
|
const idle = idleTimeoutMs === undefined ? undefined : new IdleTimeout(idleTimeoutMs);
|
|
543
542
|
const combinedSignal =
|
|
544
543
|
signal && idle
|
package/src/tools/fetch.ts
CHANGED
|
@@ -1622,7 +1622,7 @@ export async function fetchReadUrl(
|
|
|
1622
1622
|
): Promise<ReadUrlEntry> {
|
|
1623
1623
|
const { path: url, raw = false } = params;
|
|
1624
1624
|
|
|
1625
|
-
const effectiveTimeout = clampTimeout("fetch", 30);
|
|
1625
|
+
const effectiveTimeout = clampTimeout("fetch", 30, session.settings.get("tools.maxTimeout"));
|
|
1626
1626
|
|
|
1627
1627
|
if (signal?.aborted) {
|
|
1628
1628
|
throw new ToolAbortError();
|
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;
|
package/src/tools/hub/jobs.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { settings } from "../../config/settings";
|
|
|
12
12
|
import type { RenderResultOptions } from "../../extensibility/custom-tools/types";
|
|
13
13
|
import { shimmerEnabled, shimmerText } from "../../modes/theme/shimmer";
|
|
14
14
|
import type { Theme } from "../../modes/theme/theme";
|
|
15
|
+
import { USER_INTERRUPT_LABEL } from "../../session/messages";
|
|
15
16
|
import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, truncateToWidth } from "../../tui";
|
|
16
17
|
import type { ToolSession } from "..";
|
|
17
18
|
import {
|
|
@@ -304,26 +305,38 @@ export function nothingToWaitForResult(session: ToolSession): AgentToolResult<Co
|
|
|
304
305
|
}
|
|
305
306
|
|
|
306
307
|
/** `cancel`: kill the named jobs; returns immediately with outcomes + snapshots. */
|
|
307
|
-
export function executeCancel(
|
|
308
|
+
export async function executeCancel(
|
|
308
309
|
session: ToolSession,
|
|
309
310
|
manager: AsyncJobManager,
|
|
310
311
|
ownerId: string | undefined,
|
|
311
312
|
ids: string[],
|
|
312
|
-
): AgentToolResult<CoordinationDetails
|
|
313
|
+
): Promise<AgentToolResult<CoordinationDetails>> {
|
|
313
314
|
const ownerFilter = ownerId ? { ownerId } : undefined;
|
|
314
315
|
const cancelOutcomes: CancelOutcome[] = [];
|
|
315
316
|
for (const id of ids) {
|
|
316
317
|
const existing = manager.getJob(id);
|
|
317
318
|
if (!existing || (ownerId && existing.ownerId !== ownerId)) {
|
|
318
|
-
|
|
319
|
+
// No job by this id (or it belongs to another agent): a budget-aborted
|
|
320
|
+
// keep-alive subagent lives on as a jobless registration long after its
|
|
321
|
+
// job row is reaped, so let cancel reach the agent registration too.
|
|
322
|
+
cancelOutcomes.push(await cancelAgentRegistration(session, ownerId, id));
|
|
319
323
|
continue;
|
|
320
324
|
}
|
|
321
325
|
if (existing.status !== "running") {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
326
|
+
// The job row settled but may still be inside the retention window.
|
|
327
|
+
// The agent registration behind it (job id == agent id for task
|
|
328
|
+
// spawns) can outlive the row as an idle/parked zombie — try the
|
|
329
|
+
// registration kill before reporting the row as already done.
|
|
330
|
+
const regOutcome = await cancelAgentRegistration(session, ownerId, id);
|
|
331
|
+
cancelOutcomes.push(
|
|
332
|
+
regOutcome.status === "cancelled"
|
|
333
|
+
? regOutcome
|
|
334
|
+
: {
|
|
335
|
+
id,
|
|
336
|
+
status: "already_completed",
|
|
337
|
+
message: `Background job ${id} is already ${existing.status}.`,
|
|
338
|
+
},
|
|
339
|
+
);
|
|
327
340
|
continue;
|
|
328
341
|
}
|
|
329
342
|
const cancelled = manager.cancel(id, ownerFilter);
|
|
@@ -336,6 +349,52 @@ export function executeCancel(
|
|
|
336
349
|
return buildJobResult(session, manager, "cancel", visibleJobs(manager, ids, ownerId), cancelOutcomes);
|
|
337
350
|
}
|
|
338
351
|
|
|
352
|
+
/**
|
|
353
|
+
* Kill a non-job-backed agent registration named by `id`: abort any in-flight
|
|
354
|
+
* turn, then release it from the lifecycle (dispose session + unregister). This
|
|
355
|
+
* is the only kill path for a keep-alive subagent that was budget-aborted, went
|
|
356
|
+
* `idle`/`parked`, and outlived its job row — otherwise it is unstoppable short
|
|
357
|
+
* of a broker restart (issue #6315). Scoped to the caller's own descendants so
|
|
358
|
+
* cross-agent kills stay impossible; a bare test/SDK caller (no owner id) may
|
|
359
|
+
* target any sub. Never touches Main, the caller, or advisor transcripts.
|
|
360
|
+
*/
|
|
361
|
+
async function cancelAgentRegistration(
|
|
362
|
+
session: ToolSession,
|
|
363
|
+
ownerId: string | undefined,
|
|
364
|
+
id: string,
|
|
365
|
+
): Promise<CancelOutcome> {
|
|
366
|
+
const registry = session.agentRegistry;
|
|
367
|
+
const ref = registry?.get(id);
|
|
368
|
+
if (ref?.kind !== "sub") {
|
|
369
|
+
return { id, status: "not_found", message: `Background job not found: ${id}` };
|
|
370
|
+
}
|
|
371
|
+
if (id === ownerId) {
|
|
372
|
+
return { id, status: "not_found", message: `Cannot cancel yourself (${id}).` };
|
|
373
|
+
}
|
|
374
|
+
if (ownerId && ref.parentId !== ownerId) {
|
|
375
|
+
return { id, status: "not_found", message: `Agent ${id} was not spawned by you and cannot be cancelled.` };
|
|
376
|
+
}
|
|
377
|
+
const lifecycle = session.agentLifecycle?.();
|
|
378
|
+
try {
|
|
379
|
+
if (ref.status === "running" && ref.session) {
|
|
380
|
+
await ref.session.abort({ reason: USER_INTERRUPT_LABEL });
|
|
381
|
+
}
|
|
382
|
+
if (lifecycle) {
|
|
383
|
+
await lifecycle.release(id);
|
|
384
|
+
} else {
|
|
385
|
+
await ref.session?.dispose();
|
|
386
|
+
registry?.unregister(id);
|
|
387
|
+
}
|
|
388
|
+
} catch (error) {
|
|
389
|
+
return {
|
|
390
|
+
id,
|
|
391
|
+
status: "already_completed",
|
|
392
|
+
message: `Agent ${id} could not be fully cancelled: ${error instanceof Error ? error.message : String(error)}.`,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
return { id, status: "cancelled", message: `Cancelled agent ${id} (killed session, dropped registration).` };
|
|
396
|
+
}
|
|
397
|
+
|
|
339
398
|
/** `jobs`: read-only snapshot of every job plus the jobless running-agent roster. */
|
|
340
399
|
export function executeJobsSnapshot(
|
|
341
400
|
session: ToolSession,
|