pi-subagents 0.56.0 → 0.57.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 +53 -0
- package/agents/claude-code-writer.md +15 -0
- package/agents/claude-code.md +15 -0
- package/agents/codex-exec-writer.md +15 -0
- package/agents/codex-exec.md +15 -0
- package/agents/cursor-agent-writer.md +14 -0
- package/agents/cursor-agent.md +14 -0
- package/docs/agents.md +111 -15
- package/docs/configuration.md +37 -0
- package/docs/extension-api.md +2 -2
- package/docs/observability.md +8 -7
- package/docs/tool-reference.md +11 -1
- package/docs/workflows.md +20 -0
- package/package.json +1 -1
- package/skills/pi-subagents/references/execution-controls.md +1 -1
- package/src/agents/agent-management.ts +13 -5
- package/src/agents/agent-refinements.ts +4 -4
- package/src/agents/agents.ts +219 -48
- package/src/agents/builtin-names.ts +6 -0
- package/src/agents/runtime-agent-registry.ts +8 -2
- package/src/api/preflight.ts +2 -2
- package/src/extension/config.ts +70 -0
- package/src/extension/doctor.ts +3 -3
- package/src/extension/index.ts +18 -4
- package/src/extension/public-execution.ts +29 -13
- package/src/extension/rpc.ts +20 -5
- package/src/extension/schemas.ts +4 -3
- package/src/extension/tool-description.ts +11 -9
- package/src/inspectors/herdr/actions.ts +2 -1
- package/src/inspectors/herdr/inspector-runner.ts +2 -10
- package/src/inspectors/herdr/session-roots-codec.ts +42 -0
- package/src/integrations/herdr-status.ts +46 -3
- package/src/runs/background/active-async-capacity.ts +77 -10
- package/src/runs/background/async-execution.ts +100 -9
- package/src/runs/background/async-job-tracker.ts +5 -0
- package/src/runs/background/async-retention.ts +20 -3
- package/src/runs/background/async-status.ts +5 -0
- package/src/runs/background/chain-root-attachment.ts +15 -1
- package/src/runs/background/fleet-view.ts +16 -10
- package/src/runs/background/inspect-rpc.ts +8 -8
- package/src/runs/background/result-files.ts +27 -14
- package/src/runs/background/result-watcher.ts +9 -5
- package/src/runs/background/run-status.ts +33 -6
- package/src/runs/background/scheduled-runs.ts +7 -1
- package/src/runs/background/subagent-runner.ts +177 -44
- package/src/runs/background/wait-completions.ts +4 -0
- package/src/runs/foreground/execution.ts +72 -10
- package/src/runs/foreground/foreground-control.ts +6 -0
- package/src/runs/foreground/foreground-history.ts +22 -1
- package/src/runs/foreground/subagent-executor.ts +270 -81
- package/src/runs/foreground/workflow-detach-reconcile.ts +16 -1
- package/src/runs/shared/child-protocol.ts +21 -7
- package/src/runs/shared/claude-code-adapter.ts +129 -0
- package/src/runs/shared/codex-exec-adapter.ts +129 -0
- package/src/runs/shared/cursor-agent-adapter.ts +114 -0
- package/src/runs/shared/external-cli-contract.ts +167 -0
- package/src/runs/shared/external-cli-preflight.ts +122 -0
- package/src/runs/shared/external-cli-runner.ts +348 -55
- package/src/runs/shared/model-exclusions.ts +57 -7
- package/src/runs/shared/model-fallback.ts +23 -1
- package/src/runs/shared/mutation-evidence.ts +7 -2
- package/src/runs/shared/nested-events.ts +3 -1
- package/src/runs/shared/nested-render.ts +2 -2
- package/src/runs/shared/process-signal.ts +13 -0
- package/src/runs/shared/run-history.ts +21 -1
- package/src/shared/fork-context.ts +21 -0
- package/src/shared/formatters.ts +7 -1
- package/src/shared/pruned-fork.ts +450 -0
- package/src/shared/session-file-trust.ts +19 -0
- package/src/shared/session-tokens.ts +14 -3
- package/src/shared/settings.ts +10 -2
- package/src/shared/shortcuts.ts +17 -0
- package/src/shared/types.ts +118 -9
- package/src/shared/workflow-child-permit.ts +116 -0
- package/src/slash/delegation-adapters.ts +0 -1
- package/src/slash/slash-commands.ts +8 -6
- package/src/tui/fleet-status.ts +27 -10
- package/src/tui/fleet-transcript.ts +11 -5
- package/src/tui/fleet.ts +28 -13
- package/src/tui/render.ts +47 -21
- package/src/workflows/scripted-workflow.ts +248 -27
- package/src/workflows/workflow-child-summary.ts +117 -0
- package/src/workflows/workflow-receipt.ts +119 -4
|
@@ -218,19 +218,19 @@ function readOutputArtifact(outputPath: string): { output?: string; errorText?:
|
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
function readSessionBackedOutput(sessionPath: string, trustedRoots: string[]): { output?: string } {
|
|
222
|
-
if (trustedRoots.length === 0) return {};
|
|
221
|
+
function readSessionBackedOutput(sessionPath: string, trustedRoots: string[], trustedSessionFileRoot?: string): { output?: string } {
|
|
222
|
+
if (trustedRoots.length === 0 && !trustedSessionFileRoot) return {};
|
|
223
223
|
// The archive retains the child's session file as its output record. The
|
|
224
224
|
// final answer is the terminal assistant message's text — all text parts
|
|
225
225
|
// of that one session record, not just the last part.
|
|
226
|
-
const tail = readSessionMessagesTail(sessionPath, MAX_MESSAGE_LINES, trustedRoots);
|
|
226
|
+
const tail = readSessionMessagesTail(sessionPath, MAX_MESSAGE_LINES, trustedRoots, [sessionPath], trustedSessionFileRoot);
|
|
227
227
|
const last = tail.messages.findLast((message) => message.role === "assistant" && message.kind === "text");
|
|
228
228
|
if (!last) return {};
|
|
229
229
|
const parts = tail.messages.filter((message) => message.recordIndex === last.recordIndex && message.kind === "text");
|
|
230
230
|
return { output: parts.map((part) => part.text).join("\n") };
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
-
function readResultOutput(resultsDir: string, sessionId: string, runId: string, stepIndex: number | undefined, trustedRoots: string[], stepAgent: string | undefined, now: () => number): { output?: string; errorText?: string } {
|
|
233
|
+
function readResultOutput(resultsDir: string, sessionId: string, runId: string, stepIndex: number | undefined, trustedRoots: string[], stepAgent: string | undefined, now: () => number, trustedSessionFileRoot?: string): { output?: string; errorText?: string } {
|
|
234
234
|
const resultPath = resultPayloadPathForSessionRun(resultsDir, sessionId, runId);
|
|
235
235
|
if (resultPath) return resultOutput(JSON.parse(fs.readFileSync(resultPath, "utf-8")) as unknown, stepIndex);
|
|
236
236
|
// Read the raw record first: readCompletionReplay best-effort deletes
|
|
@@ -284,7 +284,7 @@ function readResultOutput(resultsDir: string, sessionId: string, runId: string,
|
|
|
284
284
|
if (artifactPath) return readOutputArtifact(artifactPath);
|
|
285
285
|
const sessionEntry = archive?.entries.find((entry) => entry.source === "session" && matches(entry));
|
|
286
286
|
const sessionPath = sessionEntry?.source === "session" ? sessionEntry.path : undefined;
|
|
287
|
-
if (sessionPath) return readSessionBackedOutput(sessionPath, trustedRoots);
|
|
287
|
+
if (sessionPath) return readSessionBackedOutput(sessionPath, trustedRoots, trustedSessionFileRoot);
|
|
288
288
|
const output = archive?.entries.find((entry) => entry.source === "result-tail" && matches(entry))?.text;
|
|
289
289
|
return output ? { output } : {};
|
|
290
290
|
}
|
|
@@ -377,8 +377,8 @@ export function buildInspectReply(request: InspectRequest, deps: InspectDeps = {
|
|
|
377
377
|
|
|
378
378
|
let messages: InspectReplyMessage[] | undefined;
|
|
379
379
|
let task: string | undefined;
|
|
380
|
-
if (sessionFile && trustedRoots.length > 0) {
|
|
381
|
-
const tail = readSessionMessagesTail(sessionFile, lineLimit, trustedRoots);
|
|
380
|
+
if (sessionFile && (trustedRoots.length > 0 || deps.state?.trustedSessionFileRoot)) {
|
|
381
|
+
const tail = readSessionMessagesTail(sessionFile, lineLimit, trustedRoots, [sessionFile], deps.state?.trustedSessionFileRoot);
|
|
382
382
|
if (tail.messages.length > 0) {
|
|
383
383
|
messages = tail.messages.map(toReplyMessage);
|
|
384
384
|
// The delegated task is the child session's first user message, but
|
|
@@ -393,7 +393,7 @@ export function buildInspectReply(request: InspectRequest, deps: InspectDeps = {
|
|
|
393
393
|
}
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
-
const result = readResultOutput(resultsDir, currentSessionId, node.status.runId, node.stepIndex, trustedRoots, step?.agent, deps.now ?? Date.now);
|
|
396
|
+
const result = readResultOutput(resultsDir, currentSessionId, node.status.runId, node.stepIndex, trustedRoots, step?.agent, deps.now ?? Date.now, deps.state?.trustedSessionFileRoot);
|
|
397
397
|
const failedText = step?.error ?? node.status.error;
|
|
398
398
|
if (result.output === undefined && result.errorText === undefined && failedText !== undefined) {
|
|
399
399
|
return errorReply(request, "internal", "Inspection could not read the async run artifacts.");
|
|
@@ -40,6 +40,11 @@ function nonEmptyString(value: unknown): string | undefined {
|
|
|
40
40
|
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/** A legacy alias can be valid metadata while still being unaddressable on the current filesystem. */
|
|
44
|
+
function isUnaddressableResultCandidate(error: unknown): boolean {
|
|
45
|
+
return (error as NodeJS.ErrnoException)?.code === "ENAMETOOLONG";
|
|
46
|
+
}
|
|
47
|
+
|
|
43
48
|
export function resultFileName(runId: string): string {
|
|
44
49
|
return `${runId}${JSON_EXTENSION}`;
|
|
45
50
|
}
|
|
@@ -227,7 +232,7 @@ function existingResultFile(resultPath: string): boolean {
|
|
|
227
232
|
try {
|
|
228
233
|
return fs.statSync(resultPath).isFile();
|
|
229
234
|
} catch (error) {
|
|
230
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") console.error(`Failed to inspect async result payload '${resultPath}':`, error);
|
|
235
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT" && !isUnaddressableResultCandidate(error)) console.error(`Failed to inspect async result payload '${resultPath}':`, error);
|
|
231
236
|
return false;
|
|
232
237
|
}
|
|
233
238
|
}
|
|
@@ -241,7 +246,7 @@ function pendingResultPayloadMatches(filePath: string, sessionId: string, runId:
|
|
|
241
246
|
const data = JSON.parse(fs.readFileSync(filePath, "utf-8")) as Record<string, unknown>;
|
|
242
247
|
return (nonEmptyString(data.runId) ?? nonEmptyString(data.id)) === runId && nonEmptyString(data.sessionId) === sessionId;
|
|
243
248
|
} catch (error) {
|
|
244
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") console.error(`Ignoring invalid pending async result '${filePath}':`, error);
|
|
249
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT" && !isUnaddressableResultCandidate(error)) console.error(`Ignoring invalid pending async result '${filePath}':`, error);
|
|
245
250
|
return false;
|
|
246
251
|
}
|
|
247
252
|
}
|
|
@@ -263,11 +268,11 @@ export function promotePendingResultFile(resultsDir: string, sessionId: string,
|
|
|
263
268
|
return existingResultFile(resultPath) ? "promoted" : "pending";
|
|
264
269
|
} catch (error) {
|
|
265
270
|
const code = (error as NodeJS.ErrnoException).code;
|
|
266
|
-
if (code === "ENOENT" || code === "EEXIST" || code === "EPERM" || code === "EACCES") {
|
|
271
|
+
if (code === "ENOENT" || code === "EEXIST" || code === "EPERM" || code === "EACCES" || isUnaddressableResultCandidate(error)) {
|
|
267
272
|
const pendingExists = existingResultFile(pendingPath);
|
|
268
273
|
const resultExists = existingResultFile(resultPath);
|
|
269
274
|
if (pendingExists) {
|
|
270
|
-
if (!resultExists && options.logFailure !== false) console.error(`Failed to promote pending async result '${pendingPath}' to '${resultPath}':`, error);
|
|
275
|
+
if (!resultExists && options.logFailure !== false && !isUnaddressableResultCandidate(error)) console.error(`Failed to promote pending async result '${pendingPath}' to '${resultPath}':`, error);
|
|
271
276
|
return "pending";
|
|
272
277
|
}
|
|
273
278
|
if (resultExists) return "promoted";
|
|
@@ -318,7 +323,7 @@ function readResultIndexForSessionRun(resultsDir: string, sessionId: string, run
|
|
|
318
323
|
} catch (error) {
|
|
319
324
|
const code = (error as NodeJS.ErrnoException).code;
|
|
320
325
|
if (code === "EPERM" || code === "EACCES") throw error;
|
|
321
|
-
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
326
|
+
if (code !== "ENOENT" && code !== "ENOTDIR" && !isUnaddressableResultCandidate(error)) {
|
|
322
327
|
console.error(`Ignoring invalid async result index for '${runId}':`, error);
|
|
323
328
|
}
|
|
324
329
|
}
|
|
@@ -338,7 +343,7 @@ export function resultPayloadPathForMissionObserverRun(resultsDir: string, runId
|
|
|
338
343
|
if (!entry || entry.runId !== runId) return undefined;
|
|
339
344
|
return resultPayloadLocationFromIndex(resultsDir, entry)?.path;
|
|
340
345
|
} catch (error) {
|
|
341
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") console.error(`Ignoring invalid async result observer index for '${runId}':`, error);
|
|
346
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT" && !isUnaddressableResultCandidate(error)) console.error(`Ignoring invalid async result observer index for '${runId}':`, error);
|
|
342
347
|
return undefined;
|
|
343
348
|
}
|
|
344
349
|
}
|
|
@@ -356,9 +361,12 @@ export function resultPayloadPathForIndexedRun(resultsDir: string, runId: string
|
|
|
356
361
|
fs.rmSync(entryPath, { force: true });
|
|
357
362
|
} catch (error) {
|
|
358
363
|
const code = (error as NodeJS.ErrnoException).code;
|
|
364
|
+
if (isUnaddressableResultCandidate(error)) return undefined;
|
|
359
365
|
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
360
366
|
console.error(`Ignoring invalid async result run index '${entryPath}':`, error);
|
|
361
|
-
try { fs.rmSync(entryPath, { force: true }); } catch {
|
|
367
|
+
try { fs.rmSync(entryPath, { force: true }); } catch {
|
|
368
|
+
// Invalid optional run-index cleanup is best-effort.
|
|
369
|
+
}
|
|
362
370
|
}
|
|
363
371
|
}
|
|
364
372
|
return undefined;
|
|
@@ -378,7 +386,7 @@ function listIndexFiles(dir: string): string[] {
|
|
|
378
386
|
.map((entry) => path.join(dir, entry.name));
|
|
379
387
|
} catch (error) {
|
|
380
388
|
const code = (error as NodeJS.ErrnoException).code;
|
|
381
|
-
if (code === "ENOENT" || code === "ENOTDIR" || code === "EPERM" || code === "EACCES") return [];
|
|
389
|
+
if (code === "ENOENT" || code === "ENOTDIR" || code === "EPERM" || code === "EACCES" || isUnaddressableResultCandidate(error)) return [];
|
|
382
390
|
throw error;
|
|
383
391
|
}
|
|
384
392
|
return files;
|
|
@@ -396,7 +404,7 @@ function resultFilesFromIndexDir(resultsDir: string, dir: string, includePending
|
|
|
396
404
|
const resultFile = indexedResultFile(resultsDir, entry, includePending);
|
|
397
405
|
if (resultFile) candidates.add(resultFile);
|
|
398
406
|
} catch (error) {
|
|
399
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") console.error(`Ignoring invalid async result index '${entryPath}':`, error);
|
|
407
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT" && !isUnaddressableResultCandidate(error)) console.error(`Ignoring invalid async result index '${entryPath}':`, error);
|
|
400
408
|
}
|
|
401
409
|
}
|
|
402
410
|
return [...candidates];
|
|
@@ -418,7 +426,7 @@ function pendingResultFilesForSession(resultsDir: string, sessionId: string): st
|
|
|
418
426
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
419
427
|
} catch (error) {
|
|
420
428
|
const code = (error as NodeJS.ErrnoException).code;
|
|
421
|
-
if (code === "ENOENT" || code === "ENOTDIR" || code === "EPERM" || code === "EACCES") continue;
|
|
429
|
+
if (code === "ENOENT" || code === "ENOTDIR" || code === "EPERM" || code === "EACCES" || isUnaddressableResultCandidate(error)) continue;
|
|
422
430
|
throw error;
|
|
423
431
|
}
|
|
424
432
|
for (const entry of entries) {
|
|
@@ -429,7 +437,7 @@ function pendingResultFilesForSession(resultsDir: string, sessionId: string): st
|
|
|
429
437
|
const runId = nonEmptyString(data.runId) ?? nonEmptyString(data.id);
|
|
430
438
|
if (runId && nonEmptyString(data.sessionId) === sessionId) files.add(resultFileName(runId));
|
|
431
439
|
} catch (error) {
|
|
432
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") console.error(`Ignoring invalid pending async result '${pendingPath}':`, error);
|
|
440
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT" && !isUnaddressableResultCandidate(error)) console.error(`Ignoring invalid pending async result '${pendingPath}':`, error);
|
|
433
441
|
}
|
|
434
442
|
}
|
|
435
443
|
}
|
|
@@ -471,14 +479,16 @@ export function cleanupResultIndexes(resultsDir: string, now = Date.now(), maxAg
|
|
|
471
479
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
472
480
|
} catch (error) {
|
|
473
481
|
const code = (error as NodeJS.ErrnoException).code;
|
|
474
|
-
if (code === "ENOENT" || code === "ENOTDIR" || code === "EPERM" || code === "EACCES") return;
|
|
482
|
+
if (code === "ENOENT" || code === "ENOTDIR" || code === "EPERM" || code === "EACCES" || isUnaddressableResultCandidate(error)) return;
|
|
475
483
|
throw error;
|
|
476
484
|
}
|
|
477
485
|
for (const entry of entries) {
|
|
478
486
|
const fullPath = path.join(dir, entry.name);
|
|
479
487
|
if (entry.isDirectory()) {
|
|
480
488
|
visit(fullPath);
|
|
481
|
-
try { fs.rmdirSync(fullPath); } catch {
|
|
489
|
+
try { fs.rmdirSync(fullPath); } catch {
|
|
490
|
+
// Non-empty or concurrently removed index directories are left in place.
|
|
491
|
+
}
|
|
482
492
|
continue;
|
|
483
493
|
}
|
|
484
494
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
@@ -492,11 +502,14 @@ export function cleanupResultIndexes(resultsDir: string, now = Date.now(), maxAg
|
|
|
492
502
|
removed += 1;
|
|
493
503
|
}
|
|
494
504
|
} catch (error) {
|
|
505
|
+
if (isUnaddressableResultCandidate(error)) continue;
|
|
495
506
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") console.error(`Ignoring invalid async result index '${fullPath}':`, error);
|
|
496
507
|
try {
|
|
497
508
|
fs.rmSync(fullPath, { force: true });
|
|
498
509
|
removed += 1;
|
|
499
|
-
} catch {
|
|
510
|
+
} catch {
|
|
511
|
+
// Invalid optional index cleanup is best-effort.
|
|
512
|
+
}
|
|
500
513
|
}
|
|
501
514
|
}
|
|
502
515
|
};
|
|
@@ -148,6 +148,10 @@ function isNotFound(error: unknown): boolean {
|
|
|
148
148
|
return errorCode(error) === "ENOENT";
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
function isAbsentResultCandidate(error: unknown): boolean {
|
|
152
|
+
return isNotFound(error) || errorCode(error) === "ENAMETOOLONG";
|
|
153
|
+
}
|
|
154
|
+
|
|
151
155
|
function isAccessDenied(error: unknown): boolean {
|
|
152
156
|
const code = errorCode(error);
|
|
153
157
|
return code === "EPERM" || code === "EACCES";
|
|
@@ -235,7 +239,7 @@ export function createResultWatcher(
|
|
|
235
239
|
try {
|
|
236
240
|
return fsApi.statSync(publicResultPath(file)).isFile();
|
|
237
241
|
} catch (error) {
|
|
238
|
-
if (!
|
|
242
|
+
if (!isAbsentResultCandidate(error)) console.error(`Failed to inspect subagent result file '${publicResultPath(file)}':`, error);
|
|
239
243
|
return false;
|
|
240
244
|
}
|
|
241
245
|
};
|
|
@@ -265,7 +269,7 @@ export function createResultWatcher(
|
|
|
265
269
|
} catch (error) {
|
|
266
270
|
identityCache.delete(file);
|
|
267
271
|
if (isAccessDenied(error)) throw error;
|
|
268
|
-
if (!
|
|
272
|
+
if (!isAbsentResultCandidate(error)) console.error(`Failed to inspect subagent result file '${resultPath}':`, error);
|
|
269
273
|
return undefined;
|
|
270
274
|
}
|
|
271
275
|
};
|
|
@@ -283,7 +287,7 @@ export function createResultWatcher(
|
|
|
283
287
|
} catch (error) {
|
|
284
288
|
identityCache.delete(file);
|
|
285
289
|
if (isAccessDenied(error)) throw error;
|
|
286
|
-
if (!
|
|
290
|
+
if (!isAbsentResultCandidate(error)) console.error(`Failed to inspect subagent result file '${resultPath}':`, error);
|
|
287
291
|
return undefined;
|
|
288
292
|
}
|
|
289
293
|
};
|
|
@@ -317,7 +321,7 @@ export function createResultWatcher(
|
|
|
317
321
|
removeResultIndex(resultsDir, sessionId, runId, toolCallId);
|
|
318
322
|
return true;
|
|
319
323
|
} catch (error) {
|
|
320
|
-
if (!
|
|
324
|
+
if (!isAbsentResultCandidate(error)) {
|
|
321
325
|
console.error(`Failed to remove delivered subagent result '${publicResultPath(file)}'; will retry:`, error);
|
|
322
326
|
return false;
|
|
323
327
|
}
|
|
@@ -384,7 +388,7 @@ export function createResultWatcher(
|
|
|
384
388
|
if (!resultPayloadWasReplaced(data, readPublicResultIdentity())) return false;
|
|
385
389
|
} catch (error) {
|
|
386
390
|
if (isAccessDenied(error)) throw error;
|
|
387
|
-
if (
|
|
391
|
+
if (isAbsentResultCandidate(error)) return false;
|
|
388
392
|
console.error(`Failed to re-read subagent result file '${publicResultPath(file)}':`, error);
|
|
389
393
|
}
|
|
390
394
|
identityCache.delete(file);
|
|
@@ -11,6 +11,7 @@ import { DIRS, type AsyncStatus, type Details, type ForegroundResumeRun, type Ne
|
|
|
11
11
|
import { inspectActiveAsyncCapacityOwner, type ActiveAsyncCapacityInspection } from "./active-async-capacity.ts";
|
|
12
12
|
import { readStatus } from "../../shared/utils.ts";
|
|
13
13
|
import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
|
|
14
|
+
import { normalizeExternalCliRunnerStatus } from "../shared/external-cli-contract.ts";
|
|
14
15
|
import { resolveSubagentResultStatus } from "../../intercom/result-intercom.ts";
|
|
15
16
|
import { readProcessTerminal, sanitizeProcessTerminal } from "./process-terminal.ts";
|
|
16
17
|
import { formatWaitSubscriptions } from "./wait-subscriptions.ts";
|
|
@@ -21,6 +22,7 @@ import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-
|
|
|
21
22
|
import { attachRootChildrenToSteps, findNestedRouteForRootId, projectNestedRegistryForRoot, type NestedRunResolutionScope } from "../shared/nested-events.ts";
|
|
22
23
|
import { readMissionBinding } from "../../missions/lifecycle.ts";
|
|
23
24
|
import { formatWorkflowJsonPreview } from "../../workflows/scripted-workflow.ts";
|
|
25
|
+
import { parseWorkflowChildSummary } from "../../workflows/workflow-child-summary.ts";
|
|
24
26
|
import { formatRunFanoutBudget, getRunFanoutBudgetSnapshot, readRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
|
|
25
27
|
import { getExternalJobProvider } from "../../api/external-job-provider.ts";
|
|
26
28
|
|
|
@@ -103,6 +105,7 @@ interface RunStatusDeps {
|
|
|
103
105
|
nested?: NestedRunResolutionScope;
|
|
104
106
|
sessionRoots?: string[];
|
|
105
107
|
activeCapacityRoot?: string;
|
|
108
|
+
abandonedSlotReleaseAfterMs?: number | false;
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
function hasExistingSessionFile(value: unknown): value is string {
|
|
@@ -405,7 +408,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
405
408
|
if (!status && diskStatus?.displayDismissedAt !== undefined) {
|
|
406
409
|
if (params.action === "debug.run") {
|
|
407
410
|
const { sidecar, overlay } = debugProcessTerminal(asyncDir, diskStatus);
|
|
408
|
-
const capacity = inspectActiveAsyncCapacityOwner({ runId: diskStatus.runId, sessionId: diskStatus.sessionId, asyncDir }, { rootDir: deps.activeCapacityRoot, liveWorkflowRunIds: new Set(deps.state?.workflowControllers?.keys() ?? []) });
|
|
411
|
+
const capacity = inspectActiveAsyncCapacityOwner({ runId: diskStatus.runId, sessionId: diskStatus.sessionId, asyncDir }, { rootDir: deps.activeCapacityRoot, liveWorkflowRunIds: new Set(deps.state?.workflowControllers?.keys() ?? []), abandonedSlotReleaseAfterMs: deps.abandonedSlotReleaseAfterMs });
|
|
409
412
|
return {
|
|
410
413
|
content: [{ type: "text", text: formatRunLifecycleDebug({ status: diskStatus, asyncDir, sidecarProcessTerminal: sidecar, overlayProcessTerminal: overlay, capacity }) }],
|
|
411
414
|
details: { mode: "single", results: [], ...((sidecar ?? overlay) ? { lifecycleStatus: { processTerminal: sidecar ?? overlay } } : {}) },
|
|
@@ -432,7 +435,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
432
435
|
if (status) {
|
|
433
436
|
if (params.action === "debug.run") {
|
|
434
437
|
const { sidecar, overlay } = debugProcessTerminal(asyncDir, status);
|
|
435
|
-
const capacity = inspectActiveAsyncCapacityOwner({ runId: status.runId, sessionId: status.sessionId, asyncDir }, { rootDir: deps.activeCapacityRoot, liveWorkflowRunIds: new Set(deps.state?.workflowControllers?.keys() ?? []) });
|
|
438
|
+
const capacity = inspectActiveAsyncCapacityOwner({ runId: status.runId, sessionId: status.sessionId, asyncDir }, { rootDir: deps.activeCapacityRoot, liveWorkflowRunIds: new Set(deps.state?.workflowControllers?.keys() ?? []), abandonedSlotReleaseAfterMs: deps.abandonedSlotReleaseAfterMs });
|
|
436
439
|
return {
|
|
437
440
|
content: [{ type: "text", text: formatRunLifecycleDebug({ status, asyncDir, sidecarProcessTerminal: sidecar, overlayProcessTerminal: overlay, capacity }) }],
|
|
438
441
|
details: { mode: "single", results: [], ...((sidecar ?? overlay) ? { lifecycleStatus: { processTerminal: sidecar ?? overlay } } : {}) },
|
|
@@ -536,9 +539,29 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
536
539
|
const phase = step.phase ? `[${step.phase}] ` : "";
|
|
537
540
|
lines.push(`${stepLineLabel(status, index)}: ${phase}${display} ${step.status}${modelText}${stepActivityText ? `, ${stepActivityText}` : ""}${steeringSuffix}${acceptanceText}${budgetText}${errorText}`);
|
|
538
541
|
if (step.runner?.type === "external-cli") {
|
|
539
|
-
|
|
542
|
+
const runner = normalizeExternalCliRunnerStatus(step.runner);
|
|
543
|
+
if (runner) {
|
|
544
|
+
lines.push(` Runner: external-cli (${runner.command}${runner.args.length ? ` ${runner.args.join(" ")}` : ""})`);
|
|
545
|
+
lines.push(` Adapter: ${runner.adapter.id} v${runner.adapter.version} (${runner.adapter.executionMode})`);
|
|
546
|
+
if (runner.safety) {
|
|
547
|
+
if ("approvalPolicy" in runner.safety) lines.push(` Safety: ${"access" in runner.safety ? `access=${runner.safety.access}, ` : ""}sandbox=${runner.safety.sandbox}, approval=${runner.safety.approvalPolicy}, ephemeral=${runner.safety.ephemeral}`);
|
|
548
|
+
else if ("mode" in runner.safety) lines.push(` Safety: access=${runner.safety.access}, auth=${runner.safety.authentication}, mode=${runner.safety.mode}, sandbox=${runner.safety.sandbox}, workspaceTrust=${runner.safety.workspaceTrust}, sessionReuse=${runner.safety.sessionReuse}`);
|
|
549
|
+
else if ("authentication" in runner.safety) lines.push(` Safety: access=${runner.safety.access}, auth=${runner.safety.authentication}, permission=${runner.safety.permissionMode}, tools=${runner.safety.tools}, mcp=${runner.safety.mcp}, settings=${runner.safety.settingSources}, settingsTrust=${runner.safety.userSettingsTrust}, persistence=${runner.safety.sessionPersistence}`);
|
|
550
|
+
else lines.push(` Safety: access=read-only, permission=${runner.safety.permissionMode}, tools=${runner.safety.tools}, mcp=${runner.safety.mcp}, settings=${runner.safety.settingSources}, persistence=${runner.safety.sessionPersistence}`);
|
|
551
|
+
}
|
|
552
|
+
lines.push(` Capabilities: stop=${runner.capabilities.stop}, steer=false, resume=false, structuredOutput=false, toolEvents=false, supervisor=unsupported, forkContext=false, extensionBindings=false`);
|
|
553
|
+
lines.push(` Unsupported steer: ${runner.unsupportedReasons.steer}`);
|
|
554
|
+
lines.push(` Unsupported resume: ${runner.nonResumableReason}`);
|
|
555
|
+
lines.push(` Unsupported supervisor: ${runner.unsupportedReasons.supervisor}`);
|
|
556
|
+
lines.push(` Context handoff: fresh only (${runner.unsupportedReasons.forkContext})`);
|
|
557
|
+
} else {
|
|
558
|
+
lines.push(" Runner: external-cli (invalid persisted runner metadata)");
|
|
559
|
+
}
|
|
540
560
|
if (step.externalProcess?.pid !== undefined) lines.push(` Process: ${step.externalProcess.pid}`);
|
|
541
|
-
if (step.externalProcess)
|
|
561
|
+
if (step.externalProcess) {
|
|
562
|
+
lines.push(` Stdout: ${step.externalProcess.stdoutPath}`, ` Stderr: ${step.externalProcess.stderrPath}`);
|
|
563
|
+
if (step.externalProcess.finalOutputPath) lines.push(` Final output: ${step.externalProcess.finalOutputPath}`);
|
|
564
|
+
}
|
|
542
565
|
} else if (step.runner?.type === "external-job") {
|
|
543
566
|
lines.push(` Runner: external-job (${step.runner.provider})`);
|
|
544
567
|
if (step.externalJob?.providerJobId) lines.push(` Provider job: ${step.externalJob.providerJobId}`);
|
|
@@ -583,7 +606,9 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
583
606
|
if (fs.existsSync(logPath)) lines.push(`Log: ${logPath}`);
|
|
584
607
|
if (fs.existsSync(eventsPath)) lines.push(`Events: ${eventsPath}`);
|
|
585
608
|
|
|
586
|
-
|
|
609
|
+
const workflowChildren = parseWorkflowChildSummary(status.workflowChildren);
|
|
610
|
+
if (workflowChildren && workflowChildren.workflowRunId !== status.runId) throw new Error("workflowChildren.workflowRunId does not match async status runId.");
|
|
611
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [], ...(workflowChildren ? { workflowChildren } : {}), ...(runFanoutBudget ? { runFanoutBudget } : {}), ...(processTerminal ? { lifecycleStatus: { processTerminal } } : {}) } };
|
|
587
612
|
}
|
|
588
613
|
}
|
|
589
614
|
|
|
@@ -627,7 +652,9 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
627
652
|
const children = Array.isArray(data.results) ? data.results : data.agent ? [{ agent: data.agent, sessionFile: data.sessionFile }] : [];
|
|
628
653
|
lines.push(formatResumeGuidance(runId, children, data.sessionFile, { stopped: status === "stopped" }));
|
|
629
654
|
if (data.summary) lines.push("", data.summary);
|
|
630
|
-
|
|
655
|
+
const workflowChildren = parseWorkflowChildSummary((data as unknown as Record<string, unknown>).workflowChildren);
|
|
656
|
+
if (workflowChildren && workflowChildren.workflowRunId !== runId) throw new Error("workflowChildren.workflowRunId does not match the result run id.");
|
|
657
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [], ...(workflowChildren ? { workflowChildren } : {}) } };
|
|
631
658
|
} catch (error) {
|
|
632
659
|
const message = error instanceof Error ? error.message : String(error);
|
|
633
660
|
return {
|
|
@@ -294,6 +294,10 @@ function resolveMaxPending(config: ExtensionConfig): number {
|
|
|
294
294
|
return typeof value === "number" && Number.isInteger(value) && value >= 1 ? value : DEFAULT_MAX_PENDING;
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
function hasPendingScheduleWork(schedule: ScheduleRecord): boolean {
|
|
298
|
+
return schedule.activeRunId !== undefined || schedule.trigger.nextRunAt !== undefined;
|
|
299
|
+
}
|
|
300
|
+
|
|
297
301
|
function nextAfter(trigger: ScheduleTrigger, plannedAt: number, now: number): string | undefined {
|
|
298
302
|
if (trigger.kind === "once") return undefined;
|
|
299
303
|
let next = plannedAt + trigger.everyMs;
|
|
@@ -479,7 +483,9 @@ export class ScheduledRunManager {
|
|
|
479
483
|
if (params.on !== undefined || params.timezone !== undefined || every === "day" || every === "week" || every === "month" || every === "year") return textResult("Calendar schedules are deferred from this first safe slice. Use a fixed interval such as every:'24h' or every:'7d'.", undefined, undefined, true);
|
|
480
484
|
const sessionId = ctx.sessionManager.getSessionId() ?? "unknown";
|
|
481
485
|
if (this.deps.resolveCapabilityCeiling?.(sessionId)) return textResult("Cannot persist a schedule while a capability ceiling is active.", undefined, undefined, true);
|
|
482
|
-
|
|
486
|
+
const pendingCount = store.list().filter(hasPendingScheduleWork).length;
|
|
487
|
+
const maxPending = resolveMaxPending(this.deps.config);
|
|
488
|
+
if (pendingCount >= maxPending) return textResult(`Schedule limit reached (${maxPending}).`, undefined, undefined, true);
|
|
483
489
|
const id = validateScheduleId((params.id?.trim() || this.randomId()));
|
|
484
490
|
if (store.ids().includes(id)) return textResult(`Schedule '${id}' already exists.`, undefined, undefined, true);
|
|
485
491
|
const now = this.now();
|