pi-subagents 0.35.1 → 0.37.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 +71 -0
- package/README.md +224 -31
- package/agents/advisor.md +73 -0
- package/agents/delegate.md +2 -0
- package/agents/worker.md +2 -0
- package/package.json +11 -13
- package/skills/pi-subagents/SKILL.md +42 -13
- package/src/agents/agent-management.ts +7 -1
- package/src/agents/agents.ts +121 -17
- package/src/api/capability-ceiling.ts +17 -0
- package/src/api/delegation.ts +127 -0
- package/src/api/preflight.ts +399 -0
- package/src/extension/config.ts +7 -1
- package/src/extension/index.ts +52 -38
- package/src/extension/rpc.ts +31 -2
- package/src/extension/schemas.ts +29 -3
- package/src/extension/tool-description.ts +4 -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 +158 -17
- package/src/runs/background/async-job-tracker.ts +4 -0
- package/src/runs/background/async-resume.ts +47 -8
- package/src/runs/background/async-status.ts +94 -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/process-terminal.ts +280 -0
- package/src/runs/background/result-watcher.ts +138 -78
- package/src/runs/background/run-status.ts +10 -2
- package/src/runs/background/scheduled-runs.ts +6 -1
- package/src/runs/background/stale-run-reconciler.ts +6 -0
- package/src/runs/background/subagent-runner.ts +406 -54
- 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 +181 -111
- package/src/runs/foreground/execution.ts +147 -47
- package/src/runs/foreground/foreground-control.ts +90 -0
- package/src/runs/foreground/subagent-executor.ts +426 -165
- package/src/runs/shared/acceptance.ts +94 -41
- package/src/runs/shared/agent-contract.ts +38 -0
- package/src/runs/shared/capability-ceiling.ts +177 -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 +5 -5
- package/src/runs/shared/long-running-guard.ts +4 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
- package/src/runs/shared/nested-events.ts +35 -3
- package/src/runs/shared/parallel-handoff.ts +154 -0
- package/src/runs/shared/parallel-utils.ts +11 -0
- package/src/runs/shared/pi-args.ts +148 -56
- package/src/runs/shared/run-history.ts +90 -5
- package/src/runs/shared/session-lease.ts +25 -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 +31 -19
- package/src/runs/shared/task-intent.ts +10 -5
- package/src/runs/shared/tool-availability.ts +21 -3
- 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/launch-contract.ts +123 -0
- package/src/shared/settings.ts +9 -1
- package/src/shared/types.ts +314 -28
- package/src/shared/utils.ts +17 -42
- package/src/slash/delegation-adapters.ts +153 -6
- 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 +15 -6
- 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 +17 -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 };
|
|
@@ -8,6 +8,7 @@ import { formatModelThinking } from "../../shared/formatters.ts";
|
|
|
8
8
|
import { formatActivityLabel } from "../../shared/status-format.ts";
|
|
9
9
|
import { ASYNC_DIR, RESULTS_DIR, type AsyncStatus, type Details, type ForegroundResumeRun, type NestedRunSummary, type SteeringStatus, type SubagentState } from "../../shared/types.ts";
|
|
10
10
|
import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
|
|
11
|
+
import { readProcessTerminal, sanitizeProcessTerminal } from "./process-terminal.ts";
|
|
11
12
|
import { resolveAsyncRunLocation } from "./async-resume.ts";
|
|
12
13
|
import { resolveSubagentRunId } from "./run-id-resolver.ts";
|
|
13
14
|
import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-groups.ts";
|
|
@@ -351,10 +352,15 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
351
352
|
const updated = status.lastUpdate ? new Date(status.lastUpdate).toISOString() : "n/a";
|
|
352
353
|
const statusActivityText = status.state === "running" ? formatActivityLabel(status.lastActivityAt, status.activityState) : undefined;
|
|
353
354
|
const steeringText = formatSteeringSummary(status);
|
|
355
|
+
const processTerminal = readProcessTerminal(asyncDir, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId })
|
|
356
|
+
?? sanitizeProcessTerminal(status.processTerminal, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId }, path.join(asyncDir, "status.json"));
|
|
354
357
|
|
|
355
358
|
const lines = [
|
|
356
359
|
`Run: ${status.runId}`,
|
|
357
360
|
`State: ${status.state}`,
|
|
361
|
+
processTerminal ? `Process terminal: ${processTerminal.state}${processTerminal.reason ? ` (${processTerminal.reason})` : ""}` : undefined,
|
|
362
|
+
status.capabilityCeiling ? `Capability ceiling: ${status.capabilityCeiling.allowedTools === undefined ? "names unrestricted" : status.capabilityCeiling.allowedTools.length === 0 ? "none" : status.capabilityCeiling.allowedTools.join(", ")}\nExtensions denied: ${status.capabilityCeiling.denyExtensions ? "yes" : "no"} (sources: ${status.capabilityCeiling.sources.join(", ")})` : undefined,
|
|
363
|
+
status.capabilityAudit ? `Capability audit: ${status.capabilityAudit.removedTools.length} tools removed, ${status.capabilityAudit.removedExtensionCount} extension entries removed` : undefined,
|
|
358
364
|
status.error ? `Error: ${status.error}` : undefined,
|
|
359
365
|
statusActivityText ? `Activity: ${statusActivityText}` : undefined,
|
|
360
366
|
steeringText ? `Steering: ${steeringText}` : undefined,
|
|
@@ -366,6 +372,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
366
372
|
status.turnBudget ? `Turn budget: ${status.turnBudget.turnCount}/${status.turnBudget.maxTurns}+${status.turnBudget.graceTurns} (${status.turnBudget.outcome})` : undefined,
|
|
367
373
|
`Dir: ${asyncDir}`,
|
|
368
374
|
outputPath ? `Output: ${outputPath}` : undefined,
|
|
375
|
+
status.parallelHandoff ? `Parallel handoff: ${status.parallelHandoff.path}` : undefined,
|
|
369
376
|
reconciliation.message ? `Diagnosis: ${reconciliation.message}` : undefined,
|
|
370
377
|
reconciliation.resultPath && fs.existsSync(reconciliation.resultPath) ? `Result: ${reconciliation.resultPath}` : undefined,
|
|
371
378
|
].filter((line): line is string => Boolean(line));
|
|
@@ -401,14 +408,14 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
401
408
|
if (fs.existsSync(logPath)) lines.push(`Log: ${logPath}`);
|
|
402
409
|
if (fs.existsSync(eventsPath)) lines.push(`Events: ${eventsPath}`);
|
|
403
410
|
|
|
404
|
-
return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [] } };
|
|
411
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [], ...(processTerminal ? { lifecycleStatus: { processTerminal } } : {}) } };
|
|
405
412
|
}
|
|
406
413
|
}
|
|
407
414
|
|
|
408
415
|
if (resultPath) {
|
|
409
416
|
try {
|
|
410
417
|
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 }> };
|
|
418
|
+
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
419
|
if (params.view === "transcript") {
|
|
413
420
|
try {
|
|
414
421
|
return { content: [{ type: "text", text: formatAsyncResultTranscript(data, resultPath, { index: params.index, lines: params.lines }) }], details: { mode: "single", results: [] } };
|
|
@@ -420,6 +427,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
420
427
|
const status = data.state === "stopped" ? "stopped" : data.success ? "complete" : data.state === "paused" || data.exitCode === 0 ? "paused" : "failed";
|
|
421
428
|
const runId = data.runId ?? data.id ?? resolvedId;
|
|
422
429
|
const lines = [`Run: ${runId}`, `State: ${status}`, `Result: ${resultPath}`];
|
|
430
|
+
if (data.parallelHandoff?.path) lines.push(`Parallel handoff: ${data.parallelHandoff.path}`);
|
|
423
431
|
const children = Array.isArray(data.results) ? data.results : data.agent ? [{ agent: data.agent, sessionFile: data.sessionFile }] : [];
|
|
424
432
|
lines.push(formatResumeGuidance(runId, children, data.sessionFile, { stopped: status === "stopped" }));
|
|
425
433
|
if (data.summary) lines.push("", data.summary);
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "../../shared/types.ts";
|
|
14
14
|
import type { SubagentParamsLike } from "../foreground/subagent-executor.ts";
|
|
15
15
|
import { validateExecutionAcceptance } from "../shared/acceptance.ts";
|
|
16
|
+
import type { ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
|
|
16
17
|
|
|
17
18
|
export const SCHEDULED_RUNS_DIR = path.join(TEMP_ROOT_DIR, "scheduled-subagent-runs");
|
|
18
19
|
export const SCHEDULED_RUN_ACTIONS = ["schedule", "schedule-list", "schedule-status", "schedule-cancel"] as const;
|
|
@@ -57,6 +58,7 @@ type ScheduledRunManagerDeps = {
|
|
|
57
58
|
storeRoot?: string;
|
|
58
59
|
now?: () => number;
|
|
59
60
|
randomId?: () => string;
|
|
61
|
+
resolveCapabilityCeiling?: (sessionId: string) => ResolvedSubagentCapabilityCeiling | undefined;
|
|
60
62
|
timers?: ScheduledRunTimers;
|
|
61
63
|
};
|
|
62
64
|
|
|
@@ -310,12 +312,15 @@ export class ScheduledRunManager {
|
|
|
310
312
|
const sanitized = sanitizeScheduledParams(params);
|
|
311
313
|
if (sanitized.error) return textResult(sanitized.error, true);
|
|
312
314
|
const scheduleInput = params.schedule!.trim();
|
|
315
|
+
const sessionId = resolveCurrentSessionId(ctx.sessionManager);
|
|
316
|
+
if (this.deps.resolveCapabilityCeiling?.(sessionId)) {
|
|
317
|
+
return textResult("Cannot schedule a capability-ceiling-restricted run because this store does not yet persist ceilings. Remove the active parent restriction or run it immediately.", true);
|
|
318
|
+
}
|
|
313
319
|
const runAt = parseScheduledRunTime(scheduleInput, this.now());
|
|
314
320
|
const pendingCount = store.list().filter((job) => job.state === "scheduled" || job.state === "running").length;
|
|
315
321
|
const maxPending = resolveMaxPending(this.deps.config);
|
|
316
322
|
if (pendingCount >= maxPending) return textResult(`Scheduled subagent limit reached (${pendingCount}/${maxPending} pending or running). Cancel an existing scheduled run before adding another.`, true);
|
|
317
323
|
const id = this.randomId();
|
|
318
|
-
const sessionId = resolveCurrentSessionId(ctx.sessionManager);
|
|
319
324
|
const scheduleName = params.scheduleName?.trim();
|
|
320
325
|
const executionParams = sanitized.params!;
|
|
321
326
|
const now = this.now();
|
|
@@ -175,6 +175,9 @@ function terminalStatusFromResult(status: AsyncStatus, resultPath: string, now:
|
|
|
175
175
|
return {
|
|
176
176
|
...status,
|
|
177
177
|
state: repair.state,
|
|
178
|
+
...(status.lifecycleArtifactVersion === 3 && (!status.processTerminal || status.processTerminal.state === "pending") ? {
|
|
179
|
+
processTerminal: { version: 1 as const, state: "unknown" as const, runId: status.runId, runnerProcessInstanceId: "observer-unavailable", reason: "observer-unavailable" as const },
|
|
180
|
+
} : {}),
|
|
178
181
|
...(repair.state === "stopped" ? { stopped: true } : {}),
|
|
179
182
|
activityState: undefined,
|
|
180
183
|
lastUpdate: now,
|
|
@@ -231,6 +234,9 @@ function buildFailedRepair(status: AsyncStatus, asyncDir: string, now: number, r
|
|
|
231
234
|
const repairedStatus: AsyncStatus = {
|
|
232
235
|
...status,
|
|
233
236
|
state: "failed",
|
|
237
|
+
...(status.lifecycleArtifactVersion === 3 && (!status.processTerminal || status.processTerminal.state === "pending") ? {
|
|
238
|
+
processTerminal: { version: 1 as const, state: "unknown" as const, runId, runnerProcessInstanceId: "observer-unavailable", reason: "stale-repair" as const },
|
|
239
|
+
} : {}),
|
|
234
240
|
activityState: undefined,
|
|
235
241
|
lastUpdate: now,
|
|
236
242
|
endedAt: now,
|