pi-subagents 0.34.0 → 0.35.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 +78 -9
- package/README.md +213 -32
- package/index.ts +1 -0
- package/install.mjs +1 -1
- package/package.json +23 -8
- package/prompts/review-loop.md +3 -1
- package/skills/pi-subagents/SKILL.md +87 -25
- package/src/agents/agent-management.ts +82 -15
- package/src/agents/agent-serializer.ts +19 -0
- package/src/agents/agents.ts +91 -49
- package/src/agents/frontmatter.ts +67 -13
- package/src/agents/skills.ts +25 -12
- package/src/api/background-work.ts +197 -0
- package/src/api/delegation.ts +158 -0
- package/src/extension/chain-validation.ts +165 -0
- package/src/extension/doctor.ts +15 -0
- package/src/extension/fanout-child.ts +3 -1
- package/src/extension/index.ts +65 -124
- package/src/extension/rpc.ts +10 -2
- package/src/extension/schemas.ts +18 -14
- package/src/extension/steering-notices.ts +35 -0
- package/src/extension/tool-description.ts +20 -9
- package/src/intercom/intercom-bridge.ts +3 -2
- package/src/intercom/native-supervisor-channel.ts +9 -1
- package/src/intercom/result-intercom.ts +4 -0
- package/src/runs/background/async-execution.ts +293 -44
- package/src/runs/background/async-job-tracker.ts +56 -9
- package/src/runs/background/async-resume.ts +159 -52
- package/src/runs/background/async-status.ts +25 -18
- package/src/runs/background/auto-drain.ts +67 -0
- package/src/runs/background/chain-root-attachment.ts +16 -8
- package/src/runs/background/control-channel.ts +260 -13
- package/src/runs/background/fleet-view.ts +23 -2
- package/src/runs/background/notify.ts +79 -10
- package/src/runs/background/result-watcher.ts +12 -9
- package/src/runs/background/run-id-resolver.ts +14 -2
- package/src/runs/background/run-status.ts +23 -15
- package/src/runs/background/scheduled-runs.ts +3 -0
- package/src/runs/background/stale-run-reconciler.ts +32 -10
- package/src/runs/background/steering.ts +237 -0
- package/src/runs/background/subagent-runner.ts +898 -236
- package/src/runs/background/subagent-wait.ts +484 -0
- package/src/runs/background/top-level-async.ts +2 -1
- package/src/runs/background/wait-config.ts +36 -0
- package/src/runs/background/wait-tool.ts +26 -0
- package/src/runs/foreground/async-steering-action.ts +230 -0
- package/src/runs/foreground/chain-clarify.ts +22 -6
- package/src/runs/foreground/chain-execution.ts +50 -32
- package/src/runs/foreground/execution.ts +308 -94
- package/src/runs/foreground/subagent-executor.ts +592 -268
- package/src/runs/shared/acceptance.ts +355 -97
- package/src/runs/shared/child-protocol.ts +121 -0
- package/src/runs/shared/completion-guard.ts +8 -127
- package/src/runs/shared/dynamic-fanout.ts +6 -4
- package/src/runs/shared/model-fallback.ts +36 -0
- package/src/runs/shared/nested-events.ts +9 -4
- package/src/runs/shared/nested-render.ts +4 -1
- package/src/runs/shared/parallel-utils.ts +7 -0
- package/src/runs/shared/pi-args.ts +34 -7
- package/src/runs/shared/pi-spawn.ts +18 -12
- package/src/runs/shared/session-lease.ts +279 -0
- package/src/runs/shared/single-output.ts +61 -6
- package/src/runs/shared/spawn-budget.ts +128 -0
- package/src/runs/shared/subagent-control.ts +10 -6
- package/src/runs/shared/subagent-prompt-runtime.ts +127 -26
- package/src/runs/shared/task-intent.ts +176 -0
- package/src/runs/shared/tool-availability.ts +65 -0
- package/src/runs/shared/turn-budget.ts +49 -4
- package/src/shared/atomic-json.ts +4 -1
- package/src/shared/fork-context.ts +28 -3
- package/src/shared/model-info.ts +7 -4
- package/src/shared/status-format.ts +7 -1
- package/src/shared/types.ts +203 -25
- package/src/shared/utils.ts +35 -7
- package/src/slash/delegation-adapters.ts +457 -0
- package/src/slash/delegation-request.ts +103 -0
- package/src/slash/prompt-template-bridge.ts +167 -344
- package/src/slash/slash-commands.ts +239 -6
- package/src/slash/subagents-admin.ts +428 -0
- package/src/slash/subagents-editor.ts +86 -0
- package/src/tui/fleet.ts +405 -0
- package/src/tui/render.ts +90 -16
- package/src/watchdog/change-signature.ts +127 -0
- package/src/watchdog/child-status.ts +205 -0
- package/src/watchdog/emission-guard.ts +123 -0
- package/src/watchdog/lsp-diagnostics.ts +532 -0
- package/src/watchdog/model-selection.ts +167 -0
- package/src/watchdog/register-child.ts +117 -0
- package/src/watchdog/register-main.ts +433 -0
- package/src/watchdog/render.ts +54 -0
- package/src/watchdog/review.ts +293 -0
- package/src/watchdog/runtime.ts +712 -0
- package/src/watchdog/settings.ts +528 -0
- package/src/watchdog/tool-actions.ts +155 -0
- package/src/watchdog/turn-delta.ts +161 -0
- package/src/watchdog/types.ts +188 -0
- package/src/watchdog/warning-format.ts +73 -0
- package/src/runs/background/wait.ts +0 -394
|
@@ -19,6 +19,7 @@ import * as fs from "node:fs";
|
|
|
19
19
|
import * as path from "node:path";
|
|
20
20
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
21
21
|
import { POLL_INTERVAL_MS } from "../../shared/types.ts";
|
|
22
|
+
import { resolveWatchPath } from "../../shared/utils.ts";
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Opportunistic fast-path interrupt signal. On Unix `SIGUSR2` is trapped by the
|
|
@@ -27,7 +28,7 @@ import { POLL_INTERVAL_MS } from "../../shared/types.ts";
|
|
|
27
28
|
*/
|
|
28
29
|
export const INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
|
|
29
30
|
|
|
30
|
-
export type ControlChannelFs = Pick<typeof fs, "mkdirSync" | "existsSync" | "rmSync" | "watch" | "readdirSync" | "readFileSync">;
|
|
31
|
+
export type ControlChannelFs = Pick<typeof fs, "mkdirSync" | "existsSync" | "rmSync" | "watch" | "readdirSync" | "readFileSync" | "realpathSync">;
|
|
31
32
|
export type ControlChannelTimers = { setInterval: typeof setInterval; clearInterval: typeof clearInterval };
|
|
32
33
|
type KillFn = (pid: number, signal?: NodeJS.Signals | 0) => unknown;
|
|
33
34
|
|
|
@@ -45,17 +46,49 @@ export interface TimeoutRequest {
|
|
|
45
46
|
reason?: string;
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
export interface StopRequest {
|
|
50
|
+
type: "stop";
|
|
51
|
+
ts?: number;
|
|
52
|
+
source?: string;
|
|
53
|
+
reason?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
48
56
|
export interface SteerRequest {
|
|
49
57
|
type: "steer";
|
|
50
58
|
id: string;
|
|
51
59
|
ts: number;
|
|
52
60
|
message: string;
|
|
53
61
|
targetIndex?: number;
|
|
62
|
+
targetIndexes?: number[];
|
|
54
63
|
source?: string;
|
|
55
64
|
}
|
|
56
65
|
|
|
66
|
+
export interface SteerCapability {
|
|
67
|
+
type: "steer-capability";
|
|
68
|
+
protocolVersion: 1;
|
|
69
|
+
index: number;
|
|
70
|
+
pid: number;
|
|
71
|
+
readyAt: number;
|
|
72
|
+
supported: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface SteerAck {
|
|
76
|
+
type: "steer-ack";
|
|
77
|
+
protocolVersion: 1;
|
|
78
|
+
requestId: string;
|
|
79
|
+
index: number;
|
|
80
|
+
ts: number;
|
|
81
|
+
state: "delivered" | "failed";
|
|
82
|
+
message: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
57
85
|
const STEER_REQUESTS_DIR = "steer-requests";
|
|
58
86
|
const STEER_TARGETS_DIR = "steer-targets";
|
|
87
|
+
const STEER_CAPABILITIES_DIR = "steer-capabilities";
|
|
88
|
+
const STEER_ACKS_DIR = "steer-acks";
|
|
89
|
+
const STEER_INBOX_CLOSED_FILE = "steer-inbox-closed.json";
|
|
90
|
+
const MAX_STEER_MESSAGE_BYTES = 128 * 1024;
|
|
91
|
+
const MAX_STEER_REQUEST_ID_LENGTH = 256;
|
|
59
92
|
|
|
60
93
|
/** Control inbox directory inside an async run dir. */
|
|
61
94
|
export function controlInboxDir(asyncDir: string): string {
|
|
@@ -72,26 +105,118 @@ export function timeoutRequestPath(asyncDir: string): string {
|
|
|
72
105
|
return path.join(controlInboxDir(asyncDir), "timeout.json");
|
|
73
106
|
}
|
|
74
107
|
|
|
108
|
+
/** Path of the portable manual stop request file. */
|
|
109
|
+
export function stopRequestPath(asyncDir: string): string {
|
|
110
|
+
return path.join(controlInboxDir(asyncDir), "stop.json");
|
|
111
|
+
}
|
|
112
|
+
|
|
75
113
|
/** Directory of parent-to-runner steering requests. */
|
|
76
114
|
export function steerRequestsDir(asyncDir: string): string {
|
|
77
115
|
return path.join(controlInboxDir(asyncDir), STEER_REQUESTS_DIR);
|
|
78
116
|
}
|
|
79
117
|
|
|
118
|
+
export function steerInboxClosedPath(asyncDir: string): string {
|
|
119
|
+
return path.join(controlInboxDir(asyncDir), STEER_INBOX_CLOSED_FILE);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function closeSteerInbox(asyncDir: string, state: string): void {
|
|
123
|
+
writeAtomicJson(steerInboxClosedPath(asyncDir), { version: 1, closedAt: Date.now(), state });
|
|
124
|
+
}
|
|
125
|
+
|
|
80
126
|
/** Per-child inbox consumed by the child prompt runtime inside the Pi process. */
|
|
81
127
|
export function stepSteerInboxDir(asyncDir: string, index: number): string {
|
|
128
|
+
assertChildIndex(index);
|
|
82
129
|
return path.join(controlInboxDir(asyncDir), STEER_TARGETS_DIR, String(index));
|
|
83
130
|
}
|
|
84
131
|
|
|
132
|
+
export function steerCapabilitiesDir(asyncDir: string): string {
|
|
133
|
+
return path.join(controlInboxDir(asyncDir), STEER_CAPABILITIES_DIR);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function steerCapabilityPath(asyncDir: string, index: number): string {
|
|
137
|
+
assertChildIndex(index);
|
|
138
|
+
return path.join(steerCapabilitiesDir(asyncDir), `${index}.json`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function steerAcksDir(asyncDir: string, index: number): string {
|
|
142
|
+
assertChildIndex(index);
|
|
143
|
+
return path.join(controlInboxDir(asyncDir), STEER_ACKS_DIR, String(index));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function steerAckFileName(requestId: string): string {
|
|
147
|
+
return `${Buffer.from(requestId).toString("base64url")}.json`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function steerAckPathFromDir(dir: string, requestId: string): string {
|
|
151
|
+
if (!/^[^\s]+$/.test(requestId) || requestId.length > 256) throw new Error("steer acknowledgment requestId is invalid.");
|
|
152
|
+
return path.join(dir, steerAckFileName(requestId));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function assertChildIndex(index: number): void {
|
|
156
|
+
if (!Number.isInteger(index) || index < 0 || index > 1_000_000) throw new Error("steer child index must be a non-negative integer.");
|
|
157
|
+
}
|
|
158
|
+
|
|
85
159
|
function steerRequestFileName(request: SteerRequest): string {
|
|
86
160
|
return `${String(request.ts).padStart(13, "0")}-${Buffer.from(request.id).toString("base64url")}.json`;
|
|
87
161
|
}
|
|
88
162
|
|
|
163
|
+
function validSteerRequest(request: Partial<SteerRequest>): request is SteerRequest {
|
|
164
|
+
return request.type === "steer"
|
|
165
|
+
&& typeof request.id === "string"
|
|
166
|
+
&& /^[^\s]+$/.test(request.id)
|
|
167
|
+
&& request.id.length <= MAX_STEER_REQUEST_ID_LENGTH
|
|
168
|
+
&& typeof request.ts === "number"
|
|
169
|
+
&& Number.isFinite(request.ts)
|
|
170
|
+
&& request.ts > 0
|
|
171
|
+
&& typeof request.message === "string"
|
|
172
|
+
&& Boolean(request.message.trim())
|
|
173
|
+
&& Buffer.byteLength(request.message, "utf8") <= MAX_STEER_MESSAGE_BYTES
|
|
174
|
+
&& (request.targetIndex === undefined || (Number.isInteger(request.targetIndex) && request.targetIndex >= 0 && request.targetIndex <= 1_000_000))
|
|
175
|
+
&& (request.targetIndexes === undefined || (
|
|
176
|
+
request.targetIndex === undefined
|
|
177
|
+
&& Array.isArray(request.targetIndexes)
|
|
178
|
+
&& request.targetIndexes.length > 0
|
|
179
|
+
&& request.targetIndexes.length <= 1_000
|
|
180
|
+
&& request.targetIndexes.every((index) => Number.isInteger(index) && index >= 0 && index <= 1_000_000)
|
|
181
|
+
&& new Set(request.targetIndexes).size === request.targetIndexes.length
|
|
182
|
+
))
|
|
183
|
+
&& (request.source === undefined || (typeof request.source === "string" && Boolean(request.source.trim()) && request.source.length <= 256));
|
|
184
|
+
}
|
|
185
|
+
|
|
89
186
|
export function writeSteerRequestToDir(dir: string, request: SteerRequest): string {
|
|
187
|
+
if (!validSteerRequest(request)) throw new Error("steer request is malformed or exceeds transport limits.");
|
|
90
188
|
const requestPath = path.join(dir, steerRequestFileName(request));
|
|
91
189
|
writeAtomicJson(requestPath, request);
|
|
92
190
|
return requestPath;
|
|
93
191
|
}
|
|
94
192
|
|
|
193
|
+
export function writeSteerCapabilityAt(filePath: string, capability: Omit<SteerCapability, "type" | "protocolVersion">): string {
|
|
194
|
+
assertChildIndex(capability.index);
|
|
195
|
+
if (!Number.isInteger(capability.pid) || capability.pid <= 0) throw new Error("steer capability pid must be a positive integer.");
|
|
196
|
+
if (!Number.isFinite(capability.readyAt) || capability.readyAt <= 0) throw new Error("steer capability readyAt must be a finite timestamp.");
|
|
197
|
+
const record: SteerCapability = { type: "steer-capability", protocolVersion: 1, ...capability };
|
|
198
|
+
writeAtomicJson(filePath, record);
|
|
199
|
+
return filePath;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function writeSteerCapability(asyncDir: string, capability: Omit<SteerCapability, "type" | "protocolVersion">): string {
|
|
203
|
+
return writeSteerCapabilityAt(steerCapabilityPath(asyncDir, capability.index), capability);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function writeSteerAckAt(filePath: string, ack: Omit<SteerAck, "type" | "protocolVersion">): string {
|
|
207
|
+
assertChildIndex(ack.index);
|
|
208
|
+
if (!/^[^\s]+$/.test(ack.requestId) || ack.requestId.length > 256) throw new Error("steer acknowledgment requestId is invalid.");
|
|
209
|
+
if (!Number.isFinite(ack.ts) || ack.ts <= 0) throw new Error("steer acknowledgment ts must be a finite timestamp.");
|
|
210
|
+
if (!ack.message.trim() || ack.message.length > 1000) throw new Error("steer acknowledgment message is invalid.");
|
|
211
|
+
const record: SteerAck = { type: "steer-ack", protocolVersion: 1, ...ack, message: ack.message.trim() };
|
|
212
|
+
writeAtomicJson(filePath, record);
|
|
213
|
+
return filePath;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function writeSteerAck(asyncDir: string, ack: Omit<SteerAck, "type" | "protocolVersion">): string {
|
|
217
|
+
return writeSteerAckAt(path.join(steerAcksDir(asyncDir, ack.index), steerAckFileName(ack.requestId)), ack);
|
|
218
|
+
}
|
|
219
|
+
|
|
95
220
|
/**
|
|
96
221
|
* Parent side: drop a portable interrupt request the runner's inbox watcher will
|
|
97
222
|
* pick up regardless of OS. Written atomically (temp + rename), dir auto-created.
|
|
@@ -118,46 +243,137 @@ export function requestAsyncTimeout(
|
|
|
118
243
|
return requestPath;
|
|
119
244
|
}
|
|
120
245
|
|
|
246
|
+
export function requestAsyncStop(
|
|
247
|
+
asyncDir: string,
|
|
248
|
+
payload: Omit<StopRequest, "type"> = {},
|
|
249
|
+
deps: { now?: () => number } = {},
|
|
250
|
+
): string {
|
|
251
|
+
const requestPath = stopRequestPath(asyncDir);
|
|
252
|
+
const request: StopRequest = { ...payload, ts: payload.ts ?? deps.now?.() ?? Date.now(), type: "stop" };
|
|
253
|
+
writeAtomicJson(requestPath, request);
|
|
254
|
+
return requestPath;
|
|
255
|
+
}
|
|
256
|
+
|
|
121
257
|
export function requestAsyncSteer(
|
|
122
258
|
asyncDir: string,
|
|
123
|
-
payload: { message: string; targetIndex?: number; source?: string; id?: string; ts?: number },
|
|
259
|
+
payload: { message: string; targetIndex?: number; targetIndexes?: number[]; source?: string; id?: string; ts?: number },
|
|
124
260
|
deps: { now?: () => number; randomId?: () => string } = {},
|
|
125
261
|
): string {
|
|
126
262
|
const message = payload.message.trim();
|
|
127
263
|
if (!message) throw new Error("steer message must not be empty.");
|
|
128
|
-
if (
|
|
129
|
-
|
|
264
|
+
if (Buffer.byteLength(message, "utf8") > MAX_STEER_MESSAGE_BYTES) throw new Error(`steer message exceeds ${MAX_STEER_MESSAGE_BYTES} UTF-8 bytes.`);
|
|
265
|
+
if (payload.targetIndex !== undefined && (!Number.isInteger(payload.targetIndex) || payload.targetIndex < 0 || payload.targetIndex > 1_000_000)) {
|
|
266
|
+
throw new Error("steer targetIndex must be an integer between 0 and 1000000.");
|
|
130
267
|
}
|
|
268
|
+
if (payload.targetIndexes !== undefined && (
|
|
269
|
+
!Array.isArray(payload.targetIndexes)
|
|
270
|
+
|| payload.targetIndex !== undefined
|
|
271
|
+
|| payload.targetIndexes.length === 0
|
|
272
|
+
|| payload.targetIndexes.length > 1_000
|
|
273
|
+
|| payload.targetIndexes.some((index) => !Number.isInteger(index) || index < 0 || index > 1_000_000)
|
|
274
|
+
|| new Set(payload.targetIndexes).size !== payload.targetIndexes.length
|
|
275
|
+
)) {
|
|
276
|
+
throw new Error("steer targetIndexes must contain 1-1000 unique non-negative integers and cannot be combined with targetIndex.");
|
|
277
|
+
}
|
|
278
|
+
const closedPath = steerInboxClosedPath(asyncDir);
|
|
279
|
+
if (fs.existsSync(closedPath)) throw new Error("Async run no longer accepts steering requests.");
|
|
131
280
|
const request: SteerRequest = {
|
|
132
281
|
type: "steer",
|
|
133
282
|
id: payload.id ?? deps.randomId?.() ?? randomUUID(),
|
|
134
283
|
ts: payload.ts ?? deps.now?.() ?? Date.now(),
|
|
135
284
|
message,
|
|
136
285
|
...(payload.targetIndex !== undefined ? { targetIndex: payload.targetIndex } : {}),
|
|
286
|
+
...(payload.targetIndexes !== undefined ? { targetIndexes: [...payload.targetIndexes] } : {}),
|
|
137
287
|
...(payload.source ? { source: payload.source } : {}),
|
|
138
288
|
};
|
|
139
|
-
|
|
289
|
+
const requestPath = writeSteerRequestToDir(steerRequestsDir(asyncDir), request);
|
|
290
|
+
if (fs.existsSync(closedPath)) {
|
|
291
|
+
fs.rmSync(requestPath, { force: true });
|
|
292
|
+
throw new Error("Async run stopped accepting steering before the request was committed.");
|
|
293
|
+
}
|
|
294
|
+
return requestPath;
|
|
140
295
|
}
|
|
141
296
|
|
|
142
297
|
export function enqueueStepSteer(asyncDir: string, index: number, request: SteerRequest): string {
|
|
143
|
-
|
|
144
|
-
|
|
298
|
+
assertChildIndex(index);
|
|
299
|
+
const { targetIndexes: _targetIndexes, ...singleTargetRequest } = request;
|
|
300
|
+
return writeSteerRequestToDir(stepSteerInboxDir(asyncDir, index), { ...singleTargetRequest, targetIndex: index, type: "steer" });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function parseSteerCapability(raw: unknown): SteerCapability | undefined {
|
|
304
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
305
|
+
const input = raw as Partial<SteerCapability>;
|
|
306
|
+
if (input.type !== "steer-capability" || input.protocolVersion !== 1) return undefined;
|
|
307
|
+
if (!Number.isInteger(input.index) || input.index < 0 || input.index > 1_000_000) return undefined;
|
|
308
|
+
if (!Number.isInteger(input.pid) || input.pid <= 0 || !Number.isFinite(input.readyAt) || input.readyAt <= 0 || typeof input.supported !== "boolean") return undefined;
|
|
309
|
+
return { type: "steer-capability", protocolVersion: 1, index: input.index, pid: input.pid, readyAt: input.readyAt, supported: input.supported };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function parseSteerAck(raw: unknown): SteerAck | undefined {
|
|
313
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
314
|
+
const input = raw as Partial<SteerAck>;
|
|
315
|
+
if (input.type !== "steer-ack" || input.protocolVersion !== 1 || typeof input.requestId !== "string" || !/^[^\s]+$/.test(input.requestId) || input.requestId.length > 256) return undefined;
|
|
316
|
+
if (!Number.isInteger(input.index) || input.index < 0 || input.index > 1_000_000 || !Number.isFinite(input.ts) || input.ts <= 0) return undefined;
|
|
317
|
+
if (input.state !== "delivered" && input.state !== "failed") return undefined;
|
|
318
|
+
if (typeof input.message !== "string" || !input.message.trim() || input.message.length > 1000) return undefined;
|
|
319
|
+
return { type: "steer-ack", protocolVersion: 1, requestId: input.requestId, index: input.index, ts: input.ts, state: input.state, message: input.message.trim() };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function readSteerCapability(asyncDir: string, index: number): SteerCapability | undefined {
|
|
323
|
+
try {
|
|
324
|
+
return parseSteerCapability(JSON.parse(fs.readFileSync(steerCapabilityPath(asyncDir, index), "utf-8")));
|
|
325
|
+
} catch {
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function consumeSteerCapabilities(asyncDir: string, fsImpl: Pick<typeof fs, "existsSync" | "readdirSync" | "readFileSync"> = fs): SteerCapability[] {
|
|
331
|
+
const dir = steerCapabilitiesDir(asyncDir);
|
|
332
|
+
if (!fsImpl.existsSync(dir)) return [];
|
|
333
|
+
const capabilities: SteerCapability[] = [];
|
|
334
|
+
for (const entry of fsImpl.readdirSync(dir).filter((name) => /^\d+\.json$/.test(name)).sort()) {
|
|
335
|
+
try {
|
|
336
|
+
const capability = parseSteerCapability(JSON.parse(fsImpl.readFileSync(path.join(dir, entry), "utf-8")));
|
|
337
|
+
if (capability) capabilities.push(capability);
|
|
338
|
+
} catch {
|
|
339
|
+
// A partially written or malformed capability is ignored until a valid one arrives.
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return capabilities;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function consumeSteerAcks(asyncDir: string, fsImpl: Pick<typeof fs, "existsSync" | "readdirSync" | "readFileSync" | "rmSync"> = fs): SteerAck[] {
|
|
346
|
+
const root = path.join(controlInboxDir(asyncDir), STEER_ACKS_DIR);
|
|
347
|
+
if (!fsImpl.existsSync(root)) return [];
|
|
348
|
+
const acks: SteerAck[] = [];
|
|
349
|
+
let indexNames: string[];
|
|
350
|
+
try { indexNames = fsImpl.readdirSync(root).filter((name) => /^\d+$/.test(name)); } catch { return []; }
|
|
351
|
+
for (const indexName of indexNames) {
|
|
352
|
+
const dir = path.join(root, indexName);
|
|
353
|
+
let entries: string[];
|
|
354
|
+
try { entries = fsImpl.readdirSync(dir).filter((name) => name.endsWith(".json")).sort(); } catch { continue; }
|
|
355
|
+
for (const entry of entries) {
|
|
356
|
+
const target = path.join(dir, entry);
|
|
357
|
+
let ack: SteerAck | undefined;
|
|
358
|
+
try { ack = parseSteerAck(JSON.parse(fsImpl.readFileSync(target, "utf-8"))); } catch { ack = undefined; }
|
|
359
|
+
try { fsImpl.rmSync(target, { force: true }); } catch { continue; }
|
|
360
|
+
if (ack) acks.push(ack);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return acks;
|
|
145
364
|
}
|
|
146
365
|
|
|
147
366
|
function parseSteerRequest(raw: unknown): SteerRequest | undefined {
|
|
148
367
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
149
368
|
const input = raw as Partial<SteerRequest>;
|
|
150
|
-
if (input
|
|
151
|
-
if (typeof input.id !== "string" || !input.id.trim()) return undefined;
|
|
152
|
-
if (typeof input.ts !== "number" || !Number.isFinite(input.ts)) return undefined;
|
|
153
|
-
if (typeof input.message !== "string" || !input.message.trim()) return undefined;
|
|
154
|
-
if (input.targetIndex !== undefined && (!Number.isInteger(input.targetIndex) || input.targetIndex < 0)) return undefined;
|
|
369
|
+
if (!validSteerRequest(input)) return undefined;
|
|
155
370
|
return {
|
|
156
371
|
type: "steer",
|
|
157
372
|
id: input.id.trim(),
|
|
158
373
|
ts: input.ts,
|
|
159
374
|
message: input.message.trim(),
|
|
160
375
|
...(input.targetIndex !== undefined ? { targetIndex: input.targetIndex } : {}),
|
|
376
|
+
...(input.targetIndexes !== undefined ? { targetIndexes: [...input.targetIndexes] } : {}),
|
|
161
377
|
...(typeof input.source === "string" && input.source.trim() ? { source: input.source } : {}),
|
|
162
378
|
};
|
|
163
379
|
}
|
|
@@ -220,6 +436,20 @@ export function consumeTimeoutRequest(
|
|
|
220
436
|
return true;
|
|
221
437
|
}
|
|
222
438
|
|
|
439
|
+
export function consumeStopRequest(
|
|
440
|
+
asyncDir: string,
|
|
441
|
+
fsImpl: Pick<typeof fs, "existsSync" | "rmSync"> = fs,
|
|
442
|
+
): boolean {
|
|
443
|
+
const requestPath = stopRequestPath(asyncDir);
|
|
444
|
+
if (!fsImpl.existsSync(requestPath)) return false;
|
|
445
|
+
try {
|
|
446
|
+
fsImpl.rmSync(requestPath, { force: true, recursive: true });
|
|
447
|
+
} catch {
|
|
448
|
+
// Already removed by a concurrent check — still counts as consumed.
|
|
449
|
+
}
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
|
|
223
453
|
/**
|
|
224
454
|
* Parent side: portable interrupt = authoritative file request + best-effort OS
|
|
225
455
|
* signal. The signal is only a latency optimization on Unix; ENOSYS on Windows
|
|
@@ -265,6 +495,17 @@ export function deliverTimeoutRequest(input: {
|
|
|
265
495
|
requestAsyncTimeout(input.asyncDir, input.source ? { source: input.source } : {}, { now: input.now });
|
|
266
496
|
}
|
|
267
497
|
|
|
498
|
+
export function deliverStopRequest(input: {
|
|
499
|
+
asyncDir: string;
|
|
500
|
+
pid?: number;
|
|
501
|
+
kill?: KillFn;
|
|
502
|
+
signal?: NodeJS.Signals;
|
|
503
|
+
now?: () => number;
|
|
504
|
+
source?: string;
|
|
505
|
+
}): void {
|
|
506
|
+
requestAsyncStop(input.asyncDir, input.source ? { source: input.source } : {}, { now: input.now });
|
|
507
|
+
}
|
|
508
|
+
|
|
268
509
|
/**
|
|
269
510
|
* Runner side: watch the control inbox and route interrupt requests into
|
|
270
511
|
* `onInterrupt`. Uses `fs.watch` when available plus an interval poll as a
|
|
@@ -276,7 +517,10 @@ export function watchAsyncControlInbox(
|
|
|
276
517
|
opts: {
|
|
277
518
|
onInterrupt: () => void;
|
|
278
519
|
onTimeout?: () => void;
|
|
520
|
+
onStop?: () => void;
|
|
279
521
|
onSteer?: (request: SteerRequest) => void;
|
|
522
|
+
onSteerCapability?: (capability: SteerCapability) => void;
|
|
523
|
+
onSteerAck?: (ack: SteerAck) => void;
|
|
280
524
|
pollIntervalMs?: number;
|
|
281
525
|
fs?: ControlChannelFs;
|
|
282
526
|
timers?: ControlChannelTimers;
|
|
@@ -295,9 +539,12 @@ export function watchAsyncControlInbox(
|
|
|
295
539
|
const check = (): void => {
|
|
296
540
|
if (disposed) return;
|
|
297
541
|
try {
|
|
542
|
+
if (consumeStopRequest(asyncDir, fsImpl)) opts.onStop?.();
|
|
298
543
|
if (consumeTimeoutRequest(asyncDir, fsImpl)) opts.onTimeout?.();
|
|
299
544
|
if (consumeInterruptRequest(asyncDir, fsImpl)) opts.onInterrupt();
|
|
300
545
|
for (const request of consumeSteerRequests(asyncDir, fsImpl)) opts.onSteer?.(request);
|
|
546
|
+
for (const capability of consumeSteerCapabilities(asyncDir, fsImpl)) opts.onSteerCapability?.(capability);
|
|
547
|
+
for (const ack of consumeSteerAcks(asyncDir, fsImpl)) opts.onSteerAck?.(ack);
|
|
301
548
|
} catch {
|
|
302
549
|
// Never let inbox errors crash the runner.
|
|
303
550
|
}
|
|
@@ -308,7 +555,7 @@ export function watchAsyncControlInbox(
|
|
|
308
555
|
|
|
309
556
|
let watcher: fs.FSWatcher | undefined;
|
|
310
557
|
try {
|
|
311
|
-
watcher = fsImpl.watch(dir, () => check());
|
|
558
|
+
watcher = fsImpl.watch(resolveWatchPath(dir, fsImpl.realpathSync.native), () => check());
|
|
312
559
|
watcher.on?.("error", () => {
|
|
313
560
|
// fs.watch can emit on transient FS errors; the interval poll keeps us live.
|
|
314
561
|
});
|
|
@@ -23,6 +23,7 @@ const MAX_TRANSCRIPT_LINES = 500;
|
|
|
23
23
|
const TRANSCRIPT_TAIL_BYTES = 256 * 1024;
|
|
24
24
|
|
|
25
25
|
type ForegroundControl = SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never;
|
|
26
|
+
type ForegroundRun = NonNullable<SubagentState["foregroundRuns"]> extends Map<string, infer T> ? T : never;
|
|
26
27
|
|
|
27
28
|
interface FleetViewParams {
|
|
28
29
|
lines?: number;
|
|
@@ -254,6 +255,20 @@ function formatForegroundFleetLines(controls: ForegroundControl[]): string[] {
|
|
|
254
255
|
return lines;
|
|
255
256
|
}
|
|
256
257
|
|
|
258
|
+
function formatDetachedForegroundFleetLines(runs: ForegroundRun[]): string[] {
|
|
259
|
+
if (runs.length === 0) return [];
|
|
260
|
+
const lines = ["Detached foreground runs:"];
|
|
261
|
+
const ordered = [...runs].sort((left, right) => right.updatedAt - left.updatedAt);
|
|
262
|
+
for (const run of ordered) {
|
|
263
|
+
const detachedChildren = run.children.filter((child) => child.status === "detached");
|
|
264
|
+
const childSummary = detachedChildren.map((child) => `${child.agent} #${child.index}`).join(", ");
|
|
265
|
+
lines.push(`- ${run.runId} | detached | ${run.mode}${childSummary ? ` | ${childSummary}` : ""}`);
|
|
266
|
+
lines.push(` status: subagent({ action: "status", id: "${run.runId}" })`);
|
|
267
|
+
lines.push(` recovery: reply to the supervisor request first, then wait with subagent_wait({ id: "${run.runId}" }); do not resume or launch a replacement while any child remains detached.`);
|
|
268
|
+
}
|
|
269
|
+
return lines;
|
|
270
|
+
}
|
|
271
|
+
|
|
257
272
|
function formatAsyncFleetLines(runs: AsyncRunSummary[]): string[] {
|
|
258
273
|
if (runs.length === 0) return [];
|
|
259
274
|
const lines = ["Async runs:"];
|
|
@@ -316,7 +331,11 @@ export function inspectSubagentFleet(_params: FleetViewParams, deps: FleetViewDe
|
|
|
316
331
|
}
|
|
317
332
|
|
|
318
333
|
const foregroundControls = deps.state ? [...deps.state.foregroundControls.values()] : [];
|
|
319
|
-
const
|
|
334
|
+
const activeForegroundIds = new Set(foregroundControls.map((control) => control.runId));
|
|
335
|
+
const detachedForegroundRuns = deps.state?.foregroundRuns
|
|
336
|
+
? [...deps.state.foregroundRuns.values()].filter((run) => run.sessionId === deps.state?.currentSessionId && !activeForegroundIds.has(run.runId) && run.children.some((child) => child.status === "detached"))
|
|
337
|
+
: [];
|
|
338
|
+
const total = foregroundControls.length + detachedForegroundRuns.length + asyncRuns.length;
|
|
320
339
|
if (total === 0) {
|
|
321
340
|
return {
|
|
322
341
|
content: [{ type: "text", text: "No active subagent fleet. Background runs that already finished are available through completion notifications or subagent({ action: \"status\", id: \"...\" })." }],
|
|
@@ -324,9 +343,11 @@ export function inspectSubagentFleet(_params: FleetViewParams, deps: FleetViewDe
|
|
|
324
343
|
};
|
|
325
344
|
}
|
|
326
345
|
|
|
327
|
-
const lines = [`Subagent fleet: ${total}
|
|
346
|
+
const lines = [`Subagent fleet: ${total} tracked`, ""];
|
|
328
347
|
const foregroundLines = formatForegroundFleetLines(foregroundControls);
|
|
329
348
|
if (foregroundLines.length) lines.push(...foregroundLines, "");
|
|
349
|
+
const detachedForegroundLines = formatDetachedForegroundFleetLines(detachedForegroundRuns);
|
|
350
|
+
if (detachedForegroundLines.length) lines.push(...detachedForegroundLines, "");
|
|
330
351
|
const asyncLines = formatAsyncFleetLines(asyncRuns);
|
|
331
352
|
if (asyncLines.length) lines.push(...asyncLines, "");
|
|
332
353
|
lines.push("Commands:");
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
createCompletionBatcher,
|
|
17
17
|
resolveCompletionBatchConfig,
|
|
18
18
|
} from "./completion-batcher.ts";
|
|
19
|
-
import { SUBAGENT_ASYNC_COMPLETE_EVENT, type SubagentState } from "../../shared/types.ts";
|
|
19
|
+
import { SUBAGENT_ASYNC_COMPLETE_EVENT, SUBAGENT_FOREGROUND_COMPLETE_EVENT, type SubagentState } from "../../shared/types.ts";
|
|
20
20
|
|
|
21
21
|
interface ChainStepResult {
|
|
22
22
|
agent: string;
|
|
@@ -27,6 +27,7 @@ interface ChainStepResult {
|
|
|
27
27
|
export interface SubagentNotifyDetails {
|
|
28
28
|
agent: string;
|
|
29
29
|
status: "completed" | "failed" | "paused";
|
|
30
|
+
source?: "async" | "foreground";
|
|
30
31
|
taskInfo?: string;
|
|
31
32
|
resultPreview: string;
|
|
32
33
|
durationMs?: number;
|
|
@@ -36,6 +37,7 @@ export interface SubagentNotifyDetails {
|
|
|
36
37
|
|
|
37
38
|
interface SubagentResult {
|
|
38
39
|
id: string | null;
|
|
40
|
+
source?: "async" | "foreground";
|
|
39
41
|
agent: string | null;
|
|
40
42
|
success: boolean;
|
|
41
43
|
summary: string;
|
|
@@ -72,8 +74,9 @@ function formatSessionLine(details: SubagentNotifyDetails): string | undefined {
|
|
|
72
74
|
|
|
73
75
|
export function formatSingleCompletion(details: SubagentNotifyDetails): string {
|
|
74
76
|
const sessionLine = formatSessionLine(details);
|
|
77
|
+
const taskKind = details.source === "foreground" ? "Detached foreground task" : "Background task";
|
|
75
78
|
return [
|
|
76
|
-
|
|
79
|
+
`${taskKind} ${details.status}: **${details.agent}**${details.taskInfo ?? ""}`,
|
|
77
80
|
"",
|
|
78
81
|
details.resultPreview.trim() ? details.resultPreview : "(no output)",
|
|
79
82
|
sessionLine ? "" : undefined,
|
|
@@ -83,6 +86,37 @@ export function formatSingleCompletion(details: SubagentNotifyDetails): string {
|
|
|
83
86
|
.join("\n");
|
|
84
87
|
}
|
|
85
88
|
|
|
89
|
+
export function parseSubagentNotifyContent(content: string): SubagentNotifyDetails | undefined {
|
|
90
|
+
const lines = content.split("\n");
|
|
91
|
+
const match = (lines[0] ?? "").match(/^(Background task|Detached foreground task) (completed|failed|paused): \*\*(.+?)\*\*(?:\s+(\([^)]*\)))?$/);
|
|
92
|
+
if (!match) return undefined;
|
|
93
|
+
const body = lines.slice(2);
|
|
94
|
+
let sessionIndex = -1;
|
|
95
|
+
for (let i = body.length - 1; i >= 1; i--) {
|
|
96
|
+
if (body[i - 1]?.trim() === "" && /^(Session|Session file|Session share error):\s+/.test(body[i]!)) {
|
|
97
|
+
sessionIndex = i;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const sessionLine = sessionIndex >= 0 ? body[sessionIndex] : undefined;
|
|
102
|
+
const resultPreview = (sessionIndex >= 0 ? body.slice(0, sessionIndex) : body).join("\n").trim() || "(no output)";
|
|
103
|
+
let sessionLabel: string | undefined;
|
|
104
|
+
let sessionValue: string | undefined;
|
|
105
|
+
if (sessionLine) {
|
|
106
|
+
const separator = sessionLine.indexOf(":");
|
|
107
|
+
sessionLabel = sessionLine.slice(0, separator).toLowerCase();
|
|
108
|
+
sessionValue = sessionLine.slice(separator + 1).trim();
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
agent: match[3]!,
|
|
112
|
+
status: match[2] as SubagentNotifyDetails["status"],
|
|
113
|
+
...(match[1] === "Detached foreground task" ? { source: "foreground" as const } : {}),
|
|
114
|
+
...(match[4] ? { taskInfo: match[4] } : {}),
|
|
115
|
+
resultPreview,
|
|
116
|
+
...(sessionLabel && sessionValue ? { sessionLabel, sessionValue } : {}),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
86
120
|
export function formatGroupedCompletion(details: SubagentNotifyDetails[]): string {
|
|
87
121
|
const header = `Background tasks completed (${details.length}): ${details.map((d) => `**${d.agent}**${d.taskInfo ?? ""}`).join(", ")}`;
|
|
88
122
|
const blocks: string[] = [header, ""];
|
|
@@ -147,6 +181,7 @@ export function buildCompletionDetails(result: SubagentResult): SubagentNotifyDe
|
|
|
147
181
|
return {
|
|
148
182
|
agent,
|
|
149
183
|
status,
|
|
184
|
+
...(result.source ? { source: result.source } : {}),
|
|
150
185
|
...(taskInfo ? { taskInfo } : {}),
|
|
151
186
|
resultPreview: summary,
|
|
152
187
|
...(typeof result.durationMs === "number" ? { durationMs: result.durationMs } : {}),
|
|
@@ -158,7 +193,7 @@ export default function registerSubagentNotify(
|
|
|
158
193
|
pi: ExtensionAPI,
|
|
159
194
|
state: Pick<SubagentState, "currentSessionId">,
|
|
160
195
|
options: RegisterSubagentNotifyOptions = {},
|
|
161
|
-
): void {
|
|
196
|
+
): () => void {
|
|
162
197
|
const unsubscribeStoreKey = "__pi_subagents_notify_unsubscribe__";
|
|
163
198
|
const batcherStoreKey = "__pi_subagents_notify_batcher__";
|
|
164
199
|
const globalStore = globalThis as Record<string, unknown>;
|
|
@@ -184,14 +219,10 @@ export default function registerSubagentNotify(
|
|
|
184
219
|
const nowFn = options.now ?? Date.now;
|
|
185
220
|
const batchConfig = resolveCompletionBatchConfig(options.batchConfig);
|
|
186
221
|
const batchers = new Map<string, CompletionBatcher<SubagentNotifyDetails>>();
|
|
187
|
-
|
|
188
|
-
dispose() {
|
|
189
|
-
for (const batcher of batchers.values()) batcher.dispose();
|
|
190
|
-
batchers.clear();
|
|
191
|
-
},
|
|
192
|
-
};
|
|
222
|
+
let disposed = false;
|
|
193
223
|
|
|
194
224
|
const handleComplete = (data: unknown) => {
|
|
225
|
+
if (disposed) return;
|
|
195
226
|
const result = data as SubagentResult;
|
|
196
227
|
if (typeof result.sessionId !== "string" || result.sessionId !== state.currentSessionId) return;
|
|
197
228
|
const now = nowFn();
|
|
@@ -199,6 +230,10 @@ export default function registerSubagentNotify(
|
|
|
199
230
|
if (markSeenWithTtl(seen, key, now, ttlMs)) return;
|
|
200
231
|
|
|
201
232
|
const details = buildCompletionDetails(result);
|
|
233
|
+
if (result.source === "foreground") {
|
|
234
|
+
sendCompletion(pi, [details]);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
202
237
|
const batchKey = completionBatchKey(result);
|
|
203
238
|
let batcher = batchers.get(batchKey);
|
|
204
239
|
if (!batcher) {
|
|
@@ -221,5 +256,39 @@ export default function registerSubagentNotify(
|
|
|
221
256
|
batcher.push(details);
|
|
222
257
|
};
|
|
223
258
|
|
|
224
|
-
|
|
259
|
+
const unsubscribers = [
|
|
260
|
+
pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, handleComplete),
|
|
261
|
+
pi.events.on(SUBAGENT_FOREGROUND_COMPLETE_EVENT, handleComplete),
|
|
262
|
+
].filter((unsubscribe): unsubscribe is () => void => typeof unsubscribe === "function");
|
|
263
|
+
const batcherRegistration = { dispose };
|
|
264
|
+
|
|
265
|
+
function dispose(): void {
|
|
266
|
+
if (disposed) return;
|
|
267
|
+
disposed = true;
|
|
268
|
+
for (const batcher of batchers.values()) {
|
|
269
|
+
try {
|
|
270
|
+
batcher.dispose();
|
|
271
|
+
} catch {
|
|
272
|
+
// Best effort cleanup must continue through every owned batcher.
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
batchers.clear();
|
|
276
|
+
for (const unsubscribe of unsubscribers) {
|
|
277
|
+
try {
|
|
278
|
+
unsubscribe();
|
|
279
|
+
} catch {
|
|
280
|
+
// Best effort cleanup must continue through every owned handler.
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (globalStore[unsubscribeStoreKey] === dispose) {
|
|
284
|
+
delete globalStore[unsubscribeStoreKey];
|
|
285
|
+
}
|
|
286
|
+
if (globalStore[batcherStoreKey] === batcherRegistration) {
|
|
287
|
+
delete globalStore[batcherStoreKey];
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
globalStore[unsubscribeStoreKey] = dispose;
|
|
292
|
+
globalStore[batcherStoreKey] = batcherRegistration;
|
|
293
|
+
return dispose;
|
|
225
294
|
}
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
resolveSubagentResultStatus,
|
|
18
18
|
} from "../../intercom/result-intercom.ts";
|
|
19
19
|
import { projectNestedRegistryForRoot, sanitizeSummary } from "../shared/nested-events.ts";
|
|
20
|
+
import { resolveWatchPath } from "../../shared/utils.ts";
|
|
20
21
|
|
|
21
22
|
const WATCHER_RESTART_DELAY_MS = 3000;
|
|
22
23
|
const POLL_INTERVAL_MS = 3000;
|
|
@@ -40,6 +41,8 @@ type ResultFileChild = {
|
|
|
40
41
|
output?: string;
|
|
41
42
|
error?: string;
|
|
42
43
|
success?: boolean;
|
|
44
|
+
state?: string;
|
|
45
|
+
stopped?: boolean;
|
|
43
46
|
sessionFile?: string;
|
|
44
47
|
artifactPaths?: { outputPath?: string };
|
|
45
48
|
intercomTarget?: string;
|
|
@@ -91,13 +94,6 @@ function shouldFallBackToPolling(error: unknown): boolean {
|
|
|
91
94
|
return code === "EMFILE" || code === "ENOSPC";
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
function resolveNativeWatchDir(fsApi: ResultWatcherFs, resultsDir: string): string {
|
|
95
|
-
try {
|
|
96
|
-
return fsApi.realpathSync.native(resultsDir);
|
|
97
|
-
} catch {
|
|
98
|
-
return resultsDir;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
97
|
|
|
102
98
|
export function createResultWatcher(
|
|
103
99
|
pi: { events: IntercomEventBus },
|
|
@@ -155,11 +151,18 @@ export function createResultWatcher(
|
|
|
155
151
|
: output;
|
|
156
152
|
const sessionPath = result.sessionFile ?? (resultChildren.length === 1 ? data.sessionFile : undefined);
|
|
157
153
|
const childNestedChildren = sanitizeNestedResultChildren(result.children, resultPath, `results[${index}].children`);
|
|
154
|
+
const childState = result.state === "paused" || result.state === "stopped"
|
|
155
|
+
? result.state
|
|
156
|
+
: result.stopped === true
|
|
157
|
+
? "stopped"
|
|
158
|
+
: data.state === "paused" || (!hasResultChildren && (data.state === "stopped" || typeof result.success !== "boolean"))
|
|
159
|
+
? data.state
|
|
160
|
+
: undefined;
|
|
158
161
|
return {
|
|
159
162
|
agent: result.agent ?? data.agent ?? `step-${index + 1}`,
|
|
160
163
|
status: resolveSubagentResultStatus({
|
|
161
164
|
success: result.success,
|
|
162
|
-
state:
|
|
165
|
+
state: childState,
|
|
163
166
|
}),
|
|
164
167
|
summary,
|
|
165
168
|
index,
|
|
@@ -271,7 +274,7 @@ export function createResultWatcher(
|
|
|
271
274
|
state.watcherRestartTimer = null;
|
|
272
275
|
}
|
|
273
276
|
try {
|
|
274
|
-
const watchDir =
|
|
277
|
+
const watchDir = resolveWatchPath(resultsDir, fsApi.realpathSync.native);
|
|
275
278
|
state.watcher = fsApi.watch(watchDir, (ev, file) => {
|
|
276
279
|
if (ev !== "rename" || !file) return;
|
|
277
280
|
const fileName = file.toString();
|