pi-subagents 0.83.0 → 0.84.0
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 +39 -2
- package/bundle/agents.js +4 -4
- package/bundle/capability-ceiling.js +1 -1
- package/bundle/child-tool-plan.js +3 -3
- package/bundle/{chunk-HJJAVHFB.js → chunk-235AWJUQ.js} +35 -32
- package/bundle/{chunk-P2NCCTZP.js → chunk-2D52JFLF.js} +1 -1
- package/bundle/{chunk-WG2OTTAK.js → chunk-2DHZTCOL.js} +2 -2
- package/bundle/{chunk-MBDOYZ2V.js → chunk-2S4K53MV.js} +1 -1
- package/bundle/{chunk-527E3D2J.js → chunk-4E7BKDTX.js} +5 -5
- package/bundle/{chunk-CZTMUBUI.js → chunk-7BWGNDNK.js} +4 -4
- package/bundle/{chunk-HXVKGS33.js → chunk-AKOIVLA5.js} +4 -4
- package/bundle/{chunk-MHX22USZ.js → chunk-C5P5CHYK.js} +1 -1
- package/bundle/{chunk-5ESXK62X.js → chunk-CGN2TVE7.js} +417 -389
- package/bundle/{chunk-VTMNBPCQ.js → chunk-D3JAFGMO.js} +1 -1
- package/bundle/{chunk-CDI3GMNV.js → chunk-DZDETGRJ.js} +18 -15
- package/bundle/{chunk-BKFWQ642.js → chunk-F4ASL6OV.js} +1 -1
- package/bundle/{chunk-JHZ6DZ5M.js → chunk-G2Z6ZH26.js} +1 -1
- package/bundle/{chunk-XXKG3P2W.js → chunk-J5YL7LYR.js} +4 -4
- package/bundle/{chunk-LDMXJIA6.js → chunk-JG4QKI5M.js} +58 -67
- package/bundle/{chunk-OOFW75PV.js → chunk-L3WFKBEI.js} +15 -11
- package/bundle/{chunk-Y3I6H6DV.js → chunk-LFDBD4PW.js} +2 -2
- package/bundle/{chunk-7F5SK2U3.js → chunk-LNRJCP2Y.js} +17 -14
- package/bundle/{chunk-4BW55BTL.js → chunk-NENDSQUY.js} +4 -4
- package/bundle/{chunk-6B4ITO2W.js → chunk-NKQ6JNAS.js} +1 -1
- package/bundle/{chunk-RZJN63GU.js → chunk-PALJGGYC.js} +2 -2
- package/bundle/{chunk-TJQJ3MEA.js → chunk-PV63UFUR.js} +10 -10
- package/bundle/{chunk-Z7KVHQHL.js → chunk-QXASAHIA.js} +2 -2
- package/bundle/{chunk-HLE22FSH.js → chunk-SSG7IEP5.js} +2 -2
- package/bundle/{chunk-CSSZRTKY.js → chunk-V2BZYLEP.js} +2 -2
- package/bundle/{chunk-CNLDYEH4.js → chunk-VQVVHSY6.js} +11 -11
- package/bundle/{chunk-CGXWSNPA.js → chunk-XDQORLZ5.js} +4 -4
- package/bundle/control-channel.js +3 -3
- package/bundle/index.js +1 -1
- package/bundle/intercom-bridge.js +3 -3
- package/bundle/preflight.js +9 -9
- package/bundle/project-panes.js +4 -4
- package/docs/configuration.md +12 -1
- package/docs/extension-api.md +2 -0
- package/docs/missions.md +2 -0
- package/docs/tool-reference.md +1 -1
- package/package.json +1 -1
- package/src/agents/agent-management.d.ts +1 -1
- package/src/agents/agent-management.js +2 -2
- package/src/agents/agents.js +1 -2
- package/src/agents/frontmatter.js +22 -18
- package/src/extension/index.js +2 -2
- package/src/missions/actions.js +7 -2
- package/src/missions/lifecycle.d.ts +11 -0
- package/src/missions/lifecycle.js +37 -6
- package/src/missions/store.js +1 -0
- package/src/missions/types.d.ts +2 -0
- package/src/runs/background/async-execution.d.ts +4 -1
- package/src/runs/background/async-execution.js +18 -3
- package/src/runs/background/async-resume.js +3 -2
- package/src/runs/background/async-retention.d.ts +1 -0
- package/src/runs/background/async-retention.js +16 -10
- package/src/runs/background/retained-children.js +1 -1
- package/src/runs/background/run-child-session.d.ts +2 -0
- package/src/runs/background/run-child-session.js +9 -1
- package/src/runs/background/subagent-runner.js +1 -0
- package/src/runs/foreground/subagent-executor.js +29 -51
- package/src/runs/shared/capability-ceiling.d.ts +2 -1
- package/src/runs/shared/capability-ceiling.js +3 -2
- package/src/runs/shared/child-session.d.ts +2 -0
- package/src/runs/shared/child-session.js +1 -0
- package/src/runs/shared/model-resolution-diagnostic.d.ts +11 -0
- package/src/runs/shared/model-resolution-diagnostic.js +25 -0
- package/src/runs/shared/parallel-handoff.d.ts +1 -1
- package/src/runs/shared/parallel-handoff.js +10 -6
- package/src/runs/shared/parallel-utils.d.ts +2 -0
- package/src/shared/types.d.ts +1 -1
- package/src/workflows/scripted-workflow.js +6 -1
- package/workflow-script-worker.cjs +450 -175
|
@@ -97,22 +97,15 @@ export function parseFrontmatter(content) {
|
|
|
97
97
|
// neither end a pending list block nor become part of its value.
|
|
98
98
|
if (trimmed.startsWith("#") && !currentFolded && !currentLiteral)
|
|
99
99
|
continue;
|
|
100
|
-
|
|
100
|
+
// Exactly-empty lines belong to any pending block, not only to | and > scalars.
|
|
101
|
+
if (currentKey !== null && currentBlockLines !== null && (indent > (currentIndent ?? 0) || trimmed === "")) {
|
|
101
102
|
// This line is part of the current block value
|
|
102
103
|
currentBlockLines.push(line);
|
|
103
104
|
continue;
|
|
104
105
|
}
|
|
105
106
|
// Flush any pending block value
|
|
106
107
|
if (currentKey !== null && currentBlockLines !== null) {
|
|
107
|
-
|
|
108
|
-
// serializer can add its own indentation level.
|
|
109
|
-
const rawBlock = currentBlockLines.join("\n");
|
|
110
|
-
const leadingSpaces = rawBlock.match(/^[ \t]+(?=\S)/m);
|
|
111
|
-
const prefix = leadingSpaces?.[0] ?? "";
|
|
112
|
-
const stripped = prefix
|
|
113
|
-
? rawBlock.replace(new RegExp(`^${escapeRegex(prefix)}`, "gm"), "").replace(/^\n/, "")
|
|
114
|
-
: rawBlock;
|
|
115
|
-
frontmatter[currentKey] = currentFolded ? foldBlock(stripped) : stripped;
|
|
108
|
+
frontmatter[currentKey] = flushBlockLines(currentBlockLines, currentFolded, currentLiteral);
|
|
116
109
|
currentKey = null;
|
|
117
110
|
currentBlockLines = null;
|
|
118
111
|
currentIndent = null;
|
|
@@ -146,17 +139,28 @@ export function parseFrontmatter(content) {
|
|
|
146
139
|
frontmatter[key] = value;
|
|
147
140
|
}
|
|
148
141
|
}
|
|
149
|
-
// Lines that don't match a key pattern (e.g., comments, empty lines) are ignored
|
|
150
142
|
}
|
|
151
143
|
// Flush final block value
|
|
152
144
|
if (currentKey !== null && currentBlockLines !== null) {
|
|
153
|
-
|
|
154
|
-
const leadingSpaces = rawBlock.match(/^[ \t]+(?=\S)/m);
|
|
155
|
-
const prefix = leadingSpaces?.[0] ?? "";
|
|
156
|
-
const stripped = prefix
|
|
157
|
-
? rawBlock.replace(new RegExp(`^${escapeRegex(prefix)}`, "gm"), "").replace(/^\n/, "")
|
|
158
|
-
: rawBlock;
|
|
159
|
-
frontmatter[currentKey] = currentFolded ? foldBlock(stripped) : stripped;
|
|
145
|
+
frontmatter[currentKey] = flushBlockLines(currentBlockLines, currentFolded, currentLiteral);
|
|
160
146
|
}
|
|
161
147
|
return { frontmatter, body };
|
|
162
148
|
}
|
|
149
|
+
function flushBlockLines(blockLines, folded, literal) {
|
|
150
|
+
let lines = blockLines;
|
|
151
|
+
if (!folded && !literal) {
|
|
152
|
+
// Drop exactly-empty edge lines, and every line from the first exactly-empty one after
|
|
153
|
+
// the last content line, so nested values without inner blank lines parse as before.
|
|
154
|
+
const end = lines.indexOf("", lines.findLastIndex((line) => line.trim() !== "") + 1);
|
|
155
|
+
lines = lines.slice(lines.findIndex((line) => line !== ""), end < 0 ? lines.length : end);
|
|
156
|
+
}
|
|
157
|
+
// Strip the common leading whitespace from the block so the
|
|
158
|
+
// serializer can add its own indentation level.
|
|
159
|
+
const rawBlock = lines.join("\n");
|
|
160
|
+
const leadingSpaces = rawBlock.match(/^[ \t]+(?=\S)/m);
|
|
161
|
+
const prefix = leadingSpaces?.[0] ?? "";
|
|
162
|
+
const stripped = prefix
|
|
163
|
+
? rawBlock.replace(new RegExp(`^${escapeRegex(prefix)}`, "gm"), "").replace(/^\n/, "")
|
|
164
|
+
: rawBlock;
|
|
165
|
+
return folded ? foldBlock(stripped) : stripped;
|
|
166
|
+
}
|
package/src/extension/index.js
CHANGED
|
@@ -735,7 +735,7 @@ export default function registerSubagentExtension(pi) {
|
|
|
735
735
|
event.systemPromptOptions.sections.active_subagents = activeRuns;
|
|
736
736
|
if (!event.systemPromptOptions.selectedTools.includes("subagent"))
|
|
737
737
|
return;
|
|
738
|
-
const catalog = buildAdvertisedAgentCatalog(advertisedAgents, resolveCurrentSubagentCapabilityCeiling(
|
|
738
|
+
const catalog = buildAdvertisedAgentCatalog(advertisedAgents, resolveCurrentSubagentCapabilityCeiling(ctx.sessionManager.getSessionId() ?? undefined), recordedPromptSection(pi, "advertised_subagents"));
|
|
739
739
|
if (catalog)
|
|
740
740
|
event.systemPromptOptions.sections.advertised_subagents = catalog;
|
|
741
741
|
});
|
|
@@ -1207,7 +1207,7 @@ export default function registerSubagentExtension(pi) {
|
|
|
1207
1207
|
hasPendingSupervisorRequest: () => [...supervisorChannel.pending.values()].some((request) => request.expectsReply),
|
|
1208
1208
|
advertisedPrompt: async () => {
|
|
1209
1209
|
await waitForAdvertisement();
|
|
1210
|
-
return buildAdvertisedAgentPrompt(advertisedAgents, resolveCurrentSubagentCapabilityCeiling(state.
|
|
1210
|
+
return buildAdvertisedAgentPrompt(advertisedAgents, resolveCurrentSubagentCapabilityCeiling(state.supervisorOwnerSessionId ?? undefined));
|
|
1211
1211
|
},
|
|
1212
1212
|
});
|
|
1213
1213
|
}
|
package/src/missions/actions.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { MISSION_STATUSES, } from "./types.js";
|
|
4
4
|
import { missionStatePath } from "./workflow-state.js";
|
|
5
|
+
import { finishedMissionStatus, missionAcceptanceStatus, readMissionBinding } from "./lifecycle.js";
|
|
5
6
|
import { createMission, listGlobalMissions, listMissions, missionRecordPath, readMission, resolveMissionStoreLocation, updateMission, validateMissionId, } from "./store.js";
|
|
6
7
|
export const MISSION_ACTIONS = [
|
|
7
8
|
"mission.create",
|
|
@@ -184,6 +185,7 @@ function validateMissionUpdate(value) {
|
|
|
184
185
|
}
|
|
185
186
|
function refreshLinkedRunStatus(location, record) {
|
|
186
187
|
const warnings = [];
|
|
188
|
+
const runAcceptance = [];
|
|
187
189
|
const updates = record.runs.flatMap((run) => {
|
|
188
190
|
if (!run.asyncDir)
|
|
189
191
|
return [];
|
|
@@ -207,6 +209,7 @@ function refreshLinkedRunStatus(location, record) {
|
|
|
207
209
|
warnings.push(`Linked run status '${statusPath}' is missing state`);
|
|
208
210
|
return [];
|
|
209
211
|
}
|
|
212
|
+
runAcceptance.push({ runId: run.runId, status: missionAcceptanceStatus(status.steps) });
|
|
210
213
|
if (state === run.status)
|
|
211
214
|
return [];
|
|
212
215
|
return [{
|
|
@@ -232,7 +235,8 @@ function refreshLinkedRunStatus(location, record) {
|
|
|
232
235
|
: states.some((state) => state === "stopped" || state === "rejected" || state === "cancelled")
|
|
233
236
|
? "cancelled"
|
|
234
237
|
: states.length > 0 && states.every((state) => state === "complete" || state === "completed")
|
|
235
|
-
|
|
238
|
+
// A run without a binding counts as auto-created.
|
|
239
|
+
? finishedMissionStatus(record, !merged.some((run) => run.asyncDir && readMissionBinding(run.asyncDir)?.autoCreated === false), runAcceptance)
|
|
236
240
|
: record.status;
|
|
237
241
|
return { record: updateMission(location, record.id, { status, addRuns: updates }), warnings };
|
|
238
242
|
}
|
|
@@ -261,7 +265,8 @@ function formatMission(record) {
|
|
|
261
265
|
const identity = child.runId ? `${child.key} (${child.runId})` : child.key;
|
|
262
266
|
const heartbeat = child.heartbeat ? `; heartbeat ${child.heartbeat.status ?? child.status}${child.heartbeat.phase ? `/${child.heartbeat.phase}` : ""} at ${child.heartbeat.updatedAt}` : "";
|
|
263
267
|
const recovery = child.sessionPath ? `; session ${child.sessionPath}` : child.artifactPaths.length ? `; artifacts ${child.artifactPaths.length}` : "";
|
|
264
|
-
|
|
268
|
+
const acceptance = child.acceptanceStatus ? `; acceptance ${child.acceptanceStatus}` : "";
|
|
269
|
+
lines.push(` ${identity}: ${child.status}${child.agent ? ` — ${child.agent}` : ""}${child.phase ? ` [${child.phase}]` : ""}${acceptance}; updated ${child.updatedAt}${heartbeat}${recovery}`);
|
|
265
270
|
}
|
|
266
271
|
}
|
|
267
272
|
if (record.decisions.length) {
|
|
@@ -21,6 +21,7 @@ export interface MissionLaunchParams {
|
|
|
21
21
|
export interface MissionLaunchBinding {
|
|
22
22
|
missionId: string;
|
|
23
23
|
location: MissionStoreLocation;
|
|
24
|
+
/** True when the launch made the mission without a `mission` or `missionId` request. */
|
|
24
25
|
autoCreated: boolean;
|
|
25
26
|
announceInContent?: boolean;
|
|
26
27
|
}
|
|
@@ -30,6 +31,16 @@ export declare function prepareMissionLaunch(input: {
|
|
|
30
31
|
config?: MissionStoreConfig;
|
|
31
32
|
ownerSessionId?: string;
|
|
32
33
|
}): MissionLaunchBinding | undefined;
|
|
34
|
+
/** The first unaccepted acceptance status among child results, else the first status found. */
|
|
35
|
+
export declare function missionAcceptanceStatus(results: unknown): string | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* The status of a mission whose runs all finished successfully. Every path that
|
|
38
|
+
* completes a mission from run state goes through here.
|
|
39
|
+
*/
|
|
40
|
+
export declare function finishedMissionStatus(record: MissionRecord, autoCreated: boolean, runAcceptance: ReadonlyArray<{
|
|
41
|
+
runId: string;
|
|
42
|
+
status: string | undefined;
|
|
43
|
+
}>): "completed" | "waiting";
|
|
33
44
|
export declare function writeMissionAsyncBinding(asyncDir: string, binding: MissionLaunchBinding): void;
|
|
34
45
|
export declare function attachMissionToLaunchResult(input: {
|
|
35
46
|
binding: MissionLaunchBinding;
|
|
@@ -50,7 +50,7 @@ export function prepareMissionLaunch(input) {
|
|
|
50
50
|
...(mission?.labels ? { labels: mission.labels } : {}),
|
|
51
51
|
...(input.ownerSessionId ? { ownerSessionId: input.ownerSessionId } : {}),
|
|
52
52
|
}, new Date(), input.config?.retainTerminal);
|
|
53
|
-
return { missionId: record.id, location, autoCreated:
|
|
53
|
+
return { missionId: record.id, location, autoCreated: input.params.mission === undefined, announceInContent: true };
|
|
54
54
|
}
|
|
55
55
|
function toolResultIsError(result) {
|
|
56
56
|
return "isError" in result && result.isError === true;
|
|
@@ -67,7 +67,30 @@ function runStatusForResult(result) {
|
|
|
67
67
|
return "failed";
|
|
68
68
|
return "completed";
|
|
69
69
|
}
|
|
70
|
-
|
|
70
|
+
/** Acceptance statuses that mean nobody accepted the child's result. */
|
|
71
|
+
const UNACCEPTED_ACCEPTANCE_STATUSES = new Set(["pending", "claimed", "review-required", "rejected"]);
|
|
72
|
+
/** The first unaccepted acceptance status among child results, else the first status found. */
|
|
73
|
+
export function missionAcceptanceStatus(results) {
|
|
74
|
+
const statuses = (Array.isArray(results) ? results : [])
|
|
75
|
+
.map((result) => result?.acceptance?.status)
|
|
76
|
+
.filter((status) => typeof status === "string" && status.trim() !== "");
|
|
77
|
+
return statuses.find((status) => UNACCEPTED_ACCEPTANCE_STATUSES.has(status)) ?? statuses[0];
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The status of a mission whose runs all finished successfully. Every path that
|
|
81
|
+
* completes a mission from run state goes through here.
|
|
82
|
+
*/
|
|
83
|
+
export function finishedMissionStatus(record, autoCreated, runAcceptance) {
|
|
84
|
+
// Nobody is expected to close a mission made by default for an ordinary launch.
|
|
85
|
+
if (autoCreated)
|
|
86
|
+
return "completed";
|
|
87
|
+
if (runAcceptance.some((run) => UNACCEPTED_ACCEPTANCE_STATUSES.has(run.status)))
|
|
88
|
+
return "waiting";
|
|
89
|
+
// A run reported here supersedes its older workflow child record.
|
|
90
|
+
const reported = new Set(runAcceptance.map((run) => run.runId));
|
|
91
|
+
return record.workflowChildren.some((child) => !reported.has(child.runId) && UNACCEPTED_ACCEPTANCE_STATUSES.has(child.acceptanceStatus)) ? "waiting" : "completed";
|
|
92
|
+
}
|
|
93
|
+
function missionStatusForRun(record, runId, runStatus, acceptanceStatus, autoCreated) {
|
|
71
94
|
if (record.status === "completed" || record.status === "failed" || record.status === "cancelled")
|
|
72
95
|
return record.status;
|
|
73
96
|
if (runStatus === "active" || runStatus === "queued" || runStatus === "running")
|
|
@@ -80,7 +103,7 @@ function missionStatusForRun(record, runId, runStatus) {
|
|
|
80
103
|
if (otherActive)
|
|
81
104
|
return "active";
|
|
82
105
|
if (runStatus === "completed" || runStatus === "complete")
|
|
83
|
-
return
|
|
106
|
+
return finishedMissionStatus(record, autoCreated, [{ runId, status: acceptanceStatus }]);
|
|
84
107
|
if (runStatus === "stopped" || runStatus === "rejected" || runStatus === "cancelled")
|
|
85
108
|
return "cancelled";
|
|
86
109
|
return "failed";
|
|
@@ -129,6 +152,7 @@ function persistedBinding(binding) {
|
|
|
129
152
|
globalIndexDir: binding.location.globalIndexDir,
|
|
130
153
|
writeGlobalIndex: binding.location.writeGlobalIndex,
|
|
131
154
|
...(binding.location.retainTerminal !== undefined ? { retainTerminal: binding.location.retainTerminal } : {}),
|
|
155
|
+
autoCreated: binding.autoCreated,
|
|
132
156
|
};
|
|
133
157
|
}
|
|
134
158
|
export function writeMissionAsyncBinding(asyncDir, binding) {
|
|
@@ -170,7 +194,7 @@ export function attachMissionToLaunchResult(input) {
|
|
|
170
194
|
...(usage ? { usage } : {}),
|
|
171
195
|
};
|
|
172
196
|
let mission = updateMission(input.binding.location, input.binding.missionId, {
|
|
173
|
-
status: missionStatusForRun(current, runId, runStatus),
|
|
197
|
+
status: missionStatusForRun(current, runId, runStatus, missionAcceptanceStatus(input.result.details.results), input.binding.autoCreated),
|
|
174
198
|
addRuns: [run],
|
|
175
199
|
addArtifacts: artifactsForResult(input.result),
|
|
176
200
|
...(firstText(input.result) && runStatus !== "active" ? { summary: firstText(input.result) } : {}),
|
|
@@ -188,6 +212,8 @@ export function attachMissionToLaunchResult(input) {
|
|
|
188
212
|
asyncDir: input.result.details.asyncDir,
|
|
189
213
|
mode: input.result.details.mode,
|
|
190
214
|
state: status.state,
|
|
215
|
+
// A run that finished before this launch result was attached keeps its acceptance in the status steps.
|
|
216
|
+
...(Array.isArray(status.steps) ? { results: status.steps.map((step) => ({ acceptance: step?.acceptance })) } : {}),
|
|
191
217
|
...(firstText(input.result) ? { summary: firstText(input.result) } : {}),
|
|
192
218
|
}) ?? mission;
|
|
193
219
|
}
|
|
@@ -237,9 +263,12 @@ function parsePersistedBinding(value, source) {
|
|
|
237
263
|
throw new Error(`${source}.writeGlobalIndex must be boolean`);
|
|
238
264
|
if (input.retainTerminal !== undefined && (!Number.isInteger(input.retainTerminal) || input.retainTerminal < 1))
|
|
239
265
|
throw new Error(`${source}.retainTerminal must be a positive integer`);
|
|
266
|
+
if (input.autoCreated !== undefined && typeof input.autoCreated !== "boolean")
|
|
267
|
+
throw new Error(`${source}.autoCreated must be boolean`);
|
|
240
268
|
return {
|
|
241
269
|
missionId: validateMissionId(input.missionId, `${source}.missionId`),
|
|
242
|
-
|
|
270
|
+
// Bindings saved before this field existed belong to runs that completed their mission without an acceptance check.
|
|
271
|
+
autoCreated: input.autoCreated !== false,
|
|
243
272
|
location: {
|
|
244
273
|
projectRoot: input.projectRoot,
|
|
245
274
|
missionDir: input.missionDir,
|
|
@@ -339,8 +368,9 @@ export function syncMissionFromAsyncCompletion(value) {
|
|
|
339
368
|
? "stopped"
|
|
340
369
|
: "failed";
|
|
341
370
|
const workflowChildTerminal = !["running", "queued", "active", "paused"].includes(workflowChildStatus);
|
|
371
|
+
const acceptanceStatus = missionAcceptanceStatus(event.results);
|
|
342
372
|
return updateMission(binding.location, binding.missionId, {
|
|
343
|
-
status: missionStatusForRun(current, runId, runStatus),
|
|
373
|
+
status: missionStatusForRun(current, runId, runStatus, acceptanceStatus, binding.autoCreated),
|
|
344
374
|
addRuns: [{ runId, mode: typeof event.mode === "string" && ["single", "parallel", "chain", "workflow"].includes(event.mode) ? event.mode : "external", asyncDir: event.asyncDir, status: runStatus, completedAt, ...(usage && usage.tokens > 0 ? { usage } : {}) }],
|
|
345
375
|
addArtifacts: artifacts,
|
|
346
376
|
...(workflowRunId && workflowKey ? { upsertWorkflowChildren: [{
|
|
@@ -350,6 +380,7 @@ export function syncMissionFromAsyncCompletion(value) {
|
|
|
350
380
|
status: workflowChildStatus,
|
|
351
381
|
artifactPaths: artifacts.map((artifact) => artifact.path),
|
|
352
382
|
...(workflowChildTerminal ? { completedAt } : {}),
|
|
383
|
+
...(acceptanceStatus ? { acceptanceStatus } : {}),
|
|
353
384
|
heartbeat: { status: workflowChildStatus, ...(summary ? { message: summary } : {}) },
|
|
354
385
|
}] } : {}),
|
|
355
386
|
...(summary ? { summary } : {}),
|
package/src/missions/store.js
CHANGED
|
@@ -133,6 +133,7 @@ function parseWorkflowChild(value, label) {
|
|
|
133
133
|
...(optionalString(input.phase, `${label}.phase`) ? { phase: input.phase } : {}),
|
|
134
134
|
...(input.completedAt !== undefined ? { completedAt: timestamp(input.completedAt, `${label}.completedAt`) } : {}),
|
|
135
135
|
...(optionalString(input.sessionPath, `${label}.sessionPath`) ? { sessionPath: input.sessionPath } : {}),
|
|
136
|
+
...(optionalString(input.acceptanceStatus, `${label}.acceptanceStatus`) ? { acceptanceStatus: input.acceptanceStatus } : {}),
|
|
136
137
|
...(heartbeat ? { heartbeat: {
|
|
137
138
|
updatedAt: timestamp(heartbeat.updatedAt, `${label}.heartbeat.updatedAt`),
|
|
138
139
|
...(optionalString(heartbeat.status, `${label}.heartbeat.status`) ? { status: heartbeat.status } : {}),
|
package/src/missions/types.d.ts
CHANGED
|
@@ -57,6 +57,8 @@ export interface MissionWorkflowChild {
|
|
|
57
57
|
sessionPath?: string;
|
|
58
58
|
artifactPaths: string[];
|
|
59
59
|
heartbeat?: MissionChildHeartbeat;
|
|
60
|
+
/** Latest acceptance ledger status reported for the child. */
|
|
61
|
+
acceptanceStatus?: string;
|
|
60
62
|
}
|
|
61
63
|
export type MissionWorkflowChildUpdate = Pick<MissionWorkflowChild, "workflowRunId" | "key" | "status"> & Partial<Omit<MissionWorkflowChild, "workflowRunId" | "key" | "status" | "startedAt" | "updatedAt" | "artifactPaths" | "heartbeat">> & {
|
|
62
64
|
startedAt?: string;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Async execution logic for subagent tool
|
|
3
3
|
*/
|
|
4
4
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { ParentProviderRegistry } from "../shared/child-session.ts";
|
|
5
6
|
import { type AgentConfig, type UnknownAgentDiagnosticContext } from "../../agents/agents.ts";
|
|
6
7
|
import { type ClaudeCodeOverride } from "../shared/claude-code-adapter.ts";
|
|
7
8
|
import { type ChainStep } from "../../shared/settings.ts";
|
|
@@ -25,7 +26,7 @@ interface AsyncExecutionContext {
|
|
|
25
26
|
cwd: string;
|
|
26
27
|
currentSessionId: string;
|
|
27
28
|
completionOwnerId?: string;
|
|
28
|
-
/** Parent session id
|
|
29
|
+
/** Parent Pi session id (`getSessionId()`): keys permission-system ask forwarding and capability ceilings. */
|
|
29
30
|
parentSessionId?: string;
|
|
30
31
|
/** Parent session file, recorded as each new child session's `parentSession` header. */
|
|
31
32
|
parentSessionFile?: string;
|
|
@@ -44,6 +45,8 @@ interface AsyncExecutionContext {
|
|
|
44
45
|
childRuntime?: ChildRuntimeConfig;
|
|
45
46
|
/** The launching session's project trust; undefined when the host has no trust concept. */
|
|
46
47
|
projectTrusted?: boolean;
|
|
48
|
+
/** The parent's model registry, read at launch to name a provider an extension registered there. */
|
|
49
|
+
parentProviderRegistry?: Pick<ParentProviderRegistry, "getRegisteredProviderIds">;
|
|
47
50
|
}
|
|
48
51
|
export declare const DEFAULT_ASYNC_TIMEOUT_MS: number;
|
|
49
52
|
interface AsyncChainParams {
|
|
@@ -135,6 +135,17 @@ function supportsNativeRunner(nodeExecutable) {
|
|
|
135
135
|
&& nodeExecutable === process.execPath
|
|
136
136
|
&& !sourceUnderNodeModules();
|
|
137
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* A runner child without the ambient extensions does not load the parent extension that
|
|
140
|
+
* registered its model's provider, and still resolves the model when a built-in provider
|
|
141
|
+
* has the same id. Name that provider so a failed run can say so.
|
|
142
|
+
*/
|
|
143
|
+
function parentExtensionProviderForChild(ctx, disableAmbientExtensions, model, availableModels, preferredProvider) {
|
|
144
|
+
if (!disableAmbientExtensions || !ctx.parentProviderRegistry)
|
|
145
|
+
return undefined;
|
|
146
|
+
const provider = findModelInfo(model, availableModels, preferredProvider)?.provider;
|
|
147
|
+
return provider && ctx.parentProviderRegistry.getRegisteredProviderIds().includes(provider) ? provider : undefined;
|
|
148
|
+
}
|
|
138
149
|
export const DEFAULT_ASYNC_TIMEOUT_MS = 30 * 60 * 1000;
|
|
139
150
|
export function formatAsyncStartedMessage(headline, interactive) {
|
|
140
151
|
const guidance = interactive
|
|
@@ -997,6 +1008,7 @@ export function buildAsyncRunnerSteps(id, params) {
|
|
|
997
1008
|
runtimeSnapshotHost: ctx.pi,
|
|
998
1009
|
});
|
|
999
1010
|
const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
|
|
1011
|
+
const parentExtensionProvider = externalRunner || machine ? undefined : parentExtensionProviderForChild(ctx, toolPlan.disableAmbientExtensions, selectedModel, availableModels, a.modelProvider ?? ctx.currentModelProvider);
|
|
1000
1012
|
if (externalRunner && permissionRules) {
|
|
1001
1013
|
throw new AsyncStartValidationError(`Agent '${a.name}' uses runner.type='${externalRunnerType}', which cannot enforce native Pi child permission rules.`);
|
|
1002
1014
|
}
|
|
@@ -1026,6 +1038,7 @@ export function buildAsyncRunnerSteps(id, params) {
|
|
|
1026
1038
|
thinking: resolveEffectiveThinking(selectedModel, effectiveThinking),
|
|
1027
1039
|
...(thinkingCeiling ? { thinkingCeiling } : {}),
|
|
1028
1040
|
launchResolvedExtensions,
|
|
1041
|
+
...(parentExtensionProvider ? { parentExtensionProvider } : {}),
|
|
1029
1042
|
...(requestedModel ? { requestedModel } : {}),
|
|
1030
1043
|
...(primaryModelFromParent ? { skipPrimaryModelVerification: true } : {}),
|
|
1031
1044
|
...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
|
|
@@ -1233,7 +1246,8 @@ export function executeAsyncChain(id, params) {
|
|
|
1233
1246
|
.find((name) => name !== undefined && !lookupRunnerLauncher(ctx.runnerLaunchers, name));
|
|
1234
1247
|
if (undefinedLauncher !== undefined)
|
|
1235
1248
|
return formatAsyncStartError(resultMode, `Launcher '${undefinedLauncher}' used by this ${resultMode} is not defined in runnerLaunchers in the user subagent config.`);
|
|
1236
|
-
const
|
|
1249
|
+
const launchParentSessionId = ctx.parentSessionId ?? ctx.currentSessionId;
|
|
1250
|
+
const capabilityCeiling = params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(launchParentSessionId);
|
|
1237
1251
|
const inheritedNestedRoute = inheritedNestedRouteOf(ctx.childRuntime);
|
|
1238
1252
|
const nestedAddress = inheritedNestedRoute ? inheritedNestedParentAddressOf(ctx.childRuntime) : undefined;
|
|
1239
1253
|
const asyncDir = inheritedNestedRoute
|
|
@@ -1336,7 +1350,6 @@ export function executeAsyncChain(id, params) {
|
|
|
1336
1350
|
const initialStatusAt = Date.now();
|
|
1337
1351
|
const initialCompletionOwnerId = ctx.completionOwnerId ?? currentCompletionOwnerId();
|
|
1338
1352
|
const completionOwnerProcess = currentRecordedPid();
|
|
1339
|
-
const launchParentSessionId = ctx.parentSessionId ?? ctx.currentSessionId;
|
|
1340
1353
|
try {
|
|
1341
1354
|
writeRetainedRequiredChildExtensions(asyncDir, params.requiredExtensions ?? ctx.childRuntime?.requiredExtensions ?? resolveRequiredChildExtensions(launchParentSessionId ?? undefined));
|
|
1342
1355
|
}
|
|
@@ -1600,7 +1613,7 @@ export function executeAsyncSingle(id, params) {
|
|
|
1600
1613
|
if (unsupported.length > 0)
|
|
1601
1614
|
return formatAsyncStartError("single", `Agent '${agentConfig.name}' uses runner.type='${externalRunnerType}' and does not support: ${unsupported.join(", ")}.`);
|
|
1602
1615
|
}
|
|
1603
|
-
const capabilityCeiling = intersectSubagentCapabilityCeilings(params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId), ctx.childRuntime?.capabilityCeiling);
|
|
1616
|
+
const capabilityCeiling = intersectSubagentCapabilityCeilings(params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(ctx.parentSessionId ?? ctx.currentSessionId), ctx.childRuntime?.capabilityCeiling);
|
|
1604
1617
|
try {
|
|
1605
1618
|
assertAgentAllowedByCapabilityCeiling(agentConfig.name, capabilityCeiling);
|
|
1606
1619
|
}
|
|
@@ -1811,6 +1824,7 @@ export function executeAsyncSingle(id, params) {
|
|
|
1811
1824
|
runtimeSnapshotHost: ctx.pi,
|
|
1812
1825
|
});
|
|
1813
1826
|
const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
|
|
1827
|
+
const parentExtensionProvider = externalRunner || machine ? undefined : parentExtensionProviderForChild(ctx, toolPlan.disableAmbientExtensions, selectedModel, availableModels, agentConfig.modelProvider ?? ctx.currentModelProvider);
|
|
1814
1828
|
const fast = params.fast ?? agentConfig.fast;
|
|
1815
1829
|
const launchThinking = resolveEffectiveThinking(selectedModel, effectiveThinking);
|
|
1816
1830
|
const { definitionDigest, launchContractDigest } = resolveLaunchBinding({
|
|
@@ -1933,6 +1947,7 @@ export function executeAsyncSingle(id, params) {
|
|
|
1933
1947
|
...(params.fast ?? agentConfig.fast ? { fast: params.fast ?? agentConfig.fast } : {}),
|
|
1934
1948
|
thinking: resolveEffectiveThinking(selectedModel, effectiveThinking),
|
|
1935
1949
|
...(thinkingCeiling ? { thinkingCeiling } : {}),
|
|
1950
|
+
...(parentExtensionProvider ? { parentExtensionProvider } : {}),
|
|
1936
1951
|
...(requestedModel ? { requestedModel } : {}),
|
|
1937
1952
|
...(modelOrigin === "inherited" ? { skipPrimaryModelVerification: true } : {}),
|
|
1938
1953
|
...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
|
|
@@ -484,7 +484,8 @@ export function resolveAsyncResumeTarget(params, deps = {}, options = {}) {
|
|
|
484
484
|
const requireSessionFile = options.requireSessionFile ?? true;
|
|
485
485
|
const location = resolveAsyncRunLocation(params, asyncDirRoot, resultsDir);
|
|
486
486
|
if (!location.asyncDir && !location.resultPath) {
|
|
487
|
-
|
|
487
|
+
const requested = params.id ?? params.runId;
|
|
488
|
+
throw new Error(requested ? `Async run not found for '${requested}'.` : "Async run not found. Provide id or dir.");
|
|
488
489
|
}
|
|
489
490
|
const reconciliation = location.asyncDir
|
|
490
491
|
? reconcileAsyncRun(location.asyncDir, { resultsDir, kill: deps.kill, now: deps.now })
|
|
@@ -606,7 +607,7 @@ export function resolveAsyncResumeTarget(params, deps = {}, options = {}) {
|
|
|
606
607
|
? intersectSubagentCapabilityCeilings(status?.admissionCapabilityCeiling, result?.admissionCapabilityCeiling)
|
|
607
608
|
: intersectSubagentCapabilityCeilings(status?.capabilityCeiling, statusSteps[index]?.capabilityCeiling, result?.capabilityCeiling, resultSteps[index]?.capabilityCeiling);
|
|
608
609
|
const managedWorktreeCwd = location.asyncDir
|
|
609
|
-
? resolveRetainedWorktreeCwd(parallelHandoffPath(location.asyncDir), runId, index)
|
|
610
|
+
? resolveRetainedWorktreeCwd(parallelHandoffPath(location.asyncDir), runId, index, statusSteps[index]?.worktreePath)
|
|
610
611
|
: undefined;
|
|
611
612
|
const resumeCwd = validateResumeCwd(runId, managedWorktreeCwd ?? status?.cwd ?? result?.cwd ?? recoveryDescriptor?.cwd);
|
|
612
613
|
return {
|
|
@@ -3,6 +3,7 @@ export declare const ASYNC_RETENTION_DAYS = 30;
|
|
|
3
3
|
export declare const ASYNC_RETENTION_BATCH_SIZE = 100;
|
|
4
4
|
export declare const ASYNC_RETENTION_DELAY_MS = 60000;
|
|
5
5
|
export declare const ASYNC_RETENTION_TOMBSTONE_GRACE_MS: number;
|
|
6
|
+
export declare const RESUMABLE_RETENTION_DAYS = 90;
|
|
6
7
|
export interface AsyncRetentionOptions {
|
|
7
8
|
asyncDirRoot: string;
|
|
8
9
|
resultsDir: string;
|
|
@@ -15,7 +15,11 @@ export const ASYNC_RETENTION_DAYS = 30;
|
|
|
15
15
|
export const ASYNC_RETENTION_BATCH_SIZE = 100;
|
|
16
16
|
export const ASYNC_RETENTION_DELAY_MS = 60_000;
|
|
17
17
|
export const ASYNC_RETENTION_TOMBSTONE_GRACE_MS = 24 * 60 * 60 * 1000;
|
|
18
|
+
// A run or result whose session file still exists can be resumed, so it is kept this long
|
|
19
|
+
// instead of ASYNC_RETENTION_DAYS. Session files outlive run dirs, so without a ceiling it is kept forever.
|
|
20
|
+
export const RESUMABLE_RETENTION_DAYS = 90;
|
|
18
21
|
const RETENTION_MS = ASYNC_RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
|
22
|
+
const RESUMABLE_RETENTION_MS = RESUMABLE_RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
|
19
23
|
const LOCK_NAME = ".async-retention.lock";
|
|
20
24
|
const CURSOR_NAME = ".async-retention-cursor.json";
|
|
21
25
|
const LOG_NAME = "async-retention-maintenance.jsonl";
|
|
@@ -239,9 +243,10 @@ function runSkipReason(input) {
|
|
|
239
243
|
return "mission-reference";
|
|
240
244
|
if (hasUnresolvedRunHandoff(runDir, status))
|
|
241
245
|
return "handoff-reference";
|
|
242
|
-
if (hasResumableContract(runDir, status))
|
|
243
|
-
return "resumable";
|
|
244
246
|
const timestamp = statusTimestamp(status, runDir);
|
|
247
|
+
// Unknown ages stay protected.
|
|
248
|
+
if (hasResumableContract(runDir, status) && (timestamp === undefined || timestamp > input.resumableCutoff))
|
|
249
|
+
return "resumable";
|
|
245
250
|
if (timestamp === undefined)
|
|
246
251
|
return "unknown-age";
|
|
247
252
|
if (timestamp > input.cutoff)
|
|
@@ -464,13 +469,13 @@ function resultSkipReason(input) {
|
|
|
464
469
|
return "workflow-reference";
|
|
465
470
|
if (hasUnresolvedResultHandoff(input.data))
|
|
466
471
|
return "handoff-reference";
|
|
467
|
-
|
|
472
|
+
const timestamp = resultTimestamp(input.data, input.candidate.path);
|
|
473
|
+
if (resultHasResumableSession(input.data) && (timestamp === undefined || timestamp > input.resumableCutoff))
|
|
468
474
|
return "resumable";
|
|
469
475
|
if (input.candidate.kind === "archive" && fs.existsSync(path.join(input.resultsDir, "completion-replay", `${encodeURIComponent(runId)}.json`)))
|
|
470
476
|
return "replay-reference";
|
|
471
477
|
if (!terminalResult(input.data) && input.candidate.kind !== "replay" && input.candidate.kind !== "archive" && input.candidate.kind !== "tombstone")
|
|
472
478
|
return "non-terminal-result";
|
|
473
|
-
const timestamp = resultTimestamp(input.data, input.candidate.path);
|
|
474
479
|
if (timestamp === undefined)
|
|
475
480
|
return "unknown-age";
|
|
476
481
|
if (timestamp > input.cutoff)
|
|
@@ -690,6 +695,7 @@ export async function cleanupAsyncRetention(options) {
|
|
|
690
695
|
return finish();
|
|
691
696
|
const protectedRunIds = new Set(options.protectedRunIds ?? []);
|
|
692
697
|
const cutoff = currentTime - retentionMs;
|
|
698
|
+
const resumableCutoff = currentTime - RESUMABLE_RETENTION_MS;
|
|
693
699
|
const cursor = readCursor(maintenanceRoot);
|
|
694
700
|
const runBudget = Math.ceil(batchSize / 2);
|
|
695
701
|
const resultBudget = batchSize - runBudget;
|
|
@@ -765,7 +771,7 @@ export async function cleanupAsyncRetention(options) {
|
|
|
765
771
|
if (reconciliation.repaired)
|
|
766
772
|
result.repairedRuns += 1;
|
|
767
773
|
}
|
|
768
|
-
const initialReason = runSkipReason({ runDir, status, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, cutoff, protectedRunIds, waitRunIds: waitReferences.runIds });
|
|
774
|
+
const initialReason = runSkipReason({ runDir, status, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, cutoff, resumableCutoff, protectedRunIds, waitRunIds: waitReferences.runIds });
|
|
769
775
|
if (initialReason) {
|
|
770
776
|
increment(result.skipped, initialReason);
|
|
771
777
|
continue;
|
|
@@ -780,7 +786,7 @@ export async function cleanupAsyncRetention(options) {
|
|
|
780
786
|
continue;
|
|
781
787
|
}
|
|
782
788
|
const freshWaitReferences = parseWaitRunIds(waitSubscriptionsDir);
|
|
783
|
-
const finalReason = freshWaitReferences.safe ? runSkipReason({ runDir, status, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, cutoff, protectedRunIds, waitRunIds: freshWaitReferences.runIds }) : "wait-references-unknown";
|
|
789
|
+
const finalReason = freshWaitReferences.safe ? runSkipReason({ runDir, status, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, cutoff, resumableCutoff, protectedRunIds, waitRunIds: freshWaitReferences.runIds }) : "wait-references-unknown";
|
|
784
790
|
if (finalReason) {
|
|
785
791
|
increment(result.skipped, `recheck-${finalReason}`);
|
|
786
792
|
continue;
|
|
@@ -805,7 +811,7 @@ export async function cleanupAsyncRetention(options) {
|
|
|
805
811
|
}
|
|
806
812
|
const recheckStatus = readStatus(tombstone);
|
|
807
813
|
const freshWaitReferences = parseWaitRunIds(waitSubscriptionsDir);
|
|
808
|
-
const recheckReason = freshWaitReferences.safe ? runSkipReason({ runDir: tombstone, status: recheckStatus, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, cutoff, protectedRunIds, waitRunIds: freshWaitReferences.runIds }) : "wait-references-unknown";
|
|
814
|
+
const recheckReason = freshWaitReferences.safe ? runSkipReason({ runDir: tombstone, status: recheckStatus, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, cutoff, resumableCutoff, protectedRunIds, waitRunIds: freshWaitReferences.runIds }) : "wait-references-unknown";
|
|
809
815
|
if (recheckReason) {
|
|
810
816
|
if (!fs.existsSync(runDir)) {
|
|
811
817
|
fs.renameSync(tombstone, runDir);
|
|
@@ -842,7 +848,7 @@ export async function cleanupAsyncRetention(options) {
|
|
|
842
848
|
continue;
|
|
843
849
|
}
|
|
844
850
|
const data = readJson(candidate.path);
|
|
845
|
-
const reason = resultSkipReason({ candidate, data, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, maintenanceRoot, cutoff, protectedRunIds, waitRunIds: waitReferences.runIds });
|
|
851
|
+
const reason = resultSkipReason({ candidate, data, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, maintenanceRoot, cutoff, resumableCutoff, protectedRunIds, waitRunIds: waitReferences.runIds });
|
|
846
852
|
if (reason) {
|
|
847
853
|
increment(result.skipped, reason);
|
|
848
854
|
continue;
|
|
@@ -853,7 +859,7 @@ export async function cleanupAsyncRetention(options) {
|
|
|
853
859
|
continue;
|
|
854
860
|
}
|
|
855
861
|
const freshWaitReferences = parseWaitRunIds(waitSubscriptionsDir);
|
|
856
|
-
const finalReason = freshWaitReferences.safe ? resultSkipReason({ candidate, data, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, maintenanceRoot, cutoff, protectedRunIds, waitRunIds: freshWaitReferences.runIds }) : "wait-references-unknown";
|
|
862
|
+
const finalReason = freshWaitReferences.safe ? resultSkipReason({ candidate, data, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, maintenanceRoot, cutoff, resumableCutoff, protectedRunIds, waitRunIds: freshWaitReferences.runIds }) : "wait-references-unknown";
|
|
857
863
|
if (finalReason) {
|
|
858
864
|
increment(result.skipped, `recheck-${finalReason}`);
|
|
859
865
|
continue;
|
|
@@ -870,7 +876,7 @@ export async function cleanupAsyncRetention(options) {
|
|
|
870
876
|
const tombstoneCandidate = { ...candidate, path: tombstone, kind: "tombstone" };
|
|
871
877
|
const recheckData = readJson(tombstone);
|
|
872
878
|
const freshWaitReferences = parseWaitRunIds(waitSubscriptionsDir);
|
|
873
|
-
const recheckReason = freshWaitReferences.safe ? resultSkipReason({ candidate: tombstoneCandidate, data: recheckData, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, maintenanceRoot, cutoff, protectedRunIds, waitRunIds: freshWaitReferences.runIds }) : "wait-references-unknown";
|
|
879
|
+
const recheckReason = freshWaitReferences.safe ? resultSkipReason({ candidate: tombstoneCandidate, data: recheckData, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, maintenanceRoot, cutoff, resumableCutoff, protectedRunIds, waitRunIds: freshWaitReferences.runIds }) : "wait-references-unknown";
|
|
874
880
|
if (recheckReason) {
|
|
875
881
|
if (!fs.existsSync(candidate.path)) {
|
|
876
882
|
fs.renameSync(tombstone, candidate.path);
|
|
@@ -52,7 +52,7 @@ function childResumability(run, step) {
|
|
|
52
52
|
}
|
|
53
53
|
let requiredCwd;
|
|
54
54
|
try {
|
|
55
|
-
requiredCwd = resolveRetainedWorktreeCwd(parallelHandoffPath(run.asyncDir), run.id, step.index) ?? recoveryDescriptor.cwd ?? run.cwd;
|
|
55
|
+
requiredCwd = resolveRetainedWorktreeCwd(parallelHandoffPath(run.asyncDir), run.id, step.index, step.worktreePath) ?? recoveryDescriptor.cwd ?? run.cwd;
|
|
56
56
|
if (!requiredCwd || !fs.statSync(requiredCwd).isDirectory())
|
|
57
57
|
return { state: "not-resumable", reason: `required cwd is missing: ${requiredCwd ?? "unknown"}` };
|
|
58
58
|
}
|
|
@@ -83,6 +83,8 @@ export interface RunChildSessionInput {
|
|
|
83
83
|
}>;
|
|
84
84
|
modelResponseAliases?: Record<string, string[]>;
|
|
85
85
|
mutationTools?: readonly string[];
|
|
86
|
+
/** Provider of the launch model that a parent extension registered and this child does not load. */
|
|
87
|
+
parentExtensionProvider?: string;
|
|
86
88
|
}
|
|
87
89
|
export interface RunChildSessionResult {
|
|
88
90
|
exitCode: number;
|
|
@@ -3,7 +3,7 @@ import { qualifyModelWithProvider } from "../../shared/model-info.js";
|
|
|
3
3
|
import { acceptChildWatchdogEvent, applyChildWatchdogMessage, childWatchdogIsActive, isChildWatchdogStatusEvent, } from "../../watchdog/child-status.js";
|
|
4
4
|
import { projectChildLifecycle } from "../shared/child-lifecycle.js";
|
|
5
5
|
import { formatSubagentModelVerificationError } from "../shared/model-resolution.js";
|
|
6
|
-
import { formatChildModelResolutionDiagnostic, isChildModelResolutionFailure } from "../shared/model-resolution-diagnostic.js";
|
|
6
|
+
import { formatChildModelResolutionDiagnostic, formatMissingParentProviderDiagnostic, isChildModelResolutionFailure } from "../shared/model-resolution-diagnostic.js";
|
|
7
7
|
import { isMutatingTool, resolveCurrentPath } from "../shared/long-running-guard.js";
|
|
8
8
|
import { effectiveToolTimeoutMs, formatToolTimeoutMessage, toolTimeoutCallKey } from "../shared/tool-timeout.js";
|
|
9
9
|
import { createReportedChildSessionInput } from "../shared/child-launch.js";
|
|
@@ -520,6 +520,14 @@ export function runChildSession(input) {
|
|
|
520
520
|
&& !input.launch.session.ambientExtensions) {
|
|
521
521
|
finalError = `${promptErrorMessage}\n\n${formatChildModelResolutionDiagnostic({ agent: input.launch.config.agent, model: input.launch.session.model, host: "runner", capabilityCeiling: input.launch.toolPlan.capabilityCeiling })}`;
|
|
522
522
|
}
|
|
523
|
+
else if (input.parentExtensionProvider
|
|
524
|
+
&& session?.claimedProvider !== input.parentExtensionProvider
|
|
525
|
+
&& finalError !== undefined
|
|
526
|
+
&& (finalError === promptErrorMessage || finalError === assistantError)
|
|
527
|
+
&& toolCount === 0
|
|
528
|
+
&& messages.every((message) => message.role !== "assistant" || message.stopReason === "error")) {
|
|
529
|
+
finalError = `${finalError}\n\n${formatMissingParentProviderDiagnostic({ agent: input.launch.config.agent, model: input.launch.session.model, provider: input.parentExtensionProvider, capabilityCeiling: input.launch.toolPlan.capabilityCeiling })}`;
|
|
530
|
+
}
|
|
523
531
|
const forcedDrainAfterFinalSuccess = (forced || forcedTermination) && (cleanTerminalAssistantStopReceived || agentSettledReceived) && !finalError;
|
|
524
532
|
const forcedDrainAfterEmptyTerminal = forcedDrainAfterFinalSuccess && hasEmptyTerminalAssistantResponse(messages);
|
|
525
533
|
if (!finalError && forced && !forcedDrainAfterFinalSuccess && !interrupted && !timedOut && !stopped) {
|
|
@@ -938,6 +938,7 @@ export async function runSingleStepInner(step, ctx) {
|
|
|
938
938
|
modelVerificationRegistry: step.modelVerificationRegistry,
|
|
939
939
|
modelResponseAliases: step.modelResponseAliases,
|
|
940
940
|
mutationTools: step.mutationTools,
|
|
941
|
+
parentExtensionProvider: step.parentExtensionProvider,
|
|
941
942
|
}));
|
|
942
943
|
launched = true;
|
|
943
944
|
aggregateUsage.input += run.usage.input;
|