pi-subagents 0.49.0 → 0.50.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 +35 -0
- package/agents/oracle.md +7 -5
- package/agents/researcher.md +1 -1
- package/agents/reviewer.md +2 -2
- package/agents/scout.md +1 -1
- package/agents/worker.md +1 -1
- package/docs/agents.md +2 -0
- package/docs/configuration.md +47 -3
- package/docs/extension-api.md +36 -0
- package/docs/missions.md +1 -1
- package/docs/observability.md +16 -0
- package/docs/tool-reference.md +19 -2
- package/docs/workflows.md +2 -2
- package/package.json +1 -1
- package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
- package/skills/pi-subagents/references/execution-controls.md +5 -4
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/skills/pi-subagents/references/prompting-and-roles.md +31 -15
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +10 -0
- package/src/api/external-runs.ts +174 -84
- package/src/api/preflight.ts +12 -6
- package/src/extension/config.ts +33 -0
- package/src/extension/index.ts +48 -5
- package/src/extension/public-execution.ts +3 -1
- package/src/extension/rpc.ts +5 -1
- package/src/extension/schemas.ts +2 -1
- package/src/extension/tool-description.ts +4 -2
- package/src/inspectors/herdr/actions.ts +3 -7
- package/src/inspectors/herdr/project-panes.ts +2 -6
- package/src/inspectors/herdr/shell-command.ts +16 -0
- package/src/intercom/intercom-bridge.ts +2 -3
- package/src/intercom/native-supervisor-channel.ts +19 -42
- package/src/missions/goal-driver.ts +3 -1
- package/src/runs/background/active-run-index.ts +71 -1
- package/src/runs/background/async-execution.ts +49 -7
- package/src/runs/background/async-job-tracker.ts +1 -0
- package/src/runs/background/async-resume.ts +3 -5
- package/src/runs/background/async-status-snapshot.ts +277 -0
- package/src/runs/background/async-status.ts +8 -3
- package/src/runs/background/chain-root-attachment.ts +2 -2
- package/src/runs/background/completion-replay.ts +11 -1
- package/src/runs/background/fleet-view.ts +3 -1
- package/src/runs/background/result-files.ts +437 -0
- package/src/runs/background/result-watcher.ts +188 -41
- package/src/runs/background/retained-children.ts +69 -20
- package/src/runs/background/run-id-resolver.ts +30 -24
- package/src/runs/background/run-status.ts +7 -2
- package/src/runs/background/scheduled-runs.ts +54 -28
- package/src/runs/background/stale-run-reconciler.ts +27 -13
- package/src/runs/background/subagent-runner.ts +294 -32
- package/src/runs/background/subagent-wait.ts +2 -0
- package/src/runs/background/wait-completions.ts +5 -2
- package/src/runs/foreground/async-dismiss-action.ts +2 -1
- package/src/runs/foreground/chain-execution.ts +16 -0
- package/src/runs/foreground/execution.ts +219 -15
- package/src/runs/foreground/subagent-executor.ts +114 -28
- package/src/runs/shared/completion-guard.ts +96 -6
- package/src/runs/shared/external-cli-runner.ts +4 -0
- package/src/runs/shared/model-fallback.ts +16 -2
- package/src/runs/shared/nested-events.ts +66 -62
- package/src/runs/shared/orca-progress-tabs.ts +375 -0
- package/src/runs/shared/parallel-utils.ts +2 -0
- package/src/runs/shared/subagent-control.ts +15 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +1 -9
- package/src/runs/shared/subagent-startup-retry.ts +12 -0
- package/src/runs/shared/tool-timeout.ts +93 -0
- package/src/shared/types.ts +29 -2
- package/src/slash/slash-commands.ts +34 -25
- package/src/slash/slash-live-state.ts +3 -0
- package/src/tui/fleet-status.ts +160 -45
- package/src/tui/fleet.ts +125 -15
- package/src/tui/render.ts +41 -10
- package/src/workflows/chat-progress.ts +6 -4
- package/src/workflows/scripted-workflow.ts +227 -70
|
@@ -22,12 +22,14 @@ import { projectNestedRegistryForRoot, sanitizeSummary } from "../shared/nested-
|
|
|
22
22
|
import { resolveWatchPath } from "../../shared/utils.ts";
|
|
23
23
|
import { recordWaitCompletion } from "./wait-completions.ts";
|
|
24
24
|
import { MISSION_BINDING_FILE, syncMissionFromAsyncCompletion } from "../../missions/lifecycle.ts";
|
|
25
|
+
import { missionObserverResultCandidateFiles, promotePendingResultFile, removeMissionObserverIndex, removeResultIndex, resultCandidateFilesForSession, resultPayloadPathForIndexedRun, resultPayloadPathForMissionObserverRun, resultPayloadPathForSessionRun, writeAsyncResultFile, writeResultIndexForData } from "./result-files.ts";
|
|
25
26
|
import type { CompletionNotifier, CompletionNotification } from "./notify.ts";
|
|
26
27
|
|
|
27
28
|
const WATCHER_RESTART_DELAY_MS = 3000;
|
|
28
29
|
const POLL_INTERVAL_MS = 3000;
|
|
29
30
|
const HEALTHY_SCAN_INTERVAL_MS = 60_000;
|
|
30
31
|
const RETRY_DELAY_MS = 100;
|
|
32
|
+
const SLOW_RESULT_SCAN_MS = 500;
|
|
31
33
|
|
|
32
34
|
type ResultWatcherFs = Pick<typeof fs, "existsSync" | "readFileSync" | "unlinkSync" | "readdirSync" | "mkdirSync" | "realpathSync" | "statSync" | "watch">;
|
|
33
35
|
|
|
@@ -50,6 +52,8 @@ type ResultWatcherDeps = {
|
|
|
50
52
|
parseResult?: (raw: string) => ResultFileData;
|
|
51
53
|
/** External grouped-result transport. Disable when native completion notifications own delivery. */
|
|
52
54
|
deliverIntercomResults?: boolean;
|
|
55
|
+
/** Coalesces result-file events. Tests can lower this without changing retry timing. */
|
|
56
|
+
coalesceDelayMs?: number;
|
|
53
57
|
};
|
|
54
58
|
|
|
55
59
|
type ResultFileChild = {
|
|
@@ -79,6 +83,7 @@ type ResultFileData = CompletionNotification & {
|
|
|
79
83
|
asyncDir?: string;
|
|
80
84
|
intercomTarget?: string;
|
|
81
85
|
parallelHandoff?: ParallelHandoffReference;
|
|
86
|
+
notificationDeliveredAt?: unknown;
|
|
82
87
|
};
|
|
83
88
|
|
|
84
89
|
type ResultFileIdentity = {
|
|
@@ -87,6 +92,12 @@ type ResultFileIdentity = {
|
|
|
87
92
|
asyncDir?: string;
|
|
88
93
|
};
|
|
89
94
|
|
|
95
|
+
interface ResultScanStats {
|
|
96
|
+
files: number;
|
|
97
|
+
scheduled: number;
|
|
98
|
+
startedAt: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
90
101
|
function jsonStringProperty(raw: string, property: string): string | undefined {
|
|
91
102
|
const matches = raw.matchAll(new RegExp(`"${property}"\\s*:\\s*("(?:\\\\.|[^"\\\\])*")`, "g"));
|
|
92
103
|
let encoded: string | undefined;
|
|
@@ -134,6 +145,16 @@ function shouldPoll(error: unknown): boolean {
|
|
|
134
145
|
return code === "EMFILE" || code === "ENOSPC";
|
|
135
146
|
}
|
|
136
147
|
|
|
148
|
+
function hasDeliveredNotification(data: ResultFileData): boolean {
|
|
149
|
+
return typeof data.notificationDeliveredAt === "number" && Number.isFinite(data.notificationDeliveredAt);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function markDeliveredNotification(resultPath: string, data: ResultFileData, runId: string, now: number): ResultFileData {
|
|
153
|
+
const marked = { ...data, runId, notificationDeliveredAt: now };
|
|
154
|
+
writeAsyncResultFile(resultPath, marked);
|
|
155
|
+
return marked;
|
|
156
|
+
}
|
|
157
|
+
|
|
137
158
|
/**
|
|
138
159
|
* Watches persisted async results for the session currently owned by this
|
|
139
160
|
* runtime. `stopResultWatcher()` revokes ownership before closing resources,
|
|
@@ -177,16 +198,55 @@ export function createResultWatcher(
|
|
|
177
198
|
state.resultFileCoalescer.schedule(file, delayMs);
|
|
178
199
|
};
|
|
179
200
|
|
|
180
|
-
const
|
|
181
|
-
|
|
201
|
+
const publicResultPath = (file: string): string => path.join(resultsDir, file);
|
|
202
|
+
const publicResultFileExists = (file: string): boolean => {
|
|
203
|
+
try {
|
|
204
|
+
return fsApi.statSync(publicResultPath(file)).isFile();
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (!isNotFound(error)) console.error(`Failed to inspect subagent result file '${publicResultPath(file)}':`, error);
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
const resultPayloadPath = (file: string, observed?: ReadonlySet<string>): string | undefined => {
|
|
211
|
+
if (file !== path.basename(file) || !file.endsWith(".json")) return undefined;
|
|
212
|
+
const runId = file.replace(/\.json$/i, "");
|
|
213
|
+
const sessionResult = state.currentSessionId ? resultPayloadPathForSessionRun(resultsDir, state.currentSessionId, runId) : undefined;
|
|
214
|
+
if (sessionResult) return sessionResult;
|
|
215
|
+
const observerResult = resultPayloadPathForMissionObserverRun(resultsDir, runId);
|
|
216
|
+
if (observerResult) return observerResult;
|
|
217
|
+
if (observed?.has(runId)) {
|
|
218
|
+
const indexedResult = resultPayloadPathForIndexedRun(resultsDir, runId);
|
|
219
|
+
if (indexedResult) return indexedResult;
|
|
220
|
+
}
|
|
221
|
+
return publicResultFileExists(file) ? publicResultPath(file) : undefined;
|
|
222
|
+
};
|
|
223
|
+
const resultSignature = (file: string, observed?: ReadonlySet<string>): string | undefined => {
|
|
224
|
+
const resultPath = resultPayloadPath(file, observed);
|
|
225
|
+
if (!resultPath) {
|
|
226
|
+
identityCache.delete(file);
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
182
229
|
try {
|
|
183
230
|
const stat = fsApi.statSync(resultPath);
|
|
184
|
-
|
|
231
|
+
if (!stat.isFile()) return undefined;
|
|
232
|
+
return `${resultPath}:${stat.size}:${stat.mtimeMs}`;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
identityCache.delete(file);
|
|
235
|
+
if (!isNotFound(error)) console.error(`Failed to inspect subagent result file '${resultPath}':`, error);
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
const inspectResult = (file: string, knownSignature?: string, observed?: ReadonlySet<string>): { identity: ResultFileIdentity; signature: string } | undefined => {
|
|
240
|
+
const resultPath = resultPayloadPath(file, observed);
|
|
241
|
+
if (!resultPath) return undefined;
|
|
242
|
+
try {
|
|
243
|
+
const signature = knownSignature ?? resultSignature(file, observed);
|
|
244
|
+
if (!signature) return undefined;
|
|
185
245
|
const cached = identityCache.get(file);
|
|
186
|
-
if (cached?.signature === signature) return cached.identity;
|
|
246
|
+
if (cached?.signature === signature) return { identity: cached.identity, signature };
|
|
187
247
|
const identity = resultFileIdentity(fsApi.readFileSync(resultPath, "utf-8"), file);
|
|
188
248
|
identityCache.set(file, { signature, identity });
|
|
189
|
-
return identity;
|
|
249
|
+
return { identity, signature };
|
|
190
250
|
} catch (error) {
|
|
191
251
|
identityCache.delete(file);
|
|
192
252
|
if (!isNotFound(error)) console.error(`Failed to inspect subagent result file '${resultPath}':`, error);
|
|
@@ -203,9 +263,10 @@ export function createResultWatcher(
|
|
|
203
263
|
}
|
|
204
264
|
};
|
|
205
265
|
|
|
206
|
-
const shouldProcessResult = (file: string, observed?: ReadonlySet<string
|
|
207
|
-
const
|
|
208
|
-
if (!
|
|
266
|
+
const shouldProcessResult = (file: string, observed?: ReadonlySet<string>, knownSignature?: string): boolean => {
|
|
267
|
+
const inspected = inspectResult(file, knownSignature, observed);
|
|
268
|
+
if (!inspected) return false;
|
|
269
|
+
const { identity } = inspected;
|
|
209
270
|
// Missing identity stays on the normal parser path so malformed or legacy
|
|
210
271
|
// files keep their existing diagnostics and compatibility behavior.
|
|
211
272
|
if (!identity.sessionId) return true;
|
|
@@ -215,32 +276,80 @@ export function createResultWatcher(
|
|
|
215
276
|
return Boolean(deps.observeCompletion && !deps.observedCompletionRunIds);
|
|
216
277
|
};
|
|
217
278
|
|
|
279
|
+
const removeDeliveredResult = (file: string, sessionId: string, runId: string, toolCallId: string | undefined): boolean => {
|
|
280
|
+
try {
|
|
281
|
+
if (publicResultFileExists(file)) fsApi.unlinkSync(publicResultPath(file));
|
|
282
|
+
identityCache.delete(file);
|
|
283
|
+
removeResultIndex(resultsDir, sessionId, runId, toolCallId);
|
|
284
|
+
return true;
|
|
285
|
+
} catch (error) {
|
|
286
|
+
if (!isNotFound(error)) {
|
|
287
|
+
console.error(`Failed to remove delivered subagent result '${publicResultPath(file)}'; will retry:`, error);
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
};
|
|
218
293
|
const handleResult = async (file: string, triggerTurn: boolean) => {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (!shouldProcessResult(file))
|
|
294
|
+
if (processing.has(file)) return;
|
|
295
|
+
let observed: ReadonlySet<string> | undefined;
|
|
296
|
+
if (!shouldProcessResult(file)) {
|
|
297
|
+
const runId = file === path.basename(file) && file.endsWith(".json") ? file.replace(/\.json$/i, "") : undefined;
|
|
298
|
+
observed = observedRunIds();
|
|
299
|
+
if (!runId || !observed.has(runId) || !shouldProcessResult(file, observed)) return;
|
|
300
|
+
}
|
|
222
301
|
processing.add(file);
|
|
302
|
+
let resultPath = publicResultPath(file);
|
|
223
303
|
try {
|
|
224
|
-
const
|
|
304
|
+
const payloadPath = resultPayloadPath(file, observed);
|
|
305
|
+
if (!payloadPath) return;
|
|
306
|
+
resultPath = payloadPath;
|
|
307
|
+
let raw = fsApi.readFileSync(resultPath, "utf-8");
|
|
308
|
+
let identity = resultFileIdentity(raw, file);
|
|
309
|
+
if (identity.sessionId && identity.runId) {
|
|
310
|
+
const pendingState = promotePendingResultFile(resultsDir, identity.sessionId, identity.runId, file);
|
|
311
|
+
if (pendingState === "promoted") {
|
|
312
|
+
identityCache.delete(file);
|
|
313
|
+
resultPath = publicResultPath(file);
|
|
314
|
+
raw = fsApi.readFileSync(resultPath, "utf-8");
|
|
315
|
+
identity = resultFileIdentity(raw, file);
|
|
316
|
+
} else if (pendingState === "pending") {
|
|
317
|
+
const pendingPath = resultPayloadPathForSessionRun(resultsDir, identity.sessionId, identity.runId);
|
|
318
|
+
if (!pendingPath) {
|
|
319
|
+
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
resultPath = pendingPath;
|
|
323
|
+
raw = fsApi.readFileSync(resultPath, "utf-8");
|
|
324
|
+
identity = resultFileIdentity(raw, file);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
let data = parseResult(raw);
|
|
225
328
|
if (typeof data.sessionId !== "string" || !data.sessionId) return;
|
|
329
|
+
const sessionId = data.sessionId;
|
|
226
330
|
const runId = data.runId ?? data.id ?? file.replace(/\.json$/i, "");
|
|
331
|
+
const toolCallId = typeof data.toolCallId === "string" ? data.toolCallId : undefined;
|
|
332
|
+
let observerSucceeded = true;
|
|
227
333
|
try {
|
|
228
334
|
syncMissionFromAsyncCompletion({ ...data, runId });
|
|
229
335
|
} catch (error) {
|
|
336
|
+
observerSucceeded = false;
|
|
230
337
|
console.error(`Mission completion sync failed for '${resultPath}':`, error);
|
|
231
338
|
}
|
|
232
339
|
try {
|
|
233
340
|
deps.observeCompletion?.({ ...data, runId });
|
|
234
341
|
} catch (error) {
|
|
342
|
+
observerSucceeded = false;
|
|
235
343
|
console.error(`Completion observer failed for '${resultPath}':`, error);
|
|
236
344
|
}
|
|
345
|
+
if (observerSucceeded) removeMissionObserverIndex(resultsDir, runId);
|
|
237
346
|
const epoch = deliveryEpoch;
|
|
238
|
-
if (!ownsSession(
|
|
347
|
+
if (!ownsSession(sessionId, epoch)) return;
|
|
239
348
|
// Recorded before dedupe and before the unlink below so subagent_wait can
|
|
240
349
|
// use the in-memory record or its bounded durable replay after cleanup.
|
|
241
350
|
recordWaitCompletion(state, runId, data, Date.now(), completionTtlMs, {
|
|
242
351
|
resultsDir,
|
|
243
|
-
sessionId
|
|
352
|
+
sessionId,
|
|
244
353
|
});
|
|
245
354
|
const hasExplicitNestedChildren = data.nestedChildren !== undefined;
|
|
246
355
|
let nestedChildren = compactNestedResultChildren(sanitizeNestedResultChildren(data.nestedChildren, resultPath, "nestedChildren"));
|
|
@@ -255,20 +364,17 @@ export function createResultWatcher(
|
|
|
255
364
|
}
|
|
256
365
|
|
|
257
366
|
const completionKey = buildCompletionKey(data, `result:${file}`);
|
|
367
|
+
const alreadyDelivered = hasDeliveredNotification(data);
|
|
258
368
|
const lastSeenAt = state.completionSeen.get(completionKey);
|
|
259
369
|
if (lastSeenAt !== undefined && Date.now() - lastSeenAt > completionTtlMs) {
|
|
260
370
|
state.completionSeen.delete(completionKey);
|
|
261
371
|
} else if (lastSeenAt !== undefined) {
|
|
262
|
-
if (!
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
identityCache.delete(file);
|
|
266
|
-
} catch (error) {
|
|
267
|
-
if (!isNotFound(error)) {
|
|
268
|
-
console.error(`Failed to remove delivered subagent result '${resultPath}'; will retry:`, error);
|
|
269
|
-
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
270
|
-
}
|
|
372
|
+
if (!observerSucceeded) {
|
|
373
|
+
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
374
|
+
return;
|
|
271
375
|
}
|
|
376
|
+
if (!ownsSession(sessionId, epoch)) return;
|
|
377
|
+
if (!removeDeliveredResult(file, sessionId, runId, toolCallId)) scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
272
378
|
return;
|
|
273
379
|
}
|
|
274
380
|
|
|
@@ -316,6 +422,17 @@ export function createResultWatcher(
|
|
|
316
422
|
};
|
|
317
423
|
}), nestedChildren);
|
|
318
424
|
|
|
425
|
+
if (alreadyDelivered) {
|
|
426
|
+
markSeenWithTtl(state.completionSeen, completionKey, Date.now(), completionTtlMs);
|
|
427
|
+
if (!observerSucceeded) {
|
|
428
|
+
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (!ownsSession(sessionId, epoch)) return;
|
|
432
|
+
if (!removeDeliveredResult(file, sessionId, runId, toolCallId)) scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
319
436
|
const intercomTarget = data.intercomTarget?.trim();
|
|
320
437
|
let intercomDelivered = false;
|
|
321
438
|
if (deliverIntercomResults && intercomTarget && triggerTurn) {
|
|
@@ -332,7 +449,7 @@ export function createResultWatcher(
|
|
|
332
449
|
asyncDir: data.asyncDir,
|
|
333
450
|
...(data.parallelHandoff ? { parallelHandoff: data.parallelHandoff } : {}),
|
|
334
451
|
}));
|
|
335
|
-
if (!ownsSession(
|
|
452
|
+
if (!ownsSession(sessionId, epoch)) return;
|
|
336
453
|
if (!intercomDelivered) console.error(`Subagent async grouped result intercom delivery was not acknowledged for '${resultPath}'.`);
|
|
337
454
|
}
|
|
338
455
|
|
|
@@ -356,11 +473,19 @@ export function createResultWatcher(
|
|
|
356
473
|
})) : [],
|
|
357
474
|
} : {}),
|
|
358
475
|
});
|
|
359
|
-
if (!ownsSession(
|
|
476
|
+
if (!ownsSession(sessionId, epoch)) return;
|
|
360
477
|
if (!accepted) {
|
|
361
478
|
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
362
479
|
return;
|
|
363
480
|
}
|
|
481
|
+
try {
|
|
482
|
+
data = markDeliveredNotification(publicResultPath(file), data, runId, Date.now());
|
|
483
|
+
identityCache.delete(file);
|
|
484
|
+
} catch (error) {
|
|
485
|
+
console.error(`Failed to mark subagent result notification delivered for '${resultPath}'; will retry:`, error);
|
|
486
|
+
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
364
489
|
markSeenWithTtl(state.completionSeen, completionKey, Date.now(), completionTtlMs);
|
|
365
490
|
try {
|
|
366
491
|
pi.events.emit(SUBAGENT_ASYNC_COMPLETE_EVENT, {
|
|
@@ -385,16 +510,12 @@ export function createResultWatcher(
|
|
|
385
510
|
} catch (error) {
|
|
386
511
|
console.error(`Completion observer failed for '${resultPath}':`, error);
|
|
387
512
|
}
|
|
388
|
-
if (!
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
identityCache.delete(file);
|
|
392
|
-
} catch (error) {
|
|
393
|
-
if (!isNotFound(error)) {
|
|
394
|
-
console.error(`Failed to remove delivered subagent result '${resultPath}'; will retry:`, error);
|
|
395
|
-
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
396
|
-
}
|
|
513
|
+
if (!observerSucceeded) {
|
|
514
|
+
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
515
|
+
return;
|
|
397
516
|
}
|
|
517
|
+
if (!ownsSession(sessionId, epoch)) return;
|
|
518
|
+
if (!removeDeliveredResult(file, sessionId, runId, toolCallId)) scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
398
519
|
} catch (error) {
|
|
399
520
|
if (!isNotFound(error)) console.error(`Failed to process subagent result file '${resultPath}':`, error);
|
|
400
521
|
} finally {
|
|
@@ -406,18 +527,39 @@ export function createResultWatcher(
|
|
|
406
527
|
const triggerTurn = pendingTriggerTurn.get(file) !== false;
|
|
407
528
|
pendingTriggerTurn.delete(file);
|
|
408
529
|
void handleResult(file, triggerTurn);
|
|
409
|
-
}, 50);
|
|
530
|
+
}, deps.coalesceDelayMs ?? 50);
|
|
410
531
|
|
|
532
|
+
const logScanStats = (stats: ResultScanStats) => {
|
|
533
|
+
const elapsed = Date.now() - stats.startedAt;
|
|
534
|
+
if (elapsed < SLOW_RESULT_SCAN_MS) return;
|
|
535
|
+
console.error(`Subagent result scan inspected ${stats.files} indexed result file(s), scheduled ${stats.scheduled} in ${elapsed}ms (${resultsDir}).`);
|
|
536
|
+
};
|
|
537
|
+
const indexedResultCandidates = (observed: ReadonlySet<string>): string[] => {
|
|
538
|
+
const files = new Set<string>();
|
|
539
|
+
if (state.currentSessionId) {
|
|
540
|
+
for (const file of resultCandidateFilesForSession(resultsDir, state.currentSessionId)) files.add(file);
|
|
541
|
+
}
|
|
542
|
+
for (const runId of state.asyncJobs.keys()) files.add(`${runId}.json`);
|
|
543
|
+
for (const file of missionObserverResultCandidateFiles(resultsDir)) files.add(file);
|
|
544
|
+
for (const runId of observed) files.add(`${runId}.json`);
|
|
545
|
+
return [...files];
|
|
546
|
+
};
|
|
411
547
|
const primeExistingResults = (options: { triggerTurn?: boolean } = {}) => {
|
|
412
548
|
try {
|
|
413
549
|
const triggerTurn = options.triggerTurn !== false;
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
550
|
+
const stats: ResultScanStats = { files: 0, scheduled: 0, startedAt: Date.now() };
|
|
551
|
+
const observed = observedRunIds();
|
|
552
|
+
for (const file of indexedResultCandidates(observed)) {
|
|
553
|
+
stats.files += 1;
|
|
554
|
+
const signature = resultSignature(file, observed);
|
|
555
|
+
if (!signature) continue;
|
|
556
|
+
if (!shouldProcessResult(file, observed, signature)) continue;
|
|
557
|
+
stats.scheduled += 1;
|
|
558
|
+
scheduleResult(file, triggerTurn);
|
|
559
|
+
}
|
|
560
|
+
logScanStats(stats);
|
|
419
561
|
} catch (error) {
|
|
420
|
-
if (!isNotFound(error)) console.error(`Failed to scan subagent result
|
|
562
|
+
if (!isNotFound(error)) console.error(`Failed to scan subagent result index in '${resultsDir}':`, error);
|
|
421
563
|
}
|
|
422
564
|
};
|
|
423
565
|
|
|
@@ -474,6 +616,11 @@ export function createResultWatcher(
|
|
|
474
616
|
const fileName = file.toString();
|
|
475
617
|
if (fileName.endsWith(".json")) {
|
|
476
618
|
identityCache.delete(fileName);
|
|
619
|
+
try {
|
|
620
|
+
writeResultIndexForData(path.join(resultsDir, fileName), JSON.parse(fsApi.readFileSync(path.join(resultsDir, fileName), "utf-8")) as Record<string, unknown>);
|
|
621
|
+
} catch {
|
|
622
|
+
// The writer may still be renaming the file; handleResult will retry from the normal result path.
|
|
623
|
+
}
|
|
477
624
|
scheduleResult(fileName, true);
|
|
478
625
|
}
|
|
479
626
|
});
|
|
@@ -1,32 +1,68 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import { listAsyncRuns } from "./async-status.ts";
|
|
2
|
+
import { listAsyncRuns, type AsyncRunSummary } from "./async-status.ts";
|
|
3
|
+
import { readAsyncRecoveryDescriptor } from "./async-resume.ts";
|
|
3
4
|
import type { TokenUsage } from "../../shared/types.ts";
|
|
4
5
|
|
|
5
6
|
const MAX_RETAINED_CHILDREN = 10;
|
|
6
7
|
const MAX_TASK_SUMMARY_LENGTH = 120;
|
|
7
8
|
|
|
9
|
+
type RetainedChildState = "complete" | "failed" | "paused" | "stopped";
|
|
10
|
+
type RetainedChildResumability =
|
|
11
|
+
| { state: "resumable"; sessionPath: string }
|
|
12
|
+
| { state: "not-resumable"; reason: string };
|
|
13
|
+
|
|
8
14
|
export interface RetainedChild {
|
|
9
15
|
runId: string;
|
|
10
16
|
parentRunId?: string;
|
|
11
17
|
workflowKey?: string;
|
|
12
|
-
state:
|
|
18
|
+
state: RetainedChildState;
|
|
13
19
|
agent: string;
|
|
14
20
|
taskSummary: string;
|
|
15
21
|
completedAt: number;
|
|
16
|
-
|
|
22
|
+
resumability: RetainedChildResumability;
|
|
23
|
+
sessionPath?: string;
|
|
17
24
|
tokenTotals?: TokenUsage;
|
|
18
25
|
}
|
|
19
26
|
|
|
20
|
-
function
|
|
21
|
-
|
|
27
|
+
function isRetainedChildState(state: AsyncRunSummary["state"]): state is RetainedChildState {
|
|
28
|
+
return state === "complete" || state === "failed" || state === "paused" || state === "stopped";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isTerminalStepStatus(status: AsyncRunSummary["steps"][number]["status"]): boolean {
|
|
32
|
+
return status === "complete" || status === "completed" || status === "failed" || status === "paused" || status === "stopped";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function retainedSessionFile(sessionFile: string | undefined): RetainedChildResumability {
|
|
36
|
+
if (!sessionFile) return { state: "not-resumable", reason: "no persisted session file" };
|
|
37
|
+
if (!sessionFile.endsWith(".jsonl")) return { state: "not-resumable", reason: "persisted session file is not a .jsonl file" };
|
|
22
38
|
try {
|
|
23
39
|
const stat = fs.lstatSync(sessionFile);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
40
|
+
if (!stat.isFile() || stat.isSymbolicLink()) return { state: "not-resumable", reason: "persisted session file is not a regular file" };
|
|
41
|
+
return { state: "resumable", sessionPath: sessionFile };
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (typeof error === "object" && error !== null && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
44
|
+
return { state: "not-resumable", reason: `persisted session file is missing: ${sessionFile}` };
|
|
45
|
+
}
|
|
46
|
+
return { state: "not-resumable", reason: `persisted session file could not be inspected: ${error instanceof Error ? error.message : String(error)}` };
|
|
27
47
|
}
|
|
28
48
|
}
|
|
29
49
|
|
|
50
|
+
function childResumability(run: AsyncRunSummary, step: AsyncRunSummary["steps"][number]): RetainedChildResumability {
|
|
51
|
+
if (run.state === "stopped" || step.status === "stopped") return { state: "not-resumable", reason: "stopped run" };
|
|
52
|
+
if (step.runner?.type === "external-cli") return { state: "not-resumable", reason: "external CLI runner" };
|
|
53
|
+
const session = retainedSessionFile(step.sessionFile ?? run.sessionFile);
|
|
54
|
+
if (session.state === "not-resumable") return session;
|
|
55
|
+
try {
|
|
56
|
+
const recoveryDescriptor = readAsyncRecoveryDescriptor(run.asyncDir);
|
|
57
|
+
if (!recoveryDescriptor) return { state: "not-resumable", reason: "missing recovery descriptor" };
|
|
58
|
+
if (recoveryDescriptor.sourceRunId !== run.id) return { state: "not-resumable", reason: `recovery descriptor belongs to run ${recoveryDescriptor.sourceRunId}` };
|
|
59
|
+
if (recoveryDescriptor.agent !== step.agent) return { state: "not-resumable", reason: `recovery descriptor belongs to agent ${recoveryDescriptor.agent}` };
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return { state: "not-resumable", reason: `invalid recovery descriptor: ${error instanceof Error ? error.message : String(error)}` };
|
|
62
|
+
}
|
|
63
|
+
return session;
|
|
64
|
+
}
|
|
65
|
+
|
|
30
66
|
function boundedTaskSummary(value: string | undefined): string {
|
|
31
67
|
const normalized = value?.replace(/\s+/g, " ").trim() ?? "";
|
|
32
68
|
return normalized.length > MAX_TASK_SUMMARY_LENGTH
|
|
@@ -35,15 +71,15 @@ function boundedTaskSummary(value: string | undefined): string {
|
|
|
35
71
|
}
|
|
36
72
|
|
|
37
73
|
export function listRetainedChildren(asyncDirRoot: string, sessionId: string): RetainedChild[] {
|
|
38
|
-
|
|
74
|
+
const children = listAsyncRuns(asyncDirRoot, { sessionId, states: ["complete", "failed", "paused", "stopped"], reconcile: false })
|
|
39
75
|
.flatMap((run) => {
|
|
40
76
|
if (!run.parentWorkflowRunId || run.steps.length !== 1) return [];
|
|
41
|
-
if (run.state
|
|
77
|
+
if (!isRetainedChildState(run.state)) return [];
|
|
42
78
|
const step = run.steps[0]!;
|
|
43
|
-
|
|
44
|
-
if (!terminalStep || !retainedSessionFile(step.sessionFile ?? run.sessionFile)) return [];
|
|
79
|
+
if (!isTerminalStepStatus(step.status)) return [];
|
|
45
80
|
const completedAt = run.endedAt ?? run.lastUpdate;
|
|
46
81
|
if (completedAt === undefined) return [];
|
|
82
|
+
const resumability = childResumability(run, step);
|
|
47
83
|
return [{
|
|
48
84
|
runId: run.id,
|
|
49
85
|
...(run.parentWorkflowRunId ? { parentRunId: run.parentWorkflowRunId } : {}),
|
|
@@ -52,25 +88,38 @@ export function listRetainedChildren(asyncDirRoot: string, sessionId: string): R
|
|
|
52
88
|
agent: step.agent,
|
|
53
89
|
taskSummary: boundedTaskSummary(step.description),
|
|
54
90
|
completedAt,
|
|
55
|
-
|
|
91
|
+
resumability,
|
|
92
|
+
...(resumability.state === "resumable" ? { sessionPath: resumability.sessionPath } : {}),
|
|
56
93
|
...(step.tokens ?? run.totalTokens ? { tokenTotals: step.tokens ?? run.totalTokens } : {}),
|
|
57
94
|
}];
|
|
58
95
|
})
|
|
59
|
-
.sort((left, right) => right.completedAt - left.completedAt)
|
|
60
|
-
|
|
96
|
+
.sort((left, right) => right.completedAt - left.completedAt);
|
|
97
|
+
return children;
|
|
61
98
|
}
|
|
62
99
|
|
|
63
100
|
export function formatRetainedChildren(children: RetainedChild[]): string {
|
|
64
|
-
if (children.length === 0) return "No
|
|
101
|
+
if (children.length === 0) return "No retained workflow children in the active parent session. If a retained-writer challenge is required, launch a same-role fallback challenge and label it as fallback.";
|
|
102
|
+
const retained = children.slice(0, MAX_RETAINED_CHILDREN);
|
|
103
|
+
if (!retained.some((child) => child.resumability.state === "resumable")) {
|
|
104
|
+
const resumable = children.slice(MAX_RETAINED_CHILDREN).find((child) => child.resumability.state === "resumable");
|
|
105
|
+
if (resumable && retained.length === MAX_RETAINED_CHILDREN) retained[MAX_RETAINED_CHILDREN - 1] = resumable;
|
|
106
|
+
}
|
|
107
|
+
const hasResumableChild = retained.some((child) => child.resumability.state === "resumable");
|
|
65
108
|
return [
|
|
66
|
-
`
|
|
67
|
-
...
|
|
109
|
+
`Retained workflow children (up to ${MAX_RETAINED_CHILDREN}; newest first, with a resumable child retained when available):`,
|
|
110
|
+
...retained.flatMap((child) => [
|
|
68
111
|
`- ${child.runId} | ${child.agent} | ${child.state} | ${new Date(child.completedAt).toISOString()}`,
|
|
69
112
|
...(child.parentRunId ? [` workflow: ${child.parentRunId}${child.workflowKey ? ` (${child.workflowKey})` : ""}`] : []),
|
|
70
113
|
` task: ${child.taskSummary || "(no task summary)"}`,
|
|
71
|
-
|
|
72
|
-
|
|
114
|
+
child.resumability.state === "resumable"
|
|
115
|
+
? " resumability: resumable"
|
|
116
|
+
: ` resumability: not resumable (${child.resumability.reason})`,
|
|
117
|
+
...(child.resumability.state === "resumable" ? [
|
|
118
|
+
` session: ${child.resumability.sessionPath}`,
|
|
119
|
+
` resume: subagent({ action: "resume", id: "${child.runId}", message: "..." })`,
|
|
120
|
+
] : []),
|
|
73
121
|
...(child.tokenTotals ? [` tokens: input ${child.tokenTotals.input}, output ${child.tokenTotals.output}, total ${child.tokenTotals.total}`] : []),
|
|
74
122
|
]),
|
|
123
|
+
...(hasResumableChild ? [] : ["No resumable retained child is listed. Launch a same-role fallback challenge and label it as fallback."]),
|
|
75
124
|
].join("\n");
|
|
76
125
|
}
|
|
@@ -3,6 +3,8 @@ import * as path from "node:path";
|
|
|
3
3
|
import { DIRS, type SubagentState } from "../../shared/types.ts";
|
|
4
4
|
import { readStatus } from "../../shared/utils.ts";
|
|
5
5
|
import { findAsyncRunPrefixMatches, type AsyncRunLocation } from "./async-resume.ts";
|
|
6
|
+
import { resultFilePath, resultFilesForToolCall } from "./result-files.ts";
|
|
7
|
+
import { readActiveRunToolCallIndex } from "./active-run-index.ts";
|
|
6
8
|
import { assertSafeNestedId, findNestedRunMatchesById, type NestedRoute, type NestedRunMatch, type NestedRunResolutionScope } from "../shared/nested-events.ts";
|
|
7
9
|
|
|
8
10
|
export type ResolvedSubagentRunId =
|
|
@@ -19,7 +21,7 @@ export interface ResolveSubagentRunIdDeps {
|
|
|
19
21
|
|
|
20
22
|
function exactAsyncLocation(id: string, asyncDirRoot: string, resultsDir: string): AsyncRunLocation | undefined {
|
|
21
23
|
const asyncDir = path.join(asyncDirRoot, id);
|
|
22
|
-
const resultPath =
|
|
24
|
+
const resultPath = resultFilePath(resultsDir, id);
|
|
23
25
|
if (!fs.existsSync(asyncDir) && !fs.existsSync(resultPath)) return undefined;
|
|
24
26
|
return {
|
|
25
27
|
asyncDir: fs.existsSync(asyncDir) ? asyncDir : null,
|
|
@@ -52,39 +54,28 @@ function readWorkflowResultIdentity(resultPath: string): WorkflowResultIdentity
|
|
|
52
54
|
};
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
function directoryEntries(root: string): string[] {
|
|
56
|
-
try {
|
|
57
|
-
return fs.readdirSync(root);
|
|
58
|
-
} catch (error) {
|
|
59
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
60
|
-
throw error;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
57
|
function resultPathFor(resultsDir: string, runId: string): string | null {
|
|
65
|
-
const resultPath =
|
|
58
|
+
const resultPath = resultFilePath(resultsDir, runId);
|
|
66
59
|
return fs.existsSync(resultPath) ? resultPath : null;
|
|
67
60
|
}
|
|
68
61
|
|
|
69
|
-
function toolCallIdMatches(value: string | undefined, query: string
|
|
70
|
-
|
|
71
|
-
return options.prefix === true ? value.startsWith(query) : value === query;
|
|
62
|
+
function toolCallIdMatches(value: string | undefined, query: string): boolean {
|
|
63
|
+
return value === query;
|
|
72
64
|
}
|
|
73
65
|
|
|
74
|
-
function
|
|
66
|
+
function indexedToolCallIdAsyncLocations(toolCallId: string, asyncDirRoot: string, resultsDir: string): AsyncRunMatch[] {
|
|
75
67
|
const byId = new Map<string, AsyncRunLocation>();
|
|
76
|
-
for (const entry of
|
|
68
|
+
for (const entry of readActiveRunToolCallIndex(asyncDirRoot, toolCallId)) {
|
|
77
69
|
const asyncDir = path.join(asyncDirRoot, entry);
|
|
78
70
|
const status = readStatus(asyncDir);
|
|
79
|
-
if (!status || !toolCallIdMatches(status.toolCallId, toolCallId
|
|
71
|
+
if (!status || !toolCallIdMatches(status.toolCallId, toolCallId)) continue;
|
|
80
72
|
const runId = status.runId || entry;
|
|
81
73
|
byId.set(runId, { asyncDir, resultPath: resultPathFor(resultsDir, runId), resolvedId: runId });
|
|
82
74
|
}
|
|
83
|
-
for (const entry of
|
|
84
|
-
if (!entry.endsWith(".json")) continue;
|
|
75
|
+
for (const entry of resultFilesForToolCall(resultsDir, toolCallId)) {
|
|
85
76
|
const resultPath = path.join(resultsDir, entry);
|
|
86
77
|
const identity = readWorkflowResultIdentity(resultPath);
|
|
87
|
-
if (!identity || !toolCallIdMatches(identity.toolCallId, toolCallId
|
|
78
|
+
if (!identity || !toolCallIdMatches(identity.toolCallId, toolCallId)) continue;
|
|
88
79
|
const runId = identity.runId ?? identity.id ?? entry.slice(0, -".json".length);
|
|
89
80
|
const asyncDir = path.join(asyncDirRoot, runId);
|
|
90
81
|
byId.set(runId, { asyncDir: fs.existsSync(asyncDir) ? asyncDir : null, resultPath, resolvedId: runId });
|
|
@@ -109,6 +100,22 @@ function hasExactForegroundId(state: SubagentState | undefined, id: string): boo
|
|
|
109
100
|
return Boolean(remembered && state.currentSessionId && remembered.sessionId === state.currentSessionId);
|
|
110
101
|
}
|
|
111
102
|
|
|
103
|
+
function exactLiveAsyncToolCallMatch(state: SubagentState | undefined, toolCallId: string, asyncDirRoot: string, resultsDir: string): AsyncRunMatch | undefined {
|
|
104
|
+
if (!state?.asyncJobs) return undefined;
|
|
105
|
+
const matches = [...state.asyncJobs.values()].filter((job) => job.toolCallId === toolCallId);
|
|
106
|
+
if (matches.length > 1) throw new Error(`Subagent tool-call id '${toolCallId}' is ambiguous across async runs. Use the returned asyncId instead.`);
|
|
107
|
+
const match = matches[0];
|
|
108
|
+
if (!match) return undefined;
|
|
109
|
+
return {
|
|
110
|
+
id: match.asyncId,
|
|
111
|
+
location: {
|
|
112
|
+
asyncDir: fs.existsSync(match.asyncDir) ? match.asyncDir : path.join(asyncDirRoot, match.asyncId),
|
|
113
|
+
resultPath: resultPathFor(resultsDir, match.asyncId),
|
|
114
|
+
resolvedId: match.asyncId,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
112
119
|
function nestedScopeFromState(state: SubagentState | undefined): NestedRunResolutionScope | undefined {
|
|
113
120
|
if (!state) return undefined;
|
|
114
121
|
const routes: NestedRoute[] = [];
|
|
@@ -138,7 +145,9 @@ export function resolveSubagentRunId(id: string, deps: ResolveSubagentRunIdDeps
|
|
|
138
145
|
if (hasExactForegroundId(deps.state, id)) return { kind: "foreground", id };
|
|
139
146
|
const exactAsync = exactAsyncLocation(id, asyncDirRoot, resultsDir);
|
|
140
147
|
if (exactAsync) return { kind: "async", id, location: exactAsync };
|
|
141
|
-
const
|
|
148
|
+
const exactLiveToolCallMatch = exactLiveAsyncToolCallMatch(deps.state, id, asyncDirRoot, resultsDir);
|
|
149
|
+
if (exactLiveToolCallMatch) return { kind: "async", id: exactLiveToolCallMatch.id, location: exactLiveToolCallMatch.location };
|
|
150
|
+
const exactToolCallIdMatches = indexedToolCallIdAsyncLocations(id, asyncDirRoot, resultsDir);
|
|
142
151
|
if (exactToolCallIdMatches.length > 1) throw new Error(`Subagent tool-call id '${id}' is ambiguous across async runs. Use the returned asyncId instead.`);
|
|
143
152
|
if (exactToolCallIdMatches[0]) return { kind: "async", id: exactToolCallIdMatches[0].id, location: exactToolCallIdMatches[0].location };
|
|
144
153
|
const exactNested = findNestedRunMatchesById(id, nestedScope ? { scope: nestedScope } : {});
|
|
@@ -152,9 +161,6 @@ export function resolveSubagentRunId(id: string, deps: ResolveSubagentRunIdDeps
|
|
|
152
161
|
for (const match of asyncPrefixMatches(id, asyncDirRoot, resultsDir)) {
|
|
153
162
|
matches.push({ kind: "async", id: match.id, location: match.location });
|
|
154
163
|
}
|
|
155
|
-
for (const match of toolCallIdAsyncLocations(id, asyncDirRoot, resultsDir, { prefix: true })) {
|
|
156
|
-
matches.push({ kind: "async", id: match.id, location: match.location });
|
|
157
|
-
}
|
|
158
164
|
for (const match of findNestedRunMatchesById(id, nestedScope ? { prefix: true, scope: nestedScope } : { prefix: true })) {
|
|
159
165
|
matches.push({ kind: "nested", id: match.run.id, match });
|
|
160
166
|
}
|
|
@@ -113,15 +113,20 @@ function formatCheckpointGuidance(runId: string | undefined, checkpoint: AsyncSt
|
|
|
113
113
|
return `Checkpoint: ${checkpoint.name}${checkpoint.message ? ` — ${checkpoint.message}` : ""}\nApprove: subagent({ action: "approve-checkpoint", id: "${runId}" })\nReject: subagent({ action: "reject-checkpoint", id: "${runId}" })`;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
function formatResumeGuidance(runId: string | undefined, children: Array<{ agent?: unknown; sessionFile?: unknown; runId?: unknown; workflowKey?: unknown }>, fallbackSessionFile?: unknown, options: { stopped?: boolean } = {}): string {
|
|
116
|
+
function formatResumeGuidance(runId: string | undefined, children: Array<{ agent?: unknown; sessionFile?: unknown; runId?: unknown; workflowKey?: unknown; status?: unknown; activityState?: unknown }>, fallbackSessionFile?: unknown, options: { stopped?: boolean } = {}): string {
|
|
117
117
|
if (options.stopped) return "Resume: unavailable; stopped runs are not resumable. Start a new run instead.";
|
|
118
118
|
const knownChildren = children
|
|
119
119
|
.map((child, index) => ({ child, index }))
|
|
120
120
|
.filter(({ child }) => typeof child.agent === "string");
|
|
121
121
|
if (!runId || knownChildren.length === 0) return "Resume: unavailable; no child session file was persisted.";
|
|
122
122
|
const workflowChildren = knownChildren.filter(({ child }) => typeof child.runId === "string" && child.runId.trim() && hasExistingSessionFile(child.sessionFile));
|
|
123
|
+
const supervisorDetachedWorkflowChildren = workflowChildren.filter(({ child }) => child.status === "paused" && child.activityState === "needs_attention");
|
|
124
|
+
const resumableWorkflowChildren = workflowChildren.filter(({ child }) => !(child.status === "paused" && child.activityState === "needs_attention"));
|
|
123
125
|
if (workflowChildren.length > 0) {
|
|
124
|
-
return
|
|
126
|
+
return [
|
|
127
|
+
...supervisorDetachedWorkflowChildren.map(({ child }) => `Recovery workflow child${typeof child.workflowKey === "string" && child.workflowKey.trim() ? ` '${child.workflowKey}'` : ""}: reply to the supervisor request first, then wait with subagent_wait({ id: "${child.runId}" }). Use subagent({ action: "status", id: "${child.runId}" }) to recover the result; do not resume or launch a replacement while it remains detached.`),
|
|
128
|
+
...resumableWorkflowChildren.map(({ child }) => `Revive workflow child${typeof child.workflowKey === "string" && child.workflowKey.trim() ? ` '${child.workflowKey}'` : ""}: subagent({ action: "resume", id: "${child.runId}", message: "..." })`),
|
|
129
|
+
].join("\n");
|
|
125
130
|
}
|
|
126
131
|
const singleSessionFile = knownChildren[0]?.child.sessionFile ?? fallbackSessionFile;
|
|
127
132
|
if (children.length === 1 && knownChildren.length === 1 && hasExistingSessionFile(singleSessionFile)) {
|