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.
@@ -5,13 +5,18 @@ import { createFileCoalescer } from "../../shared/file-coalescer.ts";
5
5
  import {
6
6
  SUBAGENT_ASYNC_COMPLETE_EVENT,
7
7
  type IntercomEventBus,
8
+ type NestedRunSummary,
9
+ type SubagentResultIntercomChild,
8
10
  type SubagentState,
9
11
  } from "../../shared/types.ts";
10
12
  import {
13
+ attachNestedChildrenToResultChildren,
11
14
  buildSubagentResultIntercomPayload,
15
+ compactNestedResultChildren,
12
16
  deliverSubagentResultIntercomEvent,
13
17
  resolveSubagentResultStatus,
14
18
  } from "../../intercom/result-intercom.ts";
19
+ import { projectNestedRegistryForRoot, sanitizeSummary } from "../shared/nested-events.ts";
15
20
 
16
21
  const WATCHER_RESTART_DELAY_MS = 3000;
17
22
  const POLL_INTERVAL_MS = 3000;
@@ -30,6 +35,47 @@ type ResultWatcherDeps = {
30
35
  timers?: ResultWatcherTimers;
31
36
  };
32
37
 
38
+ type ResultFileChild = {
39
+ agent?: string;
40
+ output?: string;
41
+ error?: string;
42
+ success?: boolean;
43
+ sessionFile?: string;
44
+ artifactPaths?: { outputPath?: string };
45
+ intercomTarget?: string;
46
+ children?: unknown;
47
+ };
48
+
49
+ type ResultFileData = {
50
+ id?: string;
51
+ runId?: string;
52
+ agent?: string;
53
+ success?: boolean;
54
+ state?: string;
55
+ mode?: string;
56
+ summary?: string;
57
+ results?: ResultFileChild[];
58
+ nestedChildren?: unknown;
59
+ sessionId?: string;
60
+ cwd?: string;
61
+ sessionFile?: string;
62
+ asyncDir?: string;
63
+ intercomTarget?: string;
64
+ };
65
+
66
+ function sanitizeNestedResultChildren(value: unknown, resultPath: string, label: string): NestedRunSummary[] | undefined {
67
+ if (value === undefined) return undefined;
68
+ if (!Array.isArray(value)) {
69
+ console.error(`Ignoring invalid nested children in subagent result file '${resultPath}' at ${label}: expected an array.`);
70
+ return undefined;
71
+ }
72
+ const children = value.map((child) => sanitizeSummary(child)).filter((child): child is NestedRunSummary => Boolean(child));
73
+ if (children.length !== value.length) {
74
+ console.error(`Ignoring ${value.length - children.length} invalid nested child record(s) in subagent result file '${resultPath}' at ${label}.`);
75
+ }
76
+ return children.length ? children : undefined;
77
+ }
78
+
33
79
  function getErrorCode(error: unknown): string | undefined {
34
80
  return typeof error === "object" && error !== null && "code" in error
35
81
  ? (error as NodeJS.ErrnoException).code
@@ -63,32 +109,21 @@ export function createResultWatcher(
63
109
  const resultPath = path.join(resultsDir, file);
64
110
  if (!fsApi.existsSync(resultPath)) return;
65
111
  try {
66
- const data = JSON.parse(fsApi.readFileSync(resultPath, "utf-8")) as {
67
- id?: string;
68
- runId?: string;
69
- agent?: string;
70
- success?: boolean;
71
- state?: string;
72
- mode?: string;
73
- summary?: string;
74
- results?: Array<{
75
- agent?: string;
76
- output?: string;
77
- error?: string;
78
- success?: boolean;
79
- sessionFile?: string;
80
- artifactPaths?: { outputPath?: string };
81
- intercomTarget?: string;
82
- }>;
83
- sessionId?: string;
84
- cwd?: string;
85
- sessionFile?: string;
86
- asyncDir?: string;
87
- intercomTarget?: string;
88
- };
112
+ const data = JSON.parse(fsApi.readFileSync(resultPath, "utf-8")) as ResultFileData;
89
113
  if (data.sessionId && data.sessionId !== state.currentSessionId) return;
90
114
  if (!data.sessionId && data.cwd && (!state.baseCwd || data.cwd !== state.baseCwd)) return;
91
115
 
116
+ const runId = data.runId ?? data.id ?? file.replace(/\.json$/i, "");
117
+ const hasExplicitNestedChildren = data.nestedChildren !== undefined;
118
+ let nestedChildren = compactNestedResultChildren(sanitizeNestedResultChildren(data.nestedChildren, resultPath, "nestedChildren"));
119
+ if (!nestedChildren?.length && !hasExplicitNestedChildren) {
120
+ try {
121
+ nestedChildren = compactNestedResultChildren(projectNestedRegistryForRoot(runId)?.children);
122
+ } catch (error) {
123
+ console.error(`Failed to enrich subagent result file '${resultPath}' with nested registry children; will retry later:`, error);
124
+ return;
125
+ }
126
+ }
92
127
  const now = Date.now();
93
128
  const completionKey = buildCompletionKey(data, `result:${file}`);
94
129
  if (markSeenWithTtl(state.completionSeen, completionKey, now, completionTtlMs)) {
@@ -96,45 +131,49 @@ export function createResultWatcher(
96
131
  return;
97
132
  }
98
133
 
134
+ const hasResultChildren = Array.isArray(data.results) && data.results.length > 0;
135
+ const resultChildren = hasResultChildren
136
+ ? data.results!
137
+ : [{
138
+ agent: data.agent,
139
+ output: data.summary,
140
+ success: data.success,
141
+ }];
142
+ const normalizedChildren = attachNestedChildrenToResultChildren(runId, resultChildren.map((result = {}, index): SubagentResultIntercomChild => {
143
+ const baseOutput = result.output ?? data.summary;
144
+ const hasRealOutput = typeof baseOutput === "string" && baseOutput.trim().length > 0;
145
+ const output = hasRealOutput ? baseOutput : "(no output)";
146
+ const summary = result.success === false && result.error
147
+ ? `${result.error}${hasRealOutput ? `\n\nOutput:\n${baseOutput}` : ""}`
148
+ : output;
149
+ const sessionPath = result.sessionFile ?? (resultChildren.length === 1 ? data.sessionFile : undefined);
150
+ const childNestedChildren = sanitizeNestedResultChildren(result.children, resultPath, `results[${index}].children`);
151
+ return {
152
+ agent: result.agent ?? data.agent ?? `step-${index + 1}`,
153
+ status: resolveSubagentResultStatus({
154
+ success: result.success,
155
+ state: data.state === "paused" || typeof result.success !== "boolean" ? data.state : undefined,
156
+ }),
157
+ summary,
158
+ index,
159
+ artifactPath: result.artifactPaths?.outputPath,
160
+ ...(typeof sessionPath === "string" && fsApi.existsSync(sessionPath) ? { sessionPath } : {}),
161
+ ...(result.intercomTarget ? { intercomTarget: result.intercomTarget } : {}),
162
+ ...(childNestedChildren ? { children: childNestedChildren } : {}),
163
+ };
164
+ }), nestedChildren);
165
+
99
166
  const intercomTarget = data.intercomTarget?.trim();
100
167
  if (intercomTarget) {
101
- const childResults = Array.isArray(data.results) && data.results.length > 0
102
- ? data.results
103
- : [{
104
- agent: data.agent,
105
- output: data.summary,
106
- success: data.success,
107
- }];
108
- const runId = data.runId ?? data.id ?? file.replace(/\.json$/i, "");
109
168
  const mode = data.mode === "single" || data.mode === "parallel" || data.mode === "chain"
110
169
  ? data.mode
111
- : childResults.length > 1 ? "chain" : "single";
170
+ : resultChildren.length > 1 ? "chain" : "single";
112
171
  const payload = buildSubagentResultIntercomPayload({
113
172
  to: intercomTarget,
114
173
  runId,
115
174
  mode,
116
175
  source: "async",
117
- children: childResults.map((result = {}, index) => {
118
- const baseOutput = result.output ?? data.summary;
119
- const hasRealOutput = typeof baseOutput === "string" && baseOutput.trim().length > 0;
120
- const output = hasRealOutput ? baseOutput : "(no output)";
121
- const summary = result.success === false && result.error
122
- ? `${result.error}${hasRealOutput ? `\n\nOutput:\n${baseOutput}` : ""}`
123
- : output;
124
- const sessionPath = result.sessionFile ?? (childResults.length === 1 ? data.sessionFile : undefined);
125
- return {
126
- agent: result.agent ?? data.agent ?? `step-${index + 1}`,
127
- status: resolveSubagentResultStatus({
128
- success: result.success,
129
- state: data.state === "paused" || typeof result.success !== "boolean" ? data.state : undefined,
130
- }),
131
- summary,
132
- index,
133
- artifactPath: result.artifactPaths?.outputPath,
134
- ...(typeof sessionPath === "string" && fsApi.existsSync(sessionPath) ? { sessionPath } : {}),
135
- intercomTarget: result.intercomTarget,
136
- };
137
- }),
176
+ children: normalizedChildren,
138
177
  asyncId: data.id,
139
178
  asyncDir: data.asyncDir,
140
179
  });
@@ -144,7 +183,25 @@ export function createResultWatcher(
144
183
  }
145
184
  }
146
185
 
147
- pi.events.emit(SUBAGENT_ASYNC_COMPLETE_EVENT, data);
186
+ pi.events.emit(SUBAGENT_ASYNC_COMPLETE_EVENT, {
187
+ ...data,
188
+ runId,
189
+ ...(nestedChildren?.length ? { nestedChildren } : {}),
190
+ ...(Array.isArray(data.results) ? {
191
+ results: hasResultChildren
192
+ ? normalizedChildren.map((child, index) => ({
193
+ ...data.results![index],
194
+ agent: child.agent,
195
+ status: child.status,
196
+ summary: child.summary,
197
+ index: child.index,
198
+ artifactPath: child.artifactPath,
199
+ sessionPath: child.sessionPath,
200
+ children: child.children,
201
+ }))
202
+ : [],
203
+ } : {}),
204
+ });
148
205
  fsApi.unlinkSync(resultPath);
149
206
  } catch (error) {
150
207
  if (isNotFoundError(error)) return;
@@ -0,0 +1,83 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { ASYNC_DIR, RESULTS_DIR, type SubagentState } from "../../shared/types.ts";
4
+ import { findAsyncRunPrefixMatches, type AsyncRunLocation } from "./async-resume.ts";
5
+ import { assertSafeNestedId, findNestedRunMatchesById, type NestedRoute, type NestedRunMatch, type NestedRunResolutionScope } from "../shared/nested-events.ts";
6
+
7
+ export type ResolvedSubagentRunId =
8
+ | { kind: "foreground"; id: string }
9
+ | { kind: "async"; id: string; location: AsyncRunLocation }
10
+ | { kind: "nested"; id: string; match: NestedRunMatch };
11
+
12
+ export interface ResolveSubagentRunIdDeps {
13
+ state?: SubagentState;
14
+ asyncDirRoot?: string;
15
+ resultsDir?: string;
16
+ nested?: NestedRunResolutionScope;
17
+ }
18
+
19
+ function exactAsyncLocation(id: string, asyncDirRoot: string, resultsDir: string): AsyncRunLocation | undefined {
20
+ const asyncDir = path.join(asyncDirRoot, id);
21
+ const resultPath = path.join(resultsDir, `${id}.json`);
22
+ if (!fs.existsSync(asyncDir) && !fs.existsSync(resultPath)) return undefined;
23
+ return {
24
+ asyncDir: fs.existsSync(asyncDir) ? asyncDir : null,
25
+ resultPath: fs.existsSync(resultPath) ? resultPath : null,
26
+ resolvedId: id,
27
+ };
28
+ }
29
+
30
+ function foregroundIds(state: SubagentState | undefined): string[] {
31
+ return state ? [...state.foregroundControls.keys()] : [];
32
+ }
33
+
34
+ function nestedScopeFromState(state: SubagentState | undefined): NestedRunResolutionScope | undefined {
35
+ if (!state) return undefined;
36
+ const routes: NestedRoute[] = [];
37
+ const seen = new Set<string>();
38
+ const add = (route: NestedRoute | undefined) => {
39
+ if (!route) return;
40
+ const key = `${route.rootRunId}:${route.eventSink}:${route.controlInbox}`;
41
+ if (seen.has(key)) return;
42
+ seen.add(key);
43
+ routes.push(route);
44
+ };
45
+ for (const control of state.foregroundControls.values()) add(control.nestedRoute as NestedRoute | undefined);
46
+ for (const job of state.asyncJobs.values()) add(job.nestedRoute as NestedRoute | undefined);
47
+ return { routes };
48
+ }
49
+
50
+ function asyncPrefixMatches(prefix: string, asyncDirRoot: string, resultsDir: string): Array<{ id: string; location: AsyncRunLocation }> {
51
+ return findAsyncRunPrefixMatches(prefix, asyncDirRoot, resultsDir);
52
+ }
53
+
54
+ export function resolveSubagentRunId(id: string, deps: ResolveSubagentRunIdDeps = {}): ResolvedSubagentRunId | undefined {
55
+ assertSafeNestedId("id", id);
56
+ const asyncDirRoot = deps.asyncDirRoot ?? ASYNC_DIR;
57
+ const resultsDir = deps.resultsDir ?? RESULTS_DIR;
58
+
59
+ const nestedScope = deps.nested ?? nestedScopeFromState(deps.state);
60
+ if (deps.state?.foregroundControls.has(id)) return { kind: "foreground", id };
61
+ const exactAsync = exactAsyncLocation(id, asyncDirRoot, resultsDir);
62
+ if (exactAsync) return { kind: "async", id, location: exactAsync };
63
+ const exactNested = findNestedRunMatchesById(id, nestedScope ? { scope: nestedScope } : {});
64
+ if (exactNested.length > 1) throw new Error(`Nested run id '${id}' is ambiguous across authorized registries. Provide the full id after stale registries are cleaned up.`);
65
+ if (exactNested[0]) return { kind: "nested", id, match: exactNested[0] };
66
+
67
+ const matches: ResolvedSubagentRunId[] = [];
68
+ for (const foregroundId of foregroundIds(deps.state).filter((candidate) => candidate.startsWith(id))) {
69
+ matches.push({ kind: "foreground", id: foregroundId });
70
+ }
71
+ for (const match of asyncPrefixMatches(id, asyncDirRoot, resultsDir)) {
72
+ matches.push({ kind: "async", id: match.id, location: match.location });
73
+ }
74
+ for (const match of findNestedRunMatchesById(id, nestedScope ? { prefix: true, scope: nestedScope } : { prefix: true })) {
75
+ matches.push({ kind: "nested", id: match.run.id, match });
76
+ }
77
+ const unique = new Map(matches.map((match) => [`${match.kind}:${match.id}`, match]));
78
+ const values = [...unique.values()];
79
+ if (values.length > 1) {
80
+ throw new Error(`Ambiguous subagent run id prefix '${id}' matched: ${values.map((match) => `${match.kind}:${match.id}`).join(", ")}. Provide a longer id.`);
81
+ }
82
+ return values[0];
83
+ }
@@ -2,13 +2,16 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
4
4
  import { formatAsyncRunList, formatAsyncRunOutputPath, formatAsyncRunProgressLabel, listAsyncRuns } from "./async-status.ts";
5
+ import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
5
6
  import { formatModelThinking } from "../../shared/formatters.ts";
6
7
  import { formatActivityLabel } from "../../shared/status-format.ts";
7
- import { ASYNC_DIR, RESULTS_DIR, type AsyncStatus, type Details } from "../../shared/types.ts";
8
+ import { ASYNC_DIR, RESULTS_DIR, type AsyncStatus, type Details, type NestedRunSummary, type SubagentState } from "../../shared/types.ts";
8
9
  import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
9
10
  import { resolveAsyncRunLocation } from "./async-resume.ts";
11
+ import { resolveSubagentRunId } from "./run-id-resolver.ts";
10
12
  import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-groups.ts";
11
- import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
13
+ import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
14
+ import { attachRootChildrenToSteps, findNestedRouteForRootId, projectNestedRegistryForRoot, type NestedRunResolutionScope } from "../shared/nested-events.ts";
12
15
 
13
16
  interface RunStatusParams {
14
17
  action?: "status";
@@ -22,6 +25,8 @@ interface RunStatusDeps {
22
25
  resultsDir?: string;
23
26
  kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
24
27
  now?: () => number;
28
+ state?: SubagentState;
29
+ nested?: NestedRunResolutionScope;
25
30
  }
26
31
 
27
32
  function hasExistingSessionFile(value: unknown): value is string {
@@ -57,10 +62,53 @@ function stepLineLabel(status: AsyncStatus, index: number): string {
57
62
  return `Step ${index + 1}`;
58
63
  }
59
64
 
65
+ function nestedRunDisplayName(run: NestedRunSummary): string {
66
+ if (run.agent) return run.agent;
67
+ if (run.agents?.length) return run.agents.join(", ");
68
+ return run.id;
69
+ }
70
+
71
+ function formatNestedExactStatus(rootRunId: string, run: NestedRunSummary): string {
72
+ const lines = [
73
+ `Nested run: ${run.id}`,
74
+ `Root: ${rootRunId}`,
75
+ `Parent: ${run.parentRunId}${run.parentStepIndex !== undefined ? ` step ${run.parentStepIndex + 1}` : ""}`,
76
+ `State: ${run.state}`,
77
+ run.activityState || run.lastActivityAt ? `Activity: ${formatActivityLabel(run.lastActivityAt, run.activityState)}` : undefined,
78
+ run.mode ? `Mode: ${run.mode}` : undefined,
79
+ `Agent: ${nestedRunDisplayName(run)}`,
80
+ run.currentStep !== undefined ? `Progress: step ${run.currentStep + 1}/${run.chainStepCount ?? run.steps?.length ?? 1}` : undefined,
81
+ run.asyncDir ? `Dir: ${run.asyncDir}` : undefined,
82
+ run.sessionFile ? `Session: ${run.sessionFile}` : undefined,
83
+ run.error ? `Error: ${run.error}` : undefined,
84
+ ].filter((line): line is string => Boolean(line));
85
+ if (run.path.length) {
86
+ lines.push(`Path: ${run.path.map((part) => `${part.runId}${part.stepIndex !== undefined ? `:${part.stepIndex + 1}` : ""}${part.agent ? `:${part.agent}` : ""}`).join(" > ")} > ${run.id}`);
87
+ }
88
+ if (run.steps?.length) {
89
+ lines.push("Steps:");
90
+ for (const [index, step] of run.steps.entries()) {
91
+ const activity = step.status === "running" ? formatActivityLabel(step.lastActivityAt, step.activityState) : undefined;
92
+ lines.push(` ${index + 1}. ${step.agent} ${step.status}${activity ? `, ${activity}` : ""}${step.error ? `, error: ${step.error}` : ""}`);
93
+ lines.push(...formatNestedRunStatusLines(step.children, { indent: " ", commandHints: true }));
94
+ }
95
+ }
96
+ lines.push(...formatNestedRunStatusLines(run.children, { indent: " ", commandHints: true }));
97
+ lines.push("Commands:", ` Status: subagent({ action: "status", id: "${run.id}" })`, ` Interrupt: subagent({ action: "interrupt", id: "${run.id}" })`, ` Resume: subagent({ action: "resume", id: "${run.id}", message: "..." })`, ` Root status: subagent({ action: "status", id: "${rootRunId}" })`);
98
+ return lines.join("\n");
99
+ }
100
+
60
101
  export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDeps = {}): AgentToolResult<Details> {
61
102
  const asyncDirRoot = deps.asyncDirRoot ?? ASYNC_DIR;
62
103
  const resultsDir = deps.resultsDir ?? RESULTS_DIR;
63
104
  if (!params.id && !params.runId && !params.dir) {
105
+ if (deps.nested) {
106
+ return {
107
+ content: [{ type: "text", text: "Child-safe subagent status requires an id when no foreground run is active." }],
108
+ isError: true,
109
+ details: { mode: "single", results: [] },
110
+ };
111
+ }
64
112
  try {
65
113
  const runs = listAsyncRuns(asyncDirRoot, { states: ["queued", "running"], resultsDir, kill: deps.kill, now: deps.now });
66
114
  return {
@@ -79,7 +127,20 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
79
127
 
80
128
  let location;
81
129
  try {
82
- location = resolveAsyncRunLocation(params, asyncDirRoot, resultsDir);
130
+ const requestedId = params.id ?? params.runId;
131
+ if (!params.dir && requestedId) {
132
+ const resolved = resolveSubagentRunId(requestedId, { asyncDirRoot, resultsDir, state: deps.state, nested: deps.nested });
133
+ if (resolved?.kind === "nested") {
134
+ reconcileNestedAsyncDescendants(resolved.match.route, { resultsDir, kill: deps.kill, now: deps.now });
135
+ const refreshed = resolveSubagentRunId(requestedId, { asyncDirRoot, resultsDir, state: deps.state, nested: deps.nested });
136
+ const nested = refreshed?.kind === "nested" ? refreshed : resolved;
137
+ return { content: [{ type: "text", text: formatNestedExactStatus(nested.match.rootRunId, nested.match.run) }], details: { mode: "single", results: [] } };
138
+ }
139
+ if (resolved?.kind === "async") location = resolved.location;
140
+ else location = { asyncDir: null, resultPath: null, resolvedId: requestedId };
141
+ } else {
142
+ location = resolveAsyncRunLocation(params, asyncDirRoot, resultsDir);
143
+ }
83
144
  } catch (error) {
84
145
  const message = error instanceof Error ? error.message : String(error);
85
146
  return {
@@ -115,6 +176,16 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
115
176
  const logPath = path.join(asyncDir, `subagent-log-${effectiveRunId}.md`);
116
177
  const eventsPath = path.join(asyncDir, "events.jsonl");
117
178
  if (status) {
179
+ let nestedChildren: NestedRunSummary[] = [];
180
+ let nestedWarning: string | undefined;
181
+ try {
182
+ const nestedRoute = findNestedRouteForRootId(status.runId);
183
+ if (nestedRoute) reconcileNestedAsyncDescendants(nestedRoute, { resultsDir, kill: deps.kill, now: deps.now });
184
+ nestedChildren = projectNestedRegistryForRoot(status.runId)?.children ?? [];
185
+ attachRootChildrenToSteps(status.runId, status.steps, nestedChildren);
186
+ } catch (error) {
187
+ nestedWarning = `Nested status unavailable: ${error instanceof Error ? error.message : String(error)}`;
188
+ }
118
189
  const outputPath = formatAsyncRunOutputPath({ asyncDir, outputFile: status.outputFile });
119
190
  const progressLabel = formatAsyncRunProgressLabel({
120
191
  mode: status.mode,
@@ -147,12 +218,17 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
147
218
  const modelText = modelThinking ? ` (${modelThinking})` : "";
148
219
  const errorText = step.error ? `, error: ${step.error}` : "";
149
220
  lines.push(`${stepLineLabel(status, index)}: ${step.agent} ${step.status}${modelText}${stepActivityText ? `, ${stepActivityText}` : ""}${errorText}`);
221
+ lines.push(...formatNestedRunStatusLines(step.children, { indent: " ", commandHints: true, maxLines: 20 }));
150
222
  const stepOutputPath = path.join(asyncDir, `output-${index}.log`);
151
223
  if (stepOutputPath !== outputPath && fs.existsSync(stepOutputPath)) lines.push(` Output: ${stepOutputPath}`);
152
224
  if (step.status === "running") {
153
225
  lines.push(` Intercom target: ${resolveSubagentIntercomTarget(status.runId, step.agent, index)} (if registered)`);
154
226
  }
155
227
  }
228
+ const attached = new Set((status.steps ?? []).flatMap((step) => step.children?.map((child) => child.id) ?? []));
229
+ const unattached = nestedChildren.filter((child) => !attached.has(child.id));
230
+ lines.push(...formatNestedRunStatusLines(unattached, { indent: "", commandHints: true, maxLines: 20 }));
231
+ if (nestedWarning) lines.push(`Warning: ${nestedWarning}`);
156
232
  if (status.sessionFile) lines.push(`Session: ${status.sessionFile}`);
157
233
  if (status.state !== "running") {
158
234
  lines.push(formatResumeGuidance(status.runId, status.steps ?? [], status.sessionFile));
@@ -1,8 +1,9 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
4
- import { RESULTS_DIR, type AsyncParallelGroupStatus, type AsyncStatus, type SubagentRunMode } from "../../shared/types.ts";
4
+ import { RESULTS_DIR, type AsyncParallelGroupStatus, type AsyncStatus, type NestedRunSummary, type SubagentRunMode } from "../../shared/types.ts";
5
5
  import { normalizeParallelGroups } from "./parallel-groups.ts";
6
+ import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent, type NestedRoute } from "../shared/nested-events.ts";
6
7
 
7
8
  export type PidLiveness = "alive" | "dead" | "unknown";
8
9
 
@@ -233,6 +234,50 @@ function writeFailedRepair(asyncDir: string, status: AsyncStatus, resultPath: st
233
234
  return { status: repair.status, repaired: true, resultPath, message: repair.message };
234
235
  }
235
236
 
237
+ function terminal(state: AsyncStatus["state"]): boolean {
238
+ return state === "complete" || state === "failed" || state === "paused";
239
+ }
240
+
241
+ function* nestedRuns(children: NestedRunSummary[] | undefined): Generator<NestedRunSummary> {
242
+ for (const child of children ?? []) {
243
+ yield child;
244
+ yield* nestedRuns(child.children);
245
+ yield* nestedRuns(child.steps?.flatMap((step) => step.children ?? []));
246
+ }
247
+ }
248
+
249
+ export function reconcileNestedAsyncDescendants(route: NestedRoute, options: ReconcileAsyncRunOptions = {}): void {
250
+ const registry = projectNestedEvents(route);
251
+ for (const run of nestedRuns(registry.children)) {
252
+ if (run.state !== "running" && run.state !== "queued") continue;
253
+ const asyncDir = resolveNestedAsyncDir(route.rootRunId, run);
254
+ if (!asyncDir) continue;
255
+ const result = reconcileAsyncRun(asyncDir, {
256
+ ...options,
257
+ resultsDir: path.join(options.resultsDir ?? RESULTS_DIR, "nested", route.rootRunId),
258
+ });
259
+ const status = result.status;
260
+ if (!status) continue;
261
+ if (!result.repaired && !terminal(status.state)) continue;
262
+ const ts = options.now?.() ?? Date.now();
263
+ writeNestedEvent(route, {
264
+ type: terminal(status.state) ? "subagent.nested.completed" : "subagent.nested.updated",
265
+ ts,
266
+ parentRunId: run.parentRunId,
267
+ parentStepIndex: run.parentStepIndex,
268
+ child: nestedSummaryFromAsyncStatus(status, asyncDir, {
269
+ id: run.id,
270
+ parentRunId: run.parentRunId,
271
+ parentStepIndex: run.parentStepIndex,
272
+ depth: run.depth,
273
+ path: run.path,
274
+ mode: run.mode,
275
+ ts,
276
+ }),
277
+ });
278
+ }
279
+ }
280
+
236
281
  export function checkPidLiveness(pid: number, kill: KillFn = process.kill): PidLiveness {
237
282
  try {
238
283
  kill(pid, 0);