pi-subagents 0.24.4 → 0.25.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.
@@ -24,6 +24,7 @@ import {
24
24
  type ArtifactConfig,
25
25
  type Details,
26
26
  type MaxOutputConfig,
27
+ type NestedRouteInfo,
27
28
  type ResolvedControlConfig,
28
29
  type SubagentRunMode,
29
30
  ASYNC_DIR,
@@ -33,6 +34,7 @@ import {
33
34
  getAsyncConfigPath,
34
35
  resolveChildMaxSubagentDepth,
35
36
  } from "../../shared/types.ts";
37
+ import { nestedResultsPath, resolveInheritedNestedRouteFromEnv, resolveNestedParentAddressFromEnv, writeNestedEvent } from "../shared/nested-events.ts";
36
38
 
37
39
  const require = createRequire(import.meta.url);
38
40
  const piPackageRoot = resolvePiPackageRoot();
@@ -111,6 +113,7 @@ interface AsyncChainParams {
111
113
  controlConfig?: ResolvedControlConfig;
112
114
  controlIntercomTarget?: string;
113
115
  childIntercomTarget?: (agent: string, index: number) => string | undefined;
116
+ nestedRoute?: NestedRouteInfo;
114
117
  }
115
118
 
116
119
  interface AsyncSingleParams {
@@ -136,6 +139,7 @@ interface AsyncSingleParams {
136
139
  controlConfig?: ResolvedControlConfig;
137
140
  controlIntercomTarget?: string;
138
141
  childIntercomTarget?: (agent: string, index: number) => string | undefined;
142
+ nestedRoute?: NestedRouteInfo;
139
143
  }
140
144
 
141
145
  interface AsyncExecutionResult {
@@ -236,6 +240,7 @@ export function executeAsyncChain(
236
240
  controlConfig,
237
241
  controlIntercomTarget,
238
242
  childIntercomTarget,
243
+ nestedRoute,
239
244
  } = params;
240
245
  const resultMode = params.resultMode ?? "chain";
241
246
  const chainSkills = params.chainSkills ?? [];
@@ -261,7 +266,11 @@ export function executeAsyncChain(
261
266
  }
262
267
  }
263
268
 
264
- const asyncDir = path.join(ASYNC_DIR, id);
269
+ const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
270
+ const nestedAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
271
+ const asyncDir = inheritedNestedRoute
272
+ ? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
273
+ : path.join(ASYNC_DIR, id);
265
274
  try {
266
275
  fs.mkdirSync(asyncDir, { recursive: true });
267
276
  } catch (error) {
@@ -393,7 +402,7 @@ export function executeAsyncChain(
393
402
  {
394
403
  id,
395
404
  steps,
396
- resultPath: path.join(RESULTS_DIR, `${id}.json`),
405
+ resultPath: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : path.join(RESULTS_DIR, `${id}.json`),
397
406
  cwd: runnerCwd,
398
407
  placeholder: "{previous}",
399
408
  maxOutput,
@@ -411,6 +420,13 @@ export function executeAsyncChain(
411
420
  controlIntercomTarget,
412
421
  childIntercomTargets,
413
422
  resultMode,
423
+ nestedRoute: nestedRoute ?? inheritedNestedRoute,
424
+ nestedSelf: inheritedNestedRoute && nestedAddress ? {
425
+ parentRunId: nestedAddress.parentRunId,
426
+ parentStepIndex: nestedAddress.parentStepIndex,
427
+ depth: nestedAddress.depth,
428
+ path: nestedAddress.path,
429
+ } : undefined,
414
430
  },
415
431
  id,
416
432
  runnerCwd,
@@ -443,6 +459,40 @@ export function executeAsyncChain(
443
459
  flatStepStart++;
444
460
  }
445
461
  }
462
+ if (inheritedNestedRoute && nestedAddress) {
463
+ const now = Date.now();
464
+ try {
465
+ writeNestedEvent(inheritedNestedRoute, {
466
+ type: "subagent.nested.started",
467
+ ts: now,
468
+ parentRunId: nestedAddress.parentRunId,
469
+ parentStepIndex: nestedAddress.parentStepIndex,
470
+ child: {
471
+ id,
472
+ parentRunId: nestedAddress.parentRunId,
473
+ parentStepIndex: nestedAddress.parentStepIndex,
474
+ depth: nestedAddress.depth,
475
+ path: nestedAddress.path,
476
+ asyncDir,
477
+ pid: spawnResult.pid,
478
+ ownerIntercomTarget: process.env.PI_SUBAGENT_INTERCOM_SESSION_NAME,
479
+ leafIntercomTarget: childIntercomTargets?.[0],
480
+ intercomTarget: childIntercomTargets?.[0],
481
+ ownerState: "live",
482
+ mode: resultMode,
483
+ state: "running",
484
+ agent: firstAgents[0],
485
+ agents: flatAgents,
486
+ chainStepCount: chain.length,
487
+ parallelGroups,
488
+ startedAt: now,
489
+ lastUpdate: now,
490
+ },
491
+ });
492
+ } catch (error) {
493
+ console.error("Failed to emit nested async start event:", error);
494
+ }
495
+ }
446
496
  ctx.pi.events.emit(SUBAGENT_ASYNC_STARTED_EVENT, {
447
497
  id,
448
498
  pid: spawnResult.pid,
@@ -460,6 +510,7 @@ export function executeAsyncChain(
460
510
  parallelGroups,
461
511
  cwd: runnerCwd,
462
512
  asyncDir,
513
+ nestedRoute,
463
514
  });
464
515
  }
465
516
 
@@ -499,6 +550,7 @@ export function executeAsyncSingle(
499
550
  controlConfig,
500
551
  controlIntercomTarget,
501
552
  childIntercomTarget,
553
+ nestedRoute,
502
554
  } = params;
503
555
  const task = params.task ?? "";
504
556
  const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
@@ -512,7 +564,11 @@ export function executeAsyncSingle(
512
564
  systemPrompt = systemPrompt ? `${systemPrompt}\n\n${injection}` : injection;
513
565
  }
514
566
 
515
- const asyncDir = path.join(ASYNC_DIR, id);
567
+ const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
568
+ const nestedAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
569
+ const asyncDir = inheritedNestedRoute
570
+ ? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
571
+ : path.join(ASYNC_DIR, id);
516
572
  try {
517
573
  fs.mkdirSync(asyncDir, { recursive: true });
518
574
  } catch (error) {
@@ -564,7 +620,7 @@ export function executeAsyncSingle(
564
620
  maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, agentConfig.maxSubagentDepth),
565
621
  },
566
622
  ],
567
- resultPath: path.join(RESULTS_DIR, `${id}.json`),
623
+ resultPath: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : path.join(RESULTS_DIR, `${id}.json`),
568
624
  cwd: runnerCwd,
569
625
  placeholder: "{previous}",
570
626
  maxOutput,
@@ -582,6 +638,13 @@ export function executeAsyncSingle(
582
638
  controlIntercomTarget,
583
639
  childIntercomTargets: childIntercomTarget ? [childIntercomTarget(agent, 0)] : undefined,
584
640
  resultMode: "single",
641
+ nestedRoute: nestedRoute ?? inheritedNestedRoute,
642
+ nestedSelf: inheritedNestedRoute && nestedAddress ? {
643
+ parentRunId: nestedAddress.parentRunId,
644
+ parentStepIndex: nestedAddress.parentStepIndex,
645
+ depth: nestedAddress.depth,
646
+ path: nestedAddress.path,
647
+ } : undefined,
585
648
  },
586
649
  id,
587
650
  runnerCwd,
@@ -596,6 +659,39 @@ export function executeAsyncSingle(
596
659
  }
597
660
 
598
661
  if (spawnResult.pid) {
662
+ if (inheritedNestedRoute && nestedAddress) {
663
+ const now = Date.now();
664
+ try {
665
+ writeNestedEvent(inheritedNestedRoute, {
666
+ type: "subagent.nested.started",
667
+ ts: now,
668
+ parentRunId: nestedAddress.parentRunId,
669
+ parentStepIndex: nestedAddress.parentStepIndex,
670
+ child: {
671
+ id,
672
+ parentRunId: nestedAddress.parentRunId,
673
+ parentStepIndex: nestedAddress.parentStepIndex,
674
+ depth: nestedAddress.depth,
675
+ path: nestedAddress.path,
676
+ asyncDir,
677
+ pid: spawnResult.pid,
678
+ ownerIntercomTarget: process.env.PI_SUBAGENT_INTERCOM_SESSION_NAME,
679
+ leafIntercomTarget: childIntercomTarget?.(agent, 0),
680
+ intercomTarget: childIntercomTarget?.(agent, 0),
681
+ ownerState: "live",
682
+ mode: "single",
683
+ state: "running",
684
+ agent,
685
+ agents: [agent],
686
+ chainStepCount: 1,
687
+ startedAt: now,
688
+ lastUpdate: now,
689
+ },
690
+ });
691
+ } catch (error) {
692
+ console.error("Failed to emit nested async start event:", error);
693
+ }
694
+ }
599
695
  ctx.pi.events.emit(SUBAGENT_ASYNC_STARTED_EVENT, {
600
696
  id,
601
697
  pid: spawnResult.pid,
@@ -605,6 +701,7 @@ export function executeAsyncSingle(
605
701
  task: task?.slice(0, 50),
606
702
  cwd: runnerCwd,
607
703
  asyncDir,
704
+ nestedRoute,
608
705
  });
609
706
  }
610
707
 
@@ -15,7 +15,8 @@ import {
15
15
  } from "../../shared/types.ts";
16
16
  import { readStatus } from "../../shared/utils.ts";
17
17
  import { normalizeParallelGroups } from "./parallel-groups.ts";
18
- import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
18
+ import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
19
+ import { hasLiveNestedDescendants, updateAsyncJobNestedProjection } from "../shared/nested-events.ts";
19
20
 
20
21
  interface AsyncJobTrackerOptions {
21
22
  completionRetentionMs?: number;
@@ -38,9 +39,14 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
38
39
  renderWidget(ctx, jobs);
39
40
  ctx.ui.requestRender?.();
40
41
  };
41
- const scheduleCleanup = (asyncId: string) => {
42
+ const cancelCleanup = (asyncId: string) => {
42
43
  const existingTimer = state.cleanupTimers.get(asyncId);
43
- if (existingTimer) clearTimeout(existingTimer);
44
+ if (!existingTimer) return;
45
+ clearTimeout(existingTimer);
46
+ state.cleanupTimers.delete(asyncId);
47
+ };
48
+ const scheduleCleanup = (asyncId: string) => {
49
+ cancelCleanup(asyncId);
44
50
  const timer = setTimeout(() => {
45
51
  state.cleanupTimers.delete(asyncId);
46
52
  state.asyncJobs.delete(asyncId);
@@ -121,8 +127,27 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
121
127
  let widgetChanged = false;
122
128
  for (const job of state.asyncJobs.values()) {
123
129
  const widgetStateBefore = widgetRenderKey(job);
130
+ let nestedRefreshFailed = false;
131
+ const refreshNestedProjection = () => {
132
+ try {
133
+ updateAsyncJobNestedProjection(job);
134
+ } catch (error) {
135
+ nestedRefreshFailed = true;
136
+ console.error(`Failed to refresh nested async descendants for '${job.asyncDir}':`, error);
137
+ }
138
+ };
139
+ const reconcileNestedDescendants = () => {
140
+ try {
141
+ if (job.nestedRoute) reconcileNestedAsyncDescendants(job.nestedRoute, { resultsDir, kill: options.kill, now: options.now });
142
+ } catch (error) {
143
+ nestedRefreshFailed = true;
144
+ console.error(`Failed to refresh nested async descendants for '${job.asyncDir}':`, error);
145
+ }
146
+ refreshNestedProjection();
147
+ };
124
148
  try {
125
149
  emitNewControlEvents(job);
150
+ reconcileNestedDescendants();
126
151
  const reconciliation = reconcileAsyncRun(job.asyncDir, {
127
152
  resultsDir,
128
153
  kill: options.kill,
@@ -143,6 +168,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
143
168
  if (status) {
144
169
  const previousStatus = job.status;
145
170
  job.status = status.state;
171
+ if (job.status !== "complete" && job.status !== "failed" && job.status !== "paused") cancelCleanup(job.asyncId);
146
172
  job.sessionId = status.sessionId ?? job.sessionId;
147
173
  job.activityState = status.activityState;
148
174
  job.lastActivityAt = status.lastActivityAt ?? job.lastActivityAt;
@@ -169,6 +195,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
169
195
  job.activeParallelGroup = Boolean(activeGroup);
170
196
  job.agents = visibleSteps.map((step) => step.agent);
171
197
  job.steps = visibleSteps;
198
+ refreshNestedProjection();
172
199
  job.stepsTotal = visibleSteps.length;
173
200
  job.runningSteps = visibleSteps.filter((step) => step.status === "running").length;
174
201
  job.completedSteps = visibleSteps.filter((step) => step.status === "complete" || step.status === "completed").length;
@@ -178,7 +205,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
178
205
  job.outputFile = status.outputFile ?? job.outputFile;
179
206
  job.totalTokens = status.totalTokens ?? job.totalTokens;
180
207
  job.sessionFile = status.sessionFile ?? job.sessionFile;
181
- if ((job.status === "complete" || job.status === "failed" || job.status === "paused") && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
208
+ if ((job.status === "complete" || job.status === "failed" || job.status === "paused") && !nestedRefreshFailed && !hasLiveNestedDescendants(job.nestedChildren) && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
182
209
  scheduleCleanup(job.asyncId);
183
210
  }
184
211
  if (widgetRenderKey(job) !== widgetStateBefore) widgetChanged = true;
@@ -194,7 +221,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
194
221
  job.status = "failed";
195
222
  job.updatedAt = Date.now();
196
223
  }
197
- if (!state.cleanupTimers.has(job.asyncId)) {
224
+ if (!hasLiveNestedDescendants(job.nestedChildren) && !state.cleanupTimers.has(job.asyncId)) {
198
225
  scheduleCleanup(job.asyncId);
199
226
  }
200
227
  }
@@ -228,6 +255,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
228
255
  agents,
229
256
  chainStepCount: info.chainStepCount,
230
257
  parallelGroups: validParallelGroups,
258
+ nestedRoute: info.nestedRoute,
231
259
  stepsTotal: firstGroupCount ?? agents?.length,
232
260
  hasParallelGroups: validParallelGroups.length > 0,
233
261
  activeParallelGroup: Boolean(firstGroupCount && firstGroupCount > 0),
@@ -245,15 +273,22 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
245
273
  const asyncId = result.id;
246
274
  if (!asyncId) return;
247
275
  const job = state.asyncJobs.get(asyncId);
276
+ let nestedRefreshFailed = false;
248
277
  if (job) {
249
278
  job.status = result.success ? "complete" : "failed";
250
279
  job.updatedAt = Date.now();
251
280
  if (result.asyncDir) job.asyncDir = result.asyncDir;
281
+ try {
282
+ updateAsyncJobNestedProjection(job);
283
+ } catch (error) {
284
+ nestedRefreshFailed = true;
285
+ console.error(`Failed to refresh nested async descendants for '${job.asyncDir}':`, error);
286
+ }
252
287
  }
253
288
  if (state.lastUiContext) {
254
289
  rerenderWidget(state.lastUiContext);
255
290
  }
256
- scheduleCleanup(asyncId);
291
+ if (!nestedRefreshFailed && !hasLiveNestedDescendants(job?.nestedChildren)) scheduleCleanup(asyncId);
257
292
  };
258
293
 
259
294
  const resetJobs = (ctx?: ExtensionContext) => {
@@ -149,6 +149,29 @@ function exactResultPath(resultsDir: string, runId: string): string | null {
149
149
  return fs.existsSync(resultPath) ? resultPath : null;
150
150
  }
151
151
 
152
+ export function findAsyncRunPrefixMatches(prefix: string, asyncDirRoot: string, resultsDir: string): Array<{ id: string; location: AsyncRunLocation }> {
153
+ const requestedId = assertRunId(prefix, "id");
154
+ if (!requestedId) return [];
155
+ const asyncRoot = path.resolve(asyncDirRoot);
156
+ const resultRoot = path.resolve(resultsDir);
157
+ const matchingIds = [...new Set([
158
+ ...prefixedRunIds(asyncRoot, requestedId),
159
+ ...prefixedRunIds(resultRoot, requestedId, ".json"),
160
+ ])].sort();
161
+ return matchingIds.map((id) => {
162
+ const asyncDir = path.join(asyncRoot, id);
163
+ assertInsideRoot(asyncRoot, asyncDir, "Async run directory");
164
+ return {
165
+ id,
166
+ location: {
167
+ asyncDir: fs.existsSync(asyncDir) ? asyncDir : null,
168
+ resultPath: exactResultPath(resultRoot, id),
169
+ resolvedId: id,
170
+ },
171
+ };
172
+ });
173
+ }
174
+
152
175
  export function resolveAsyncRunLocation(params: AsyncResumeParams, asyncDirRoot: string, resultsDir: string): AsyncRunLocation {
153
176
  const asyncRoot = path.resolve(asyncDirRoot);
154
177
  const resultRoot = path.resolve(resultsDir);
@@ -175,22 +198,12 @@ export function resolveAsyncRunLocation(params: AsyncResumeParams, asyncDirRoot:
175
198
  };
176
199
  }
177
200
 
178
- const matchingIds = [...new Set([
179
- ...prefixedRunIds(asyncRoot, requestedId),
180
- ...prefixedRunIds(resultRoot, requestedId, ".json"),
181
- ])].sort();
182
- if (matchingIds.length === 0) return { asyncDir: null, resultPath: null, resolvedId: requestedId };
183
- if (matchingIds.length > 1) {
184
- throw new Error(`Ambiguous async run id prefix '${requestedId}' matched: ${matchingIds.join(", ")}. Provide a longer id.`);
201
+ const matching = findAsyncRunPrefixMatches(requestedId, asyncRoot, resultRoot);
202
+ if (matching.length === 0) return { asyncDir: null, resultPath: null, resolvedId: requestedId };
203
+ if (matching.length > 1) {
204
+ throw new Error(`Ambiguous async run id prefix '${requestedId}' matched: ${matching.map((match) => match.id).join(", ")}. Provide a longer id.`);
185
205
  }
186
- const resolvedId = matchingIds[0]!;
187
- const asyncDir = path.join(asyncRoot, resolvedId);
188
- assertInsideRoot(asyncRoot, asyncDir, "Async run directory");
189
- return {
190
- asyncDir: fs.existsSync(asyncDir) ? asyncDir : null,
191
- resultPath: exactResultPath(resultRoot, resolvedId),
192
- resolvedId,
193
- };
206
+ return matching[0]!.location;
194
207
  }
195
208
 
196
209
  function resultState(result: AsyncResultFile): AsyncStatus["state"] {
@@ -2,10 +2,12 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
4
4
  import { formatActivityLabel, formatParallelOutcome } from "../../shared/status-format.ts";
5
- import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type SubagentRunMode, type TokenUsage } from "../../shared/types.ts";
5
+ import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type NestedRunSummary, type SubagentRunMode, type TokenUsage } from "../../shared/types.ts";
6
6
  import { readStatus } from "../../shared/utils.ts";
7
+ import { attachRootChildrenToSteps, findNestedRouteForRootId, projectNestedRegistryForRoot } from "../shared/nested-events.ts";
8
+ import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
7
9
  import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-groups.ts";
8
- import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
10
+ import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
9
11
 
10
12
  interface AsyncRunStepSummary {
11
13
  index: number;
@@ -28,6 +30,7 @@ interface AsyncRunStepSummary {
28
30
  thinking?: string;
29
31
  attemptedModels?: string[];
30
32
  error?: string;
33
+ children?: NestedRunSummary[];
31
34
  }
32
35
 
33
36
  export interface AsyncRunSummary {
@@ -55,6 +58,8 @@ export interface AsyncRunSummary {
55
58
  outputFile?: string;
56
59
  totalTokens?: TokenUsage;
57
60
  sessionFile?: string;
61
+ nestedChildren?: NestedRunSummary[];
62
+ nestedWarnings?: string[];
58
63
  }
59
64
 
60
65
  interface AsyncRunListOptions {
@@ -112,7 +117,7 @@ function deriveAsyncActivityState(asyncDir: string, status: AsyncStatus): { acti
112
117
  };
113
118
  }
114
119
 
115
- function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string }): AsyncRunSummary {
120
+ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string }, nestedWarnings: string[] = []): AsyncRunSummary {
116
121
  if (status.sessionId !== undefined && typeof status.sessionId !== "string") {
117
122
  throw new Error(`Invalid async status '${path.join(asyncDir, "status.json")}': sessionId must be a string.`);
118
123
  }
@@ -120,6 +125,42 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
120
125
  const steps = status.steps ?? [];
121
126
  const chainStepCount = status.chainStepCount ?? steps.length;
122
127
  const parallelGroups = normalizeParallelGroups(status.parallelGroups, steps.length, chainStepCount);
128
+ let nestedChildren: NestedRunSummary[] = [];
129
+ if (nestedWarnings.length === 0) {
130
+ try {
131
+ nestedChildren = projectNestedRegistryForRoot(status.runId || path.basename(asyncDir))?.children ?? [];
132
+ } catch (error) {
133
+ nestedWarnings.push(`Nested status unavailable: ${getErrorMessage(error)}`);
134
+ }
135
+ }
136
+ const summarizedSteps = steps.map((step, index) => {
137
+ const stepActivityState = step.activityState;
138
+ const stepLastActivityAt = step.lastActivityAt;
139
+ return {
140
+ index,
141
+ agent: step.agent,
142
+ status: step.status,
143
+ ...(stepActivityState ? { activityState: stepActivityState } : {}),
144
+ ...(stepLastActivityAt ? { lastActivityAt: stepLastActivityAt } : {}),
145
+ ...(step.currentTool ? { currentTool: step.currentTool } : {}),
146
+ ...(step.currentToolArgs ? { currentToolArgs: step.currentToolArgs } : {}),
147
+ ...(step.currentToolStartedAt ? { currentToolStartedAt: step.currentToolStartedAt } : {}),
148
+ ...(step.currentPath ? { currentPath: step.currentPath } : {}),
149
+ ...(step.recentTools ? { recentTools: step.recentTools.map((tool) => ({ ...tool })) } : {}),
150
+ ...(step.recentOutput ? { recentOutput: [...step.recentOutput] } : {}),
151
+ ...(step.turnCount !== undefined ? { turnCount: step.turnCount } : {}),
152
+ ...(step.toolCount !== undefined ? { toolCount: step.toolCount } : {}),
153
+ ...(step.durationMs !== undefined ? { durationMs: step.durationMs } : {}),
154
+ ...(step.tokens ? { tokens: step.tokens } : {}),
155
+ ...(step.skills ? { skills: step.skills } : {}),
156
+ ...(step.model ? { model: step.model } : {}),
157
+ ...(step.thinking ? { thinking: step.thinking } : {}),
158
+ ...(step.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
159
+ ...(step.error ? { error: step.error } : {}),
160
+ ...(step.children?.length ? { children: step.children } : {}),
161
+ };
162
+ });
163
+ attachRootChildrenToSteps(status.runId || path.basename(asyncDir), summarizedSteps, nestedChildren);
123
164
  return {
124
165
  id: status.runId || path.basename(asyncDir),
125
166
  asyncDir,
@@ -140,32 +181,9 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
140
181
  currentStep: status.currentStep,
141
182
  ...(status.chainStepCount !== undefined ? { chainStepCount: status.chainStepCount } : {}),
142
183
  ...(parallelGroups.length ? { parallelGroups } : {}),
143
- steps: steps.map((step, index) => {
144
- const stepActivityState = step.activityState;
145
- const stepLastActivityAt = step.lastActivityAt;
146
- return {
147
- index,
148
- agent: step.agent,
149
- status: step.status,
150
- ...(stepActivityState ? { activityState: stepActivityState } : {}),
151
- ...(stepLastActivityAt ? { lastActivityAt: stepLastActivityAt } : {}),
152
- ...(step.currentTool ? { currentTool: step.currentTool } : {}),
153
- ...(step.currentToolArgs ? { currentToolArgs: step.currentToolArgs } : {}),
154
- ...(step.currentToolStartedAt ? { currentToolStartedAt: step.currentToolStartedAt } : {}),
155
- ...(step.currentPath ? { currentPath: step.currentPath } : {}),
156
- ...(step.recentTools ? { recentTools: step.recentTools.map((tool) => ({ ...tool })) } : {}),
157
- ...(step.recentOutput ? { recentOutput: [...step.recentOutput] } : {}),
158
- ...(step.turnCount !== undefined ? { turnCount: step.turnCount } : {}),
159
- ...(step.toolCount !== undefined ? { toolCount: step.toolCount } : {}),
160
- ...(step.durationMs !== undefined ? { durationMs: step.durationMs } : {}),
161
- ...(step.tokens ? { tokens: step.tokens } : {}),
162
- ...(step.skills ? { skills: step.skills } : {}),
163
- ...(step.model ? { model: step.model } : {}),
164
- ...(step.thinking ? { thinking: step.thinking } : {}),
165
- ...(step.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
166
- ...(step.error ? { error: step.error } : {}),
167
- };
168
- }),
184
+ steps: summarizedSteps,
185
+ ...(nestedChildren.length ? { nestedChildren } : {}),
186
+ ...(nestedWarnings.length ? { nestedWarnings } : {}),
169
187
  ...(status.sessionDir ? { sessionDir: status.sessionDir } : {}),
170
188
  ...(status.outputFile ? { outputFile: status.outputFile } : {}),
171
189
  ...(status.totalTokens ? { totalTokens: status.totalTokens } : {}),
@@ -212,7 +230,14 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
212
230
  : reconcileAsyncRun(asyncDir, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
213
231
  const status = (reconciliation?.status ?? readStatus(asyncDir)) as (AsyncStatus & { cwd?: string }) | null;
214
232
  if (!status) continue;
215
- const summary = statusToSummary(asyncDir, status);
233
+ const nestedWarnings: string[] = [];
234
+ try {
235
+ const nestedRoute = findNestedRouteForRootId(status.runId || path.basename(asyncDir));
236
+ if (nestedRoute) reconcileNestedAsyncDescendants(nestedRoute, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
237
+ } catch (error) {
238
+ nestedWarnings.push(`Nested status unavailable: ${getErrorMessage(error)}`);
239
+ }
240
+ const summary = statusToSummary(asyncDir, status, nestedWarnings);
216
241
  if (allowedStates && !allowedStates.has(summary.state)) continue;
217
242
  if (options.sessionId && summary.sessionId !== options.sessionId) continue;
218
243
  runs.push(summary);
@@ -285,7 +310,12 @@ export function formatAsyncRunList(runs: AsyncRunSummary[], heading = "Active as
285
310
  lines.push(`- ${formatRunHeader(run)}`);
286
311
  for (const step of run.steps) {
287
312
  lines.push(` ${formatStepLine(step)}`);
313
+ lines.push(...formatNestedRunStatusLines(step.children, { indent: " ", maxLines: 12 }));
288
314
  }
315
+ const attached = new Set(run.steps.flatMap((step) => step.children?.map((child) => child.id) ?? []));
316
+ const unattached = run.nestedChildren?.filter((child) => !attached.has(child.id)) ?? [];
317
+ lines.push(...formatNestedRunStatusLines(unattached, { indent: " ", maxLines: 12 }));
318
+ for (const warning of run.nestedWarnings ?? []) lines.push(` Warning: ${warning}`);
289
319
  const outputPath = formatAsyncRunOutputPath(run);
290
320
  if (outputPath) lines.push(` output: ${shortenPath(outputPath)}`);
291
321
  if (run.sessionFile) lines.push(` session: ${shortenPath(run.sessionFile)}`);