pi-subagents 0.35.0 → 0.36.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 +59 -0
- package/README.md +132 -24
- package/agents/advisor.md +73 -0
- package/package.json +8 -12
- package/skills/pi-subagents/SKILL.md +22 -9
- package/src/agents/agents.ts +22 -5
- package/src/api/delegation.ts +125 -0
- package/src/extension/config.ts +7 -1
- package/src/extension/index.ts +50 -38
- package/src/extension/rpc.ts +27 -2
- package/src/extension/schemas.ts +22 -2
- package/src/extension/tool-description.ts +2 -2
- package/src/intercom/intercom-bridge.ts +1 -1
- package/src/intercom/native-supervisor-channel.ts +45 -6
- package/src/intercom/result-intercom.ts +7 -0
- package/src/runs/background/async-execution.ts +34 -4
- package/src/runs/background/async-job-tracker.ts +4 -0
- package/src/runs/background/async-resume.ts +27 -5
- package/src/runs/background/async-status.ts +76 -3
- package/src/runs/background/chain-append.ts +2 -0
- package/src/runs/background/completion-batcher.ts +6 -4
- package/src/runs/background/completion-dedupe.ts +2 -11
- package/src/runs/background/fleet-view.ts +9 -4
- package/src/runs/background/notify.ts +132 -120
- package/src/runs/background/result-watcher.ts +138 -78
- package/src/runs/background/run-status.ts +3 -1
- package/src/runs/background/subagent-runner.ts +225 -43
- package/src/runs/background/subagent-wait.ts +130 -4
- package/src/runs/background/wait-tool.ts +2 -2
- package/src/runs/foreground/chain-execution.ts +176 -111
- package/src/runs/foreground/execution.ts +90 -36
- package/src/runs/foreground/foreground-control.ts +90 -0
- package/src/runs/foreground/subagent-executor.ts +394 -163
- package/src/runs/shared/acceptance.ts +55 -13
- package/src/runs/shared/agent-contract.ts +38 -0
- package/src/runs/shared/child-protocol.ts +1 -1
- package/src/runs/shared/completion-guard.ts +36 -5
- package/src/runs/shared/context-mode.ts +44 -0
- package/src/runs/shared/dynamic-fanout.ts +4 -4
- package/src/runs/shared/long-running-guard.ts +4 -0
- package/src/runs/shared/nested-events.ts +27 -2
- package/src/runs/shared/parallel-handoff.ts +154 -0
- package/src/runs/shared/parallel-utils.ts +6 -0
- package/src/runs/shared/pi-args.ts +23 -14
- package/src/runs/shared/run-history.ts +90 -5
- package/src/runs/shared/structured-output.ts +112 -7
- package/src/runs/shared/subagent-control.ts +4 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +17 -18
- package/src/runs/shared/task-intent.ts +10 -5
- package/src/runs/shared/tool-availability.ts +3 -1
- package/src/runs/shared/tool-budget.ts +11 -5
- package/src/runs/shared/turn-budget.ts +2 -1
- package/src/runs/shared/worktree.ts +63 -14
- package/src/shared/accessible-dir.ts +25 -0
- package/src/shared/artifacts.ts +37 -7
- package/src/shared/atomic-json.ts +14 -42
- package/src/shared/child-transcript.ts +52 -0
- package/src/shared/file-system-retry.ts +47 -0
- package/src/shared/settings.ts +9 -1
- package/src/shared/types.ts +211 -22
- package/src/slash/delegation-adapters.ts +152 -5
- package/src/slash/delegation-json.ts +108 -0
- package/src/slash/delegation-request.ts +182 -36
- package/src/slash/prompt-template-bridge.ts +222 -37
- package/src/slash/selector.ts +147 -0
- package/src/slash/slash-commands.ts +14 -5
- package/src/slash/slash-live-state.ts +2 -2
- package/src/slash/subagents-admin.ts +42 -42
- package/src/tui/fleet-status.ts +362 -0
- package/src/tui/fleet-transcript.ts +472 -0
- package/src/tui/fleet.ts +318 -59
- package/src/tui/render.ts +25 -15
- package/src/watchdog/change-signature.ts +105 -12
- package/src/watchdog/review.ts +7 -2
- package/src/watchdog/runtime.ts +5 -3
- package/src/slash/subagents-editor.ts +0 -86
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
SUBAGENT_ASYNC_COMPLETE_EVENT,
|
|
7
7
|
type IntercomEventBus,
|
|
8
8
|
type NestedRunSummary,
|
|
9
|
+
type ParallelHandoffReference,
|
|
9
10
|
type SubagentResultIntercomChild,
|
|
10
11
|
type SubagentState,
|
|
11
12
|
} from "../../shared/types.ts";
|
|
@@ -18,9 +19,11 @@ import {
|
|
|
18
19
|
} from "../../intercom/result-intercom.ts";
|
|
19
20
|
import { projectNestedRegistryForRoot, sanitizeSummary } from "../shared/nested-events.ts";
|
|
20
21
|
import { resolveWatchPath } from "../../shared/utils.ts";
|
|
22
|
+
import type { CompletionNotifier, CompletionNotification } from "./notify.ts";
|
|
21
23
|
|
|
22
24
|
const WATCHER_RESTART_DELAY_MS = 3000;
|
|
23
25
|
const POLL_INTERVAL_MS = 3000;
|
|
26
|
+
const RETRY_DELAY_MS = 100;
|
|
24
27
|
|
|
25
28
|
type ResultWatcherFs = Pick<typeof fs, "existsSync" | "readFileSync" | "unlinkSync" | "readdirSync" | "mkdirSync" | "realpathSync" | "watch">;
|
|
26
29
|
|
|
@@ -34,6 +37,7 @@ type ResultWatcherTimers = {
|
|
|
34
37
|
type ResultWatcherDeps = {
|
|
35
38
|
fs?: ResultWatcherFs;
|
|
36
39
|
timers?: ResultWatcherTimers;
|
|
40
|
+
notifier?: Pick<CompletionNotifier, "deliver">;
|
|
37
41
|
};
|
|
38
42
|
|
|
39
43
|
type ResultFileChild = {
|
|
@@ -49,21 +53,14 @@ type ResultFileChild = {
|
|
|
49
53
|
children?: unknown;
|
|
50
54
|
};
|
|
51
55
|
|
|
52
|
-
type ResultFileData = {
|
|
53
|
-
id?: string;
|
|
56
|
+
type ResultFileData = CompletionNotification & {
|
|
54
57
|
runId?: string;
|
|
55
|
-
agent?: string;
|
|
56
|
-
success?: boolean;
|
|
57
|
-
state?: string;
|
|
58
58
|
mode?: string;
|
|
59
|
-
summary?: string;
|
|
60
59
|
results?: ResultFileChild[];
|
|
61
60
|
nestedChildren?: unknown;
|
|
62
|
-
sessionId?: string;
|
|
63
|
-
cwd?: string;
|
|
64
|
-
sessionFile?: string;
|
|
65
61
|
asyncDir?: string;
|
|
66
62
|
intercomTarget?: string;
|
|
63
|
+
parallelHandoff?: ParallelHandoffReference;
|
|
67
64
|
};
|
|
68
65
|
|
|
69
66
|
function sanitizeNestedResultChildren(value: unknown, resultPath: string, label: string): NestedRunSummary[] | undefined {
|
|
@@ -79,22 +76,24 @@ function sanitizeNestedResultChildren(value: unknown, resultPath: string, label:
|
|
|
79
76
|
return children.length ? children : undefined;
|
|
80
77
|
}
|
|
81
78
|
|
|
82
|
-
function
|
|
83
|
-
return typeof error === "object" && error !== null && "code" in error
|
|
84
|
-
? (error as NodeJS.ErrnoException).code
|
|
85
|
-
: undefined;
|
|
79
|
+
function errorCode(error: unknown): string | undefined {
|
|
80
|
+
return typeof error === "object" && error !== null && "code" in error ? (error as NodeJS.ErrnoException).code : undefined;
|
|
86
81
|
}
|
|
87
82
|
|
|
88
|
-
function
|
|
89
|
-
return
|
|
83
|
+
function isNotFound(error: unknown): boolean {
|
|
84
|
+
return errorCode(error) === "ENOENT";
|
|
90
85
|
}
|
|
91
86
|
|
|
92
|
-
function
|
|
93
|
-
const code =
|
|
87
|
+
function shouldPoll(error: unknown): boolean {
|
|
88
|
+
const code = errorCode(error);
|
|
94
89
|
return code === "EMFILE" || code === "ENOSPC";
|
|
95
90
|
}
|
|
96
91
|
|
|
97
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Watches persisted async results for the session currently owned by this
|
|
94
|
+
* runtime. `stopResultWatcher()` revokes ownership before closing resources,
|
|
95
|
+
* so old callbacks can never emit or delete after reload/session replacement.
|
|
96
|
+
*/
|
|
98
97
|
export function createResultWatcher(
|
|
99
98
|
pi: { events: IntercomEventBus },
|
|
100
99
|
state: SubagentState,
|
|
@@ -103,18 +102,41 @@ export function createResultWatcher(
|
|
|
103
102
|
deps: ResultWatcherDeps = {},
|
|
104
103
|
): {
|
|
105
104
|
startResultWatcher: () => void;
|
|
106
|
-
primeExistingResults: () => void;
|
|
105
|
+
primeExistingResults: (options?: { triggerTurn?: boolean }) => void;
|
|
107
106
|
stopResultWatcher: () => void;
|
|
108
107
|
} {
|
|
109
108
|
const fsApi = deps.fs ?? fs;
|
|
110
109
|
const timers = deps.timers ?? { setTimeout, clearTimeout, setInterval, clearInterval };
|
|
110
|
+
const notifier = deps.notifier ?? { deliver: async () => true };
|
|
111
|
+
const pendingTriggerTurn = new Map<string, boolean>();
|
|
112
|
+
const processing = new Set<string>();
|
|
113
|
+
let deliveryActive = true;
|
|
114
|
+
let deliveryEpoch = 0;
|
|
115
|
+
// The sole in-memory ownership lease. It is acquired for one active session
|
|
116
|
+
// and revoked before the watcher, queues, or callbacks are torn down.
|
|
117
|
+
let activeSessionId: string | null = null;
|
|
111
118
|
|
|
112
|
-
const
|
|
119
|
+
const ownsSession = (sessionId: string, epoch: number) => {
|
|
120
|
+
if (!deliveryActive || epoch !== deliveryEpoch) return false;
|
|
121
|
+
if (!activeSessionId && state.currentSessionId) activeSessionId = state.currentSessionId;
|
|
122
|
+
return activeSessionId === sessionId && state.currentSessionId === sessionId;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const scheduleResult = (file: string, triggerTurn: boolean, delayMs = 0) => {
|
|
126
|
+
const pendingMode = pendingTriggerTurn.get(file);
|
|
127
|
+
pendingTriggerTurn.set(file, pendingMode === false || !triggerTurn ? false : true);
|
|
128
|
+
state.resultFileCoalescer.schedule(file, delayMs);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const handleResult = async (file: string, triggerTurn: boolean) => {
|
|
113
132
|
const resultPath = path.join(resultsDir, file);
|
|
114
|
-
if (!fsApi.existsSync(resultPath)) return;
|
|
133
|
+
if (processing.has(file) || !fsApi.existsSync(resultPath)) return;
|
|
134
|
+
processing.add(file);
|
|
115
135
|
try {
|
|
116
136
|
const data = JSON.parse(fsApi.readFileSync(resultPath, "utf-8")) as ResultFileData;
|
|
117
|
-
if (typeof data.sessionId !== "string" || data.sessionId
|
|
137
|
+
if (typeof data.sessionId !== "string" || !data.sessionId) return;
|
|
138
|
+
const epoch = deliveryEpoch;
|
|
139
|
+
if (!ownsSession(data.sessionId, epoch)) return;
|
|
118
140
|
|
|
119
141
|
const runId = data.runId ?? data.id ?? file.replace(/\.json$/i, "");
|
|
120
142
|
const hasExplicitNestedChildren = data.nestedChildren !== undefined;
|
|
@@ -124,24 +146,32 @@ export function createResultWatcher(
|
|
|
124
146
|
nestedChildren = compactNestedResultChildren(projectNestedRegistryForRoot(runId)?.children);
|
|
125
147
|
} catch (error) {
|
|
126
148
|
console.error(`Failed to enrich subagent result file '${resultPath}' with nested registry children; will retry later:`, error);
|
|
149
|
+
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
127
150
|
return;
|
|
128
151
|
}
|
|
129
152
|
}
|
|
130
|
-
|
|
153
|
+
|
|
131
154
|
const completionKey = buildCompletionKey(data, `result:${file}`);
|
|
132
|
-
|
|
133
|
-
|
|
155
|
+
const lastSeenAt = state.completionSeen.get(completionKey);
|
|
156
|
+
if (lastSeenAt !== undefined && Date.now() - lastSeenAt > completionTtlMs) {
|
|
157
|
+
state.completionSeen.delete(completionKey);
|
|
158
|
+
} else if (lastSeenAt !== undefined) {
|
|
159
|
+
if (!ownsSession(data.sessionId, epoch) || !fsApi.existsSync(resultPath)) return;
|
|
160
|
+
try {
|
|
161
|
+
fsApi.unlinkSync(resultPath);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (!isNotFound(error)) {
|
|
164
|
+
console.error(`Failed to remove delivered subagent result '${resultPath}'; will retry:`, error);
|
|
165
|
+
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
134
168
|
return;
|
|
135
169
|
}
|
|
136
170
|
|
|
137
171
|
const hasResultChildren = Array.isArray(data.results) && data.results.length > 0;
|
|
138
|
-
const resultChildren = hasResultChildren
|
|
172
|
+
const resultChildren: ResultFileChild[] = hasResultChildren
|
|
139
173
|
? data.results!
|
|
140
|
-
: [{
|
|
141
|
-
agent: data.agent,
|
|
142
|
-
output: data.summary,
|
|
143
|
-
success: data.success,
|
|
144
|
-
}];
|
|
174
|
+
: [{ agent: data.agent ?? undefined, output: data.summary, success: data.success }];
|
|
145
175
|
const normalizedChildren = attachNestedChildrenToResultChildren(runId, resultChildren.map((result = {}, index): SubagentResultIntercomChild => {
|
|
146
176
|
const baseOutput = result.output ?? data.summary;
|
|
147
177
|
const hasRealOutput = typeof baseOutput === "string" && baseOutput.trim().length > 0;
|
|
@@ -160,10 +190,7 @@ export function createResultWatcher(
|
|
|
160
190
|
: undefined;
|
|
161
191
|
return {
|
|
162
192
|
agent: result.agent ?? data.agent ?? `step-${index + 1}`,
|
|
163
|
-
status: resolveSubagentResultStatus({
|
|
164
|
-
success: result.success,
|
|
165
|
-
state: childState,
|
|
166
|
-
}),
|
|
193
|
+
status: resolveSubagentResultStatus({ success: result.success, state: childState }),
|
|
167
194
|
summary,
|
|
168
195
|
index,
|
|
169
196
|
artifactPath: result.artifactPaths?.outputPath,
|
|
@@ -174,11 +201,11 @@ export function createResultWatcher(
|
|
|
174
201
|
}), nestedChildren);
|
|
175
202
|
|
|
176
203
|
const intercomTarget = data.intercomTarget?.trim();
|
|
177
|
-
if (intercomTarget) {
|
|
204
|
+
if (intercomTarget && triggerTurn) {
|
|
178
205
|
const mode = data.mode === "single" || data.mode === "parallel" || data.mode === "chain"
|
|
179
206
|
? data.mode
|
|
180
207
|
: resultChildren.length > 1 ? "chain" : "single";
|
|
181
|
-
const
|
|
208
|
+
const delivered = await deliverSubagentResultIntercomEvent(pi.events, buildSubagentResultIntercomPayload({
|
|
182
209
|
to: intercomTarget,
|
|
183
210
|
runId,
|
|
184
211
|
mode,
|
|
@@ -186,20 +213,45 @@ export function createResultWatcher(
|
|
|
186
213
|
children: normalizedChildren,
|
|
187
214
|
asyncId: data.id,
|
|
188
215
|
asyncDir: data.asyncDir,
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (!
|
|
192
|
-
|
|
193
|
-
}
|
|
216
|
+
...(data.parallelHandoff ? { parallelHandoff: data.parallelHandoff } : {}),
|
|
217
|
+
}));
|
|
218
|
+
if (!ownsSession(data.sessionId, epoch)) return;
|
|
219
|
+
if (!delivered) console.error(`Subagent async grouped result intercom delivery was not acknowledged for '${resultPath}'.`);
|
|
194
220
|
}
|
|
195
221
|
|
|
196
|
-
|
|
222
|
+
const accepted = await notifier.deliver({
|
|
197
223
|
...data,
|
|
224
|
+
id: data.id ?? runId,
|
|
198
225
|
runId,
|
|
226
|
+
triggerTurn,
|
|
199
227
|
...(nestedChildren?.length ? { nestedChildren } : {}),
|
|
200
228
|
...(Array.isArray(data.results) ? {
|
|
201
|
-
results: hasResultChildren
|
|
202
|
-
|
|
229
|
+
results: hasResultChildren ? normalizedChildren.map((child, index) => ({
|
|
230
|
+
...data.results![index],
|
|
231
|
+
agent: child.agent,
|
|
232
|
+
status: child.status,
|
|
233
|
+
summary: child.summary,
|
|
234
|
+
index: child.index,
|
|
235
|
+
artifactPath: child.artifactPath,
|
|
236
|
+
sessionPath: child.sessionPath,
|
|
237
|
+
children: child.children,
|
|
238
|
+
})) : [],
|
|
239
|
+
} : {}),
|
|
240
|
+
});
|
|
241
|
+
if (!ownsSession(data.sessionId, epoch)) return;
|
|
242
|
+
if (!accepted) {
|
|
243
|
+
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
markSeenWithTtl(state.completionSeen, completionKey, Date.now(), completionTtlMs);
|
|
247
|
+
try {
|
|
248
|
+
pi.events.emit(SUBAGENT_ASYNC_COMPLETE_EVENT, {
|
|
249
|
+
...data,
|
|
250
|
+
runId,
|
|
251
|
+
triggerTurn,
|
|
252
|
+
...(nestedChildren?.length ? { nestedChildren } : {}),
|
|
253
|
+
...(Array.isArray(data.results) ? {
|
|
254
|
+
results: hasResultChildren ? normalizedChildren.map((child, index) => ({
|
|
203
255
|
...data.results![index],
|
|
204
256
|
agent: child.agent,
|
|
205
257
|
status: child.status,
|
|
@@ -208,40 +260,50 @@ export function createResultWatcher(
|
|
|
208
260
|
artifactPath: child.artifactPath,
|
|
209
261
|
sessionPath: child.sessionPath,
|
|
210
262
|
children: child.children,
|
|
211
|
-
}))
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
})
|
|
215
|
-
|
|
263
|
+
})) : [],
|
|
264
|
+
} : {}),
|
|
265
|
+
});
|
|
266
|
+
} catch (error) {
|
|
267
|
+
console.error(`Completion observer failed for '${resultPath}':`, error);
|
|
268
|
+
}
|
|
269
|
+
if (!ownsSession(data.sessionId, epoch) || !fsApi.existsSync(resultPath)) return;
|
|
270
|
+
try {
|
|
271
|
+
fsApi.unlinkSync(resultPath);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
if (!isNotFound(error)) {
|
|
274
|
+
console.error(`Failed to remove delivered subagent result '${resultPath}'; will retry:`, error);
|
|
275
|
+
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
216
278
|
} catch (error) {
|
|
217
|
-
if (
|
|
218
|
-
|
|
279
|
+
if (!isNotFound(error)) console.error(`Failed to process subagent result file '${resultPath}':`, error);
|
|
280
|
+
} finally {
|
|
281
|
+
processing.delete(file);
|
|
219
282
|
}
|
|
220
283
|
};
|
|
221
284
|
|
|
222
285
|
state.resultFileCoalescer = createFileCoalescer((file) => {
|
|
223
|
-
|
|
286
|
+
const triggerTurn = pendingTriggerTurn.get(file) !== false;
|
|
287
|
+
pendingTriggerTurn.delete(file);
|
|
288
|
+
void handleResult(file, triggerTurn);
|
|
224
289
|
}, 50);
|
|
225
290
|
|
|
226
|
-
const primeExistingResults = () => {
|
|
291
|
+
const primeExistingResults = (options: { triggerTurn?: boolean } = {}) => {
|
|
227
292
|
try {
|
|
293
|
+
const triggerTurn = options.triggerTurn !== false;
|
|
228
294
|
fsApi.readdirSync(resultsDir)
|
|
229
295
|
.filter((f) => f.endsWith(".json"))
|
|
230
|
-
.forEach((file) =>
|
|
296
|
+
.forEach((file) => scheduleResult(file, triggerTurn));
|
|
231
297
|
} catch (error) {
|
|
232
|
-
if (
|
|
233
|
-
console.error(`Failed to scan subagent result directory '${resultsDir}':`, error);
|
|
298
|
+
if (!isNotFound(error)) console.error(`Failed to scan subagent result directory '${resultsDir}':`, error);
|
|
234
299
|
}
|
|
235
300
|
};
|
|
236
301
|
|
|
237
|
-
const
|
|
302
|
+
const startPolling = (reason: unknown) => {
|
|
238
303
|
state.watcher?.close();
|
|
239
304
|
state.watcher = null;
|
|
240
305
|
if (state.watcherRestartTimer) return;
|
|
241
|
-
|
|
242
|
-
console.error(
|
|
243
|
-
`Subagent result watcher for '${resultsDir}' fell back to polling because native fs.watch is unavailable (${getErrorCode(reason) ?? "unknown error"}).`,
|
|
244
|
-
);
|
|
306
|
+
console.error(`Subagent result watcher for '${resultsDir}' fell back to polling because native fs.watch is unavailable (${errorCode(reason) ?? "unknown error"}).`);
|
|
245
307
|
primeExistingResults();
|
|
246
308
|
state.watcherRestartTimer = timers.setInterval(primeExistingResults, POLL_INTERVAL_MS);
|
|
247
309
|
state.watcherRestartTimer.unref?.();
|
|
@@ -255,10 +317,7 @@ export function createResultWatcher(
|
|
|
255
317
|
fsApi.mkdirSync(resultsDir, { recursive: true });
|
|
256
318
|
startResultWatcher();
|
|
257
319
|
} catch (error) {
|
|
258
|
-
if (
|
|
259
|
-
startPollingFallback(error);
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
320
|
+
if (shouldPoll(error)) return startPolling(error);
|
|
262
321
|
console.error(`Failed to restart subagent result watcher for '${resultsDir}':`, error);
|
|
263
322
|
scheduleRestart();
|
|
264
323
|
}
|
|
@@ -268,6 +327,9 @@ export function createResultWatcher(
|
|
|
268
327
|
|
|
269
328
|
const startResultWatcher = () => {
|
|
270
329
|
if (state.watcher) return;
|
|
330
|
+
activeSessionId = state.currentSessionId;
|
|
331
|
+
deliveryActive = true;
|
|
332
|
+
deliveryEpoch += 1;
|
|
271
333
|
if (state.watcherRestartTimer) {
|
|
272
334
|
timers.clearTimeout(state.watcherRestartTimer);
|
|
273
335
|
timers.clearInterval(state.watcherRestartTimer);
|
|
@@ -275,17 +337,13 @@ export function createResultWatcher(
|
|
|
275
337
|
}
|
|
276
338
|
try {
|
|
277
339
|
const watchDir = resolveWatchPath(resultsDir, fsApi.realpathSync.native);
|
|
278
|
-
state.watcher = fsApi.watch(watchDir, (
|
|
279
|
-
if (
|
|
340
|
+
state.watcher = fsApi.watch(watchDir, (event, file) => {
|
|
341
|
+
if (event !== "rename" || !file) return;
|
|
280
342
|
const fileName = file.toString();
|
|
281
|
-
if (
|
|
282
|
-
state.resultFileCoalescer.schedule(fileName);
|
|
343
|
+
if (fileName.endsWith(".json")) scheduleResult(fileName, true);
|
|
283
344
|
});
|
|
284
345
|
state.watcher.on("error", (error) => {
|
|
285
|
-
if (
|
|
286
|
-
startPollingFallback(error);
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
346
|
+
if (shouldPoll(error)) return startPolling(error);
|
|
289
347
|
console.error(`Subagent result watcher failed for '${resultsDir}':`, error);
|
|
290
348
|
state.watcher?.close();
|
|
291
349
|
state.watcher = null;
|
|
@@ -293,10 +351,7 @@ export function createResultWatcher(
|
|
|
293
351
|
});
|
|
294
352
|
state.watcher.unref?.();
|
|
295
353
|
} catch (error) {
|
|
296
|
-
if (
|
|
297
|
-
startPollingFallback(error);
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
354
|
+
if (shouldPoll(error)) return startPolling(error);
|
|
300
355
|
console.error(`Failed to start subagent result watcher for '${resultsDir}':`, error);
|
|
301
356
|
state.watcher = null;
|
|
302
357
|
scheduleRestart();
|
|
@@ -304,6 +359,9 @@ export function createResultWatcher(
|
|
|
304
359
|
};
|
|
305
360
|
|
|
306
361
|
const stopResultWatcher = () => {
|
|
362
|
+
deliveryActive = false;
|
|
363
|
+
activeSessionId = null;
|
|
364
|
+
deliveryEpoch += 1;
|
|
307
365
|
state.watcher?.close();
|
|
308
366
|
state.watcher = null;
|
|
309
367
|
if (state.watcherRestartTimer) {
|
|
@@ -312,6 +370,8 @@ export function createResultWatcher(
|
|
|
312
370
|
}
|
|
313
371
|
state.watcherRestartTimer = null;
|
|
314
372
|
state.resultFileCoalescer.clear();
|
|
373
|
+
pendingTriggerTurn.clear();
|
|
374
|
+
processing.clear();
|
|
315
375
|
};
|
|
316
376
|
|
|
317
377
|
return { startResultWatcher, primeExistingResults, stopResultWatcher };
|
|
@@ -366,6 +366,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
366
366
|
status.turnBudget ? `Turn budget: ${status.turnBudget.turnCount}/${status.turnBudget.maxTurns}+${status.turnBudget.graceTurns} (${status.turnBudget.outcome})` : undefined,
|
|
367
367
|
`Dir: ${asyncDir}`,
|
|
368
368
|
outputPath ? `Output: ${outputPath}` : undefined,
|
|
369
|
+
status.parallelHandoff ? `Parallel handoff: ${status.parallelHandoff.path}` : undefined,
|
|
369
370
|
reconciliation.message ? `Diagnosis: ${reconciliation.message}` : undefined,
|
|
370
371
|
reconciliation.resultPath && fs.existsSync(reconciliation.resultPath) ? `Result: ${reconciliation.resultPath}` : undefined,
|
|
371
372
|
].filter((line): line is string => Boolean(line));
|
|
@@ -408,7 +409,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
408
409
|
if (resultPath) {
|
|
409
410
|
try {
|
|
410
411
|
const raw = fs.readFileSync(resultPath, "utf-8");
|
|
411
|
-
const data = JSON.parse(raw) as { id?: string; runId?: string; agent?: string; success?: boolean; summary?: string; output?: string; exitCode?: number; state?: string; sessionFile?: string; results?: Array<{ agent?: string; output?: string; summary?: string; sessionFile?: string; state?: string; success?: boolean; exitCode?: number | null }> };
|
|
412
|
+
const data = JSON.parse(raw) as { id?: string; runId?: string; agent?: string; success?: boolean; summary?: string; output?: string; exitCode?: number; state?: string; sessionFile?: string; parallelHandoff?: { path?: string }; results?: Array<{ agent?: string; output?: string; summary?: string; sessionFile?: string; state?: string; success?: boolean; exitCode?: number | null }> };
|
|
412
413
|
if (params.view === "transcript") {
|
|
413
414
|
try {
|
|
414
415
|
return { content: [{ type: "text", text: formatAsyncResultTranscript(data, resultPath, { index: params.index, lines: params.lines }) }], details: { mode: "single", results: [] } };
|
|
@@ -420,6 +421,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
420
421
|
const status = data.state === "stopped" ? "stopped" : data.success ? "complete" : data.state === "paused" || data.exitCode === 0 ? "paused" : "failed";
|
|
421
422
|
const runId = data.runId ?? data.id ?? resolvedId;
|
|
422
423
|
const lines = [`Run: ${runId}`, `State: ${status}`, `Result: ${resultPath}`];
|
|
424
|
+
if (data.parallelHandoff?.path) lines.push(`Parallel handoff: ${data.parallelHandoff.path}`);
|
|
423
425
|
const children = Array.isArray(data.results) ? data.results : data.agent ? [{ agent: data.agent, sessionFile: data.sessionFile }] : [];
|
|
424
426
|
lines.push(formatResumeGuidance(runId, children, data.sessionFile, { stopped: status === "stopped" }));
|
|
425
427
|
if (data.summary) lines.push("", data.summary);
|