pi-subagents 0.50.0 → 0.52.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 +103 -0
- package/agents/oracle.md +3 -1
- package/agents/reviewer.md +1 -0
- package/agents/scout.md +2 -2
- package/agents/worker.md +2 -1
- package/async-retention-discovery-worker.mjs +180 -0
- package/docs/agents.md +40 -2
- package/docs/configuration.md +30 -12
- package/docs/extension-api.md +42 -1
- package/docs/models.md +2 -0
- package/docs/observability.md +41 -5
- package/docs/tool-reference.md +38 -39
- package/docs/workflows.md +171 -3
- package/package.json +4 -2
- package/skills/pi-subagents/SKILL.md +6 -4
- package/skills/pi-subagents/references/constraints-and-recipes.md +12 -6
- package/skills/pi-subagents/references/execution-controls.md +29 -15
- package/skills/pi-subagents/references/management-authoring-rpc.md +3 -3
- package/skills/pi-subagents/references/prompting-and-roles.md +4 -2
- package/src/agents/agent-management.ts +124 -351
- package/src/agents/agents.ts +157 -31
- package/src/agents/skills.ts +1 -1
- package/src/api/external-job-provider.ts +185 -0
- package/src/api/preflight.ts +36 -8
- package/src/api/shared-types.ts +2 -0
- package/src/extension/config.ts +3 -3
- package/src/extension/doctor.ts +3 -6
- package/src/extension/fanout-child.ts +2 -2
- package/src/extension/index.ts +166 -88
- package/src/extension/public-execution.ts +31 -2
- package/src/extension/schemas.ts +12 -35
- package/src/extension/tool-description.ts +35 -24
- package/src/inspectors/herdr/actions.ts +2 -2
- package/src/inspectors/herdr/inspector-runner.ts +2 -1
- package/src/inspectors/herdr/project-panes.ts +2 -2
- package/src/intercom/native-supervisor-channel.ts +32 -10
- package/src/missions/lifecycle.ts +6 -1
- package/src/missions/store.ts +4 -9
- package/src/missions/workflow-state.ts +2 -2
- package/src/profiles/profiles.ts +3 -1
- package/src/runs/background/active-run-index.ts +31 -8
- package/src/runs/background/async-execution.ts +68 -40
- package/src/runs/background/async-job-tracker.ts +20 -4
- package/src/runs/background/async-resume.ts +47 -15
- package/src/runs/background/async-retention.ts +886 -0
- package/src/runs/background/async-status.ts +39 -53
- package/src/runs/background/chain-append.ts +3 -33
- package/src/runs/background/completion-dedupe.ts +5 -1
- package/src/runs/background/completion-replay.ts +22 -12
- package/src/runs/background/control-channel.ts +14 -68
- package/src/runs/background/fleet-view.ts +68 -19
- package/src/runs/background/index-segment.ts +59 -0
- package/src/runs/background/inspect-rpc.ts +443 -0
- package/src/runs/background/notify.ts +31 -5
- package/src/runs/background/result-files.ts +158 -90
- package/src/runs/background/result-watcher.ts +116 -20
- package/src/runs/background/resume-guidance.ts +8 -5
- package/src/runs/background/retained-children.ts +13 -3
- package/src/runs/background/run-id-query.ts +7 -0
- package/src/runs/background/run-id-resolver.ts +11 -9
- package/src/runs/background/run-status.ts +27 -18
- package/src/runs/background/scheduled-runs.ts +24 -4
- package/src/runs/background/stale-run-reconciler.ts +6 -3
- package/src/runs/background/steering.ts +11 -1
- package/src/runs/background/subagent-runner.ts +253 -140
- package/src/runs/background/subagent-wait.ts +8 -8
- package/src/runs/background/terminal-run-index.ts +129 -0
- package/src/runs/background/wait-completions.ts +21 -4
- package/src/runs/background/wait-subscriptions.ts +81 -2
- package/src/runs/foreground/async-steering-action.ts +21 -14
- package/src/runs/foreground/execution.ts +3 -1
- package/src/runs/foreground/subagent-executor.ts +666 -1579
- package/src/runs/foreground/workflow-detach-reconcile.ts +194 -0
- package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
- package/src/runs/shared/acceptance.ts +4 -4
- package/src/runs/shared/chain-outputs.ts +1 -3
- package/src/runs/shared/external-job-bridge.ts +444 -0
- package/src/runs/shared/external-job-runner.ts +286 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
- package/src/runs/shared/model-fallback.ts +98 -38
- package/src/runs/shared/orca-progress-tabs.ts +84 -22
- package/src/runs/shared/parallel-handoff.ts +46 -4
- package/src/runs/shared/parallel-utils.ts +4 -15
- package/src/runs/shared/permissions.ts +5 -1
- package/src/runs/shared/pi-args.ts +8 -1
- package/src/runs/shared/session-lease.ts +0 -6
- package/src/runs/shared/subagent-control.ts +26 -4
- package/src/runs/shared/subagent-prompt-runtime.ts +12 -6
- package/src/runs/shared/workflow-graph.ts +1 -23
- package/src/runs/shared/worktree.ts +14 -2
- package/src/shared/atomic-json.ts +22 -2
- package/src/shared/capacity-resilient-json.ts +102 -0
- package/src/shared/completion-owner.ts +14 -0
- package/src/shared/file-system-retry.ts +49 -1
- package/src/shared/fork-context.ts +42 -0
- package/src/shared/prompt-resources.ts +0 -40
- package/src/shared/settings.ts +3 -27
- package/src/shared/types.ts +71 -26
- package/src/shared/utils.ts +8 -0
- package/src/shared/watch-strategy.ts +10 -0
- package/src/slash/slash-bridge.ts +2 -1
- package/src/slash/slash-commands.ts +29 -3
- package/src/tui/fleet.ts +63 -15
- package/src/watchdog/change-signature.ts +1 -1
- package/src/watchdog/lsp-diagnostics.ts +1 -0
- package/src/workflows/chat-progress.ts +2 -2
- package/src/workflows/scripted-workflow.ts +220 -67
- package/src/runs/foreground/chain-clarify.ts +0 -1354
- package/src/runs/foreground/chain-execution.ts +0 -1581
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { buildCompletionKey, markSeenWithTtl } from "./completion-dedupe.ts";
|
|
4
4
|
import { createFileCoalescer } from "../../shared/file-coalescer.ts";
|
|
5
|
+
import { shouldUseNativeFsWatch } from "../../shared/watch-strategy.ts";
|
|
5
6
|
import {
|
|
6
7
|
SUBAGENT_ASYNC_COMPLETE_EVENT,
|
|
7
8
|
type IntercomEventBus,
|
|
@@ -54,6 +55,9 @@ type ResultWatcherDeps = {
|
|
|
54
55
|
deliverIntercomResults?: boolean;
|
|
55
56
|
/** Coalesces result-file events. Tests can lower this without changing retry timing. */
|
|
56
57
|
coalesceDelayMs?: number;
|
|
58
|
+
/** Returns true while a durable completion source needs periodic delivery checks. */
|
|
59
|
+
hasDeliveryDemand?: () => boolean;
|
|
60
|
+
platform?: NodeJS.Platform;
|
|
57
61
|
};
|
|
58
62
|
|
|
59
63
|
type ResultFileChild = {
|
|
@@ -88,6 +92,7 @@ type ResultFileData = CompletionNotification & {
|
|
|
88
92
|
|
|
89
93
|
type ResultFileIdentity = {
|
|
90
94
|
sessionId?: string;
|
|
95
|
+
completionOwnerId?: string;
|
|
91
96
|
runId?: string;
|
|
92
97
|
asyncDir?: string;
|
|
93
98
|
};
|
|
@@ -114,6 +119,7 @@ function jsonStringProperty(raw: string, property: string): string | undefined {
|
|
|
114
119
|
function resultFileIdentity(raw: string, file: string): ResultFileIdentity {
|
|
115
120
|
return {
|
|
116
121
|
sessionId: jsonStringProperty(raw, "sessionId"),
|
|
122
|
+
completionOwnerId: jsonStringProperty(raw, "completionOwnerId"),
|
|
117
123
|
runId: file.replace(/\.json$/i, ""),
|
|
118
124
|
asyncDir: jsonStringProperty(raw, "asyncDir"),
|
|
119
125
|
};
|
|
@@ -140,6 +146,11 @@ function isNotFound(error: unknown): boolean {
|
|
|
140
146
|
return errorCode(error) === "ENOENT";
|
|
141
147
|
}
|
|
142
148
|
|
|
149
|
+
function isAccessDenied(error: unknown): boolean {
|
|
150
|
+
const code = errorCode(error);
|
|
151
|
+
return code === "EPERM" || code === "EACCES";
|
|
152
|
+
}
|
|
153
|
+
|
|
143
154
|
function shouldPoll(error: unknown): boolean {
|
|
144
155
|
const code = errorCode(error);
|
|
145
156
|
return code === "EMFILE" || code === "ENOSPC";
|
|
@@ -149,6 +160,21 @@ function hasDeliveredNotification(data: ResultFileData): boolean {
|
|
|
149
160
|
return typeof data.notificationDeliveredAt === "number" && Number.isFinite(data.notificationDeliveredAt);
|
|
150
161
|
}
|
|
151
162
|
|
|
163
|
+
type PublicResultIdentity = {
|
|
164
|
+
state?: string;
|
|
165
|
+
timestamp?: number;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
function resultPayloadWasReplaced(delivered: ResultFileData, disk: PublicResultIdentity | undefined): boolean {
|
|
169
|
+
if (!disk) return false;
|
|
170
|
+
const deliveredState = typeof delivered.state === "string" ? delivered.state : undefined;
|
|
171
|
+
if (disk.state && deliveredState && disk.state !== deliveredState) return true;
|
|
172
|
+
const deliveredTimestamp = typeof delivered.timestamp === "number" && Number.isFinite(delivered.timestamp)
|
|
173
|
+
? delivered.timestamp
|
|
174
|
+
: undefined;
|
|
175
|
+
return disk.timestamp !== undefined && deliveredTimestamp !== undefined && disk.timestamp !== deliveredTimestamp;
|
|
176
|
+
}
|
|
177
|
+
|
|
152
178
|
function markDeliveredNotification(resultPath: string, data: ResultFileData, runId: string, now: number): ResultFileData {
|
|
153
179
|
const marked = { ...data, runId, notificationDeliveredAt: now };
|
|
154
180
|
writeAsyncResultFile(resultPath, marked);
|
|
@@ -169,6 +195,7 @@ export function createResultWatcher(
|
|
|
169
195
|
): {
|
|
170
196
|
startResultWatcher: () => void;
|
|
171
197
|
primeExistingResults: (options?: { triggerTurn?: boolean }) => void;
|
|
198
|
+
refreshResultDelivery: () => void;
|
|
172
199
|
stopResultWatcher: () => void;
|
|
173
200
|
} {
|
|
174
201
|
const fsApi = deps.fs ?? fs;
|
|
@@ -186,10 +213,13 @@ export function createResultWatcher(
|
|
|
186
213
|
// and revoked before the watcher, queues, or callbacks are torn down.
|
|
187
214
|
let activeSessionId: string | null = null;
|
|
188
215
|
|
|
189
|
-
const
|
|
216
|
+
const ownsCompletion = (sessionId: string, completionOwnerId: unknown, epoch: number) => {
|
|
190
217
|
if (!deliveryActive || epoch !== deliveryEpoch) return false;
|
|
191
218
|
if (!activeSessionId && state.currentSessionId) activeSessionId = state.currentSessionId;
|
|
192
|
-
return activeSessionId === sessionId
|
|
219
|
+
return activeSessionId === sessionId
|
|
220
|
+
&& state.currentSessionId === sessionId
|
|
221
|
+
&& typeof completionOwnerId === "string"
|
|
222
|
+
&& completionOwnerId === state.completionOwnerId;
|
|
193
223
|
};
|
|
194
224
|
|
|
195
225
|
const scheduleResult = (file: string, triggerTurn: boolean, delayMs = 0) => {
|
|
@@ -232,6 +262,7 @@ export function createResultWatcher(
|
|
|
232
262
|
return `${resultPath}:${stat.size}:${stat.mtimeMs}`;
|
|
233
263
|
} catch (error) {
|
|
234
264
|
identityCache.delete(file);
|
|
265
|
+
if (isAccessDenied(error)) throw error;
|
|
235
266
|
if (!isNotFound(error)) console.error(`Failed to inspect subagent result file '${resultPath}':`, error);
|
|
236
267
|
return undefined;
|
|
237
268
|
}
|
|
@@ -249,6 +280,7 @@ export function createResultWatcher(
|
|
|
249
280
|
return { identity, signature };
|
|
250
281
|
} catch (error) {
|
|
251
282
|
identityCache.delete(file);
|
|
283
|
+
if (isAccessDenied(error)) throw error;
|
|
252
284
|
if (!isNotFound(error)) console.error(`Failed to inspect subagent result file '${resultPath}':`, error);
|
|
253
285
|
return undefined;
|
|
254
286
|
}
|
|
@@ -270,7 +302,7 @@ export function createResultWatcher(
|
|
|
270
302
|
// Missing identity stays on the normal parser path so malformed or legacy
|
|
271
303
|
// files keep their existing diagnostics and compatibility behavior.
|
|
272
304
|
if (!identity.sessionId) return true;
|
|
273
|
-
if (identity.sessionId === state.currentSessionId) return true;
|
|
305
|
+
if (identity.sessionId === state.currentSessionId && identity.completionOwnerId === state.completionOwnerId) return true;
|
|
274
306
|
if (identity.asyncDir && fsApi.existsSync(path.join(identity.asyncDir, MISSION_BINDING_FILE))) return true;
|
|
275
307
|
if (identity.runId && (observed ?? observedRunIds()).has(identity.runId)) return true;
|
|
276
308
|
return Boolean(deps.observeCompletion && !deps.observedCompletionRunIds);
|
|
@@ -293,13 +325,33 @@ export function createResultWatcher(
|
|
|
293
325
|
const handleResult = async (file: string, triggerTurn: boolean) => {
|
|
294
326
|
if (processing.has(file)) return;
|
|
295
327
|
let observed: ReadonlySet<string> | undefined;
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
328
|
+
try {
|
|
329
|
+
if (!shouldProcessResult(file)) {
|
|
330
|
+
const runId = file === path.basename(file) && file.endsWith(".json") ? file.replace(/\.json$/i, "") : undefined;
|
|
331
|
+
observed = observedRunIds();
|
|
332
|
+
if (!runId || !observed.has(runId) || !shouldProcessResult(file, observed)) return;
|
|
333
|
+
}
|
|
334
|
+
} catch (error) {
|
|
335
|
+
if (!isAccessDenied(error)) throw error;
|
|
336
|
+
console.error(`Failed to inspect subagent result file '${publicResultPath(file)}'; will retry:`, error);
|
|
337
|
+
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
338
|
+
return;
|
|
300
339
|
}
|
|
301
340
|
processing.add(file);
|
|
341
|
+
let rereadReplacedPayload = false;
|
|
302
342
|
let resultPath = publicResultPath(file);
|
|
343
|
+
const readPublicResultIdentity = (): PublicResultIdentity | undefined => {
|
|
344
|
+
if (!publicResultFileExists(file)) return undefined;
|
|
345
|
+
const parsed: unknown = JSON.parse(fsApi.readFileSync(publicResultPath(file), "utf-8"));
|
|
346
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return undefined;
|
|
347
|
+
const record = parsed as Record<string, unknown>;
|
|
348
|
+
const state = typeof record.state === "string" && record.state ? record.state : undefined;
|
|
349
|
+
const timestamp = typeof record.timestamp === "number" && Number.isFinite(record.timestamp)
|
|
350
|
+
? record.timestamp
|
|
351
|
+
: undefined;
|
|
352
|
+
if (!state && timestamp === undefined) return undefined;
|
|
353
|
+
return { state, timestamp };
|
|
354
|
+
};
|
|
303
355
|
try {
|
|
304
356
|
const payloadPath = resultPayloadPath(file, observed);
|
|
305
357
|
if (!payloadPath) return;
|
|
@@ -325,8 +377,21 @@ export function createResultWatcher(
|
|
|
325
377
|
}
|
|
326
378
|
}
|
|
327
379
|
let data = parseResult(raw);
|
|
380
|
+
const markReplacedPayload = (): boolean => {
|
|
381
|
+
try {
|
|
382
|
+
if (!resultPayloadWasReplaced(data, readPublicResultIdentity())) return false;
|
|
383
|
+
} catch (error) {
|
|
384
|
+
if (isAccessDenied(error)) throw error;
|
|
385
|
+
if (isNotFound(error)) return false;
|
|
386
|
+
console.error(`Failed to re-read subagent result file '${publicResultPath(file)}':`, error);
|
|
387
|
+
}
|
|
388
|
+
identityCache.delete(file);
|
|
389
|
+
rereadReplacedPayload = true;
|
|
390
|
+
return true;
|
|
391
|
+
};
|
|
328
392
|
if (typeof data.sessionId !== "string" || !data.sessionId) return;
|
|
329
393
|
const sessionId = data.sessionId;
|
|
394
|
+
const completionOwnerId = data.completionOwnerId;
|
|
330
395
|
const runId = data.runId ?? data.id ?? file.replace(/\.json$/i, "");
|
|
331
396
|
const toolCallId = typeof data.toolCallId === "string" ? data.toolCallId : undefined;
|
|
332
397
|
let observerSucceeded = true;
|
|
@@ -344,7 +409,7 @@ export function createResultWatcher(
|
|
|
344
409
|
}
|
|
345
410
|
if (observerSucceeded) removeMissionObserverIndex(resultsDir, runId);
|
|
346
411
|
const epoch = deliveryEpoch;
|
|
347
|
-
if (!
|
|
412
|
+
if (!ownsCompletion(sessionId, completionOwnerId, epoch)) return;
|
|
348
413
|
// Recorded before dedupe and before the unlink below so subagent_wait can
|
|
349
414
|
// use the in-memory record or its bounded durable replay after cleanup.
|
|
350
415
|
recordWaitCompletion(state, runId, data, Date.now(), completionTtlMs, {
|
|
@@ -373,7 +438,8 @@ export function createResultWatcher(
|
|
|
373
438
|
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
374
439
|
return;
|
|
375
440
|
}
|
|
376
|
-
if (!
|
|
441
|
+
if (!ownsCompletion(sessionId, completionOwnerId, epoch)) return;
|
|
442
|
+
if (markReplacedPayload()) return;
|
|
377
443
|
if (!removeDeliveredResult(file, sessionId, runId, toolCallId)) scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
378
444
|
return;
|
|
379
445
|
}
|
|
@@ -428,7 +494,8 @@ export function createResultWatcher(
|
|
|
428
494
|
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
429
495
|
return;
|
|
430
496
|
}
|
|
431
|
-
if (!
|
|
497
|
+
if (!ownsCompletion(sessionId, completionOwnerId, epoch)) return;
|
|
498
|
+
if (markReplacedPayload()) return;
|
|
432
499
|
if (!removeDeliveredResult(file, sessionId, runId, toolCallId)) scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
433
500
|
return;
|
|
434
501
|
}
|
|
@@ -449,7 +516,7 @@ export function createResultWatcher(
|
|
|
449
516
|
asyncDir: data.asyncDir,
|
|
450
517
|
...(data.parallelHandoff ? { parallelHandoff: data.parallelHandoff } : {}),
|
|
451
518
|
}));
|
|
452
|
-
if (!
|
|
519
|
+
if (!ownsCompletion(sessionId, completionOwnerId, epoch)) return;
|
|
453
520
|
if (!intercomDelivered) console.error(`Subagent async grouped result intercom delivery was not acknowledged for '${resultPath}'.`);
|
|
454
521
|
}
|
|
455
522
|
|
|
@@ -473,11 +540,12 @@ export function createResultWatcher(
|
|
|
473
540
|
})) : [],
|
|
474
541
|
} : {}),
|
|
475
542
|
});
|
|
476
|
-
if (!
|
|
543
|
+
if (!ownsCompletion(sessionId, completionOwnerId, epoch)) return;
|
|
477
544
|
if (!accepted) {
|
|
478
545
|
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
479
546
|
return;
|
|
480
547
|
}
|
|
548
|
+
if (markReplacedPayload()) return;
|
|
481
549
|
try {
|
|
482
550
|
data = markDeliveredNotification(publicResultPath(file), data, runId, Date.now());
|
|
483
551
|
identityCache.delete(file);
|
|
@@ -514,12 +582,16 @@ export function createResultWatcher(
|
|
|
514
582
|
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
515
583
|
return;
|
|
516
584
|
}
|
|
517
|
-
if (!
|
|
585
|
+
if (!ownsCompletion(sessionId, completionOwnerId, epoch)) return;
|
|
518
586
|
if (!removeDeliveredResult(file, sessionId, runId, toolCallId)) scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
519
587
|
} catch (error) {
|
|
520
|
-
if (
|
|
588
|
+
if (isAccessDenied(error)) {
|
|
589
|
+
console.error(`Failed to process subagent result file '${resultPath}'; will retry:`, error);
|
|
590
|
+
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
591
|
+
} else if (!isNotFound(error)) console.error(`Failed to process subagent result file '${resultPath}':`, error);
|
|
521
592
|
} finally {
|
|
522
593
|
processing.delete(file);
|
|
594
|
+
if (rereadReplacedPayload) scheduleResult(file, triggerTurn);
|
|
523
595
|
}
|
|
524
596
|
};
|
|
525
597
|
|
|
@@ -567,6 +639,30 @@ export function createResultWatcher(
|
|
|
567
639
|
if (resultScanTimer) timers.clearInterval(resultScanTimer);
|
|
568
640
|
resultScanTimer = null;
|
|
569
641
|
};
|
|
642
|
+
const useNativeWatcher = () => shouldUseNativeFsWatch("result-delivery", deps.platform);
|
|
643
|
+
const hasDeliveryDemand = () => {
|
|
644
|
+
try {
|
|
645
|
+
return deps.hasDeliveryDemand?.() === true;
|
|
646
|
+
} catch (error) {
|
|
647
|
+
console.error("Failed to inspect subagent result delivery demand:", error);
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
const clearResultPoller = () => {
|
|
652
|
+
if (!state.watcherRestartTimer) return;
|
|
653
|
+
timers.clearTimeout(state.watcherRestartTimer);
|
|
654
|
+
timers.clearInterval(state.watcherRestartTimer);
|
|
655
|
+
state.watcherRestartTimer = null;
|
|
656
|
+
};
|
|
657
|
+
const startDemandPolling = () => {
|
|
658
|
+
if (!deliveryActive || useNativeWatcher() || state.watcherRestartTimer) return;
|
|
659
|
+
if (!hasDeliveryDemand()) return;
|
|
660
|
+
state.watcherRestartTimer = timers.setInterval(() => {
|
|
661
|
+
primeExistingResults();
|
|
662
|
+
if (!hasDeliveryDemand()) clearResultPoller();
|
|
663
|
+
}, POLL_INTERVAL_MS);
|
|
664
|
+
state.watcherRestartTimer.unref?.();
|
|
665
|
+
};
|
|
570
666
|
|
|
571
667
|
const startPolling = (reason: unknown) => {
|
|
572
668
|
state.watcher?.close();
|
|
@@ -606,6 +702,10 @@ export function createResultWatcher(
|
|
|
606
702
|
timers.clearInterval(state.watcherRestartTimer);
|
|
607
703
|
state.watcherRestartTimer = null;
|
|
608
704
|
}
|
|
705
|
+
if (!useNativeWatcher()) {
|
|
706
|
+
startDemandPolling();
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
609
709
|
try {
|
|
610
710
|
const watchDir = resolveWatchPath(resultsDir, fsApi.realpathSync.native);
|
|
611
711
|
state.watcher = fsApi.watch(watchDir, (_event, file) => {
|
|
@@ -648,11 +748,7 @@ export function createResultWatcher(
|
|
|
648
748
|
deliveryEpoch += 1;
|
|
649
749
|
state.watcher?.close();
|
|
650
750
|
state.watcher = null;
|
|
651
|
-
|
|
652
|
-
timers.clearTimeout(state.watcherRestartTimer);
|
|
653
|
-
timers.clearInterval(state.watcherRestartTimer);
|
|
654
|
-
}
|
|
655
|
-
state.watcherRestartTimer = null;
|
|
751
|
+
clearResultPoller();
|
|
656
752
|
clearResultScan();
|
|
657
753
|
state.resultFileCoalescer.clear();
|
|
658
754
|
pendingTriggerTurn.clear();
|
|
@@ -660,5 +756,5 @@ export function createResultWatcher(
|
|
|
660
756
|
identityCache.clear();
|
|
661
757
|
};
|
|
662
758
|
|
|
663
|
-
return { startResultWatcher, primeExistingResults, stopResultWatcher };
|
|
759
|
+
return { startResultWatcher, primeExistingResults, stopResultWatcher, refreshResultDelivery: () => { primeExistingResults(); startDemandPolling(); } };
|
|
664
760
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import type { AsyncRunSummary } from "./async-status.ts";
|
|
3
|
+
import { readAsyncRecoveryDescriptor } from "./async-resume.ts";
|
|
3
4
|
|
|
4
5
|
const INTERCOM_DETACH_ERROR = "detached for intercom coordination";
|
|
5
6
|
|
|
@@ -15,11 +16,13 @@ function formatIntercomDetachGuidance(run: AsyncRunSummary): string | undefined
|
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export function formatAsyncReviveCommand(run: AsyncRunSummary): string | undefined {
|
|
18
|
-
const step = run.steps.find((candidate) => candidate.status === "failed"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
const step = run.steps.find((candidate) => candidate.status === "failed");
|
|
20
|
+
const sessionFile = step?.sessionFile ?? (run.steps.length === 1 ? run.sessionFile : undefined);
|
|
21
|
+
if (!step || !sessionFile || !fs.existsSync(sessionFile)) return undefined;
|
|
22
|
+
try {
|
|
23
|
+
const descriptor = readAsyncRecoveryDescriptor(run.asyncDir);
|
|
24
|
+
if (!descriptor || descriptor.sourceRunId !== run.id || descriptor.agent !== step.agent) return undefined;
|
|
25
|
+
} catch {
|
|
23
26
|
return undefined;
|
|
24
27
|
}
|
|
25
28
|
const index = run.steps.length === 1 ? "" : `, index: ${step.index}`;
|
|
@@ -2,8 +2,10 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import { listAsyncRuns, type AsyncRunSummary } from "./async-status.ts";
|
|
3
3
|
import { readAsyncRecoveryDescriptor } from "./async-resume.ts";
|
|
4
4
|
import type { TokenUsage } from "../../shared/types.ts";
|
|
5
|
+
import { parallelHandoffPath, resolveRetainedWorktreeCwd } from "../shared/parallel-handoff.ts";
|
|
5
6
|
|
|
6
7
|
const MAX_RETAINED_CHILDREN = 10;
|
|
8
|
+
const MAX_RETAINED_CHILD_CANDIDATES = 100;
|
|
7
9
|
const MAX_TASK_SUMMARY_LENGTH = 120;
|
|
8
10
|
|
|
9
11
|
type RetainedChildState = "complete" | "failed" | "paused" | "stopped";
|
|
@@ -49,17 +51,25 @@ function retainedSessionFile(sessionFile: string | undefined): RetainedChildResu
|
|
|
49
51
|
|
|
50
52
|
function childResumability(run: AsyncRunSummary, step: AsyncRunSummary["steps"][number]): RetainedChildResumability {
|
|
51
53
|
if (run.state === "stopped" || step.status === "stopped") return { state: "not-resumable", reason: "stopped run" };
|
|
52
|
-
if (step.runner?.type === "external-cli") return { state: "not-resumable", reason: "external
|
|
54
|
+
if (step.runner?.type === "external-cli" || step.runner?.type === "external-job") return { state: "not-resumable", reason: "external runner" };
|
|
53
55
|
const session = retainedSessionFile(step.sessionFile ?? run.sessionFile);
|
|
54
56
|
if (session.state === "not-resumable") return session;
|
|
57
|
+
let recoveryDescriptor: ReturnType<typeof readAsyncRecoveryDescriptor>;
|
|
55
58
|
try {
|
|
56
|
-
|
|
59
|
+
recoveryDescriptor = readAsyncRecoveryDescriptor(run.asyncDir);
|
|
57
60
|
if (!recoveryDescriptor) return { state: "not-resumable", reason: "missing recovery descriptor" };
|
|
58
61
|
if (recoveryDescriptor.sourceRunId !== run.id) return { state: "not-resumable", reason: `recovery descriptor belongs to run ${recoveryDescriptor.sourceRunId}` };
|
|
59
62
|
if (recoveryDescriptor.agent !== step.agent) return { state: "not-resumable", reason: `recovery descriptor belongs to agent ${recoveryDescriptor.agent}` };
|
|
60
63
|
} catch (error) {
|
|
61
64
|
return { state: "not-resumable", reason: `invalid recovery descriptor: ${error instanceof Error ? error.message : String(error)}` };
|
|
62
65
|
}
|
|
66
|
+
let requiredCwd: string | undefined;
|
|
67
|
+
try {
|
|
68
|
+
requiredCwd = resolveRetainedWorktreeCwd(parallelHandoffPath(run.asyncDir), run.id, step.index) ?? recoveryDescriptor.cwd ?? run.cwd;
|
|
69
|
+
if (!requiredCwd || !fs.statSync(requiredCwd).isDirectory()) return { state: "not-resumable", reason: `required cwd is missing: ${requiredCwd ?? "unknown"}` };
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return { state: "not-resumable", reason: `resume dependency unavailable: ${error instanceof Error ? error.message : String(error)}` };
|
|
72
|
+
}
|
|
63
73
|
return session;
|
|
64
74
|
}
|
|
65
75
|
|
|
@@ -71,7 +81,7 @@ function boundedTaskSummary(value: string | undefined): string {
|
|
|
71
81
|
}
|
|
72
82
|
|
|
73
83
|
export function listRetainedChildren(asyncDirRoot: string, sessionId: string): RetainedChild[] {
|
|
74
|
-
const children = listAsyncRuns(asyncDirRoot, { sessionId, states: ["complete", "failed", "paused", "stopped"], reconcile: false })
|
|
84
|
+
const children = listAsyncRuns(asyncDirRoot, { sessionId, states: ["complete", "failed", "paused", "stopped"], entryLimit: MAX_RETAINED_CHILD_CANDIDATES, reconcile: false })
|
|
75
85
|
.flatMap((run) => {
|
|
76
86
|
if (!run.parentWorkflowRunId || run.steps.length !== 1) return [];
|
|
77
87
|
if (!isRetainedChildState(run.state)) return [];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const MIN_SAFE_ASYNC_RUN_PREFIX_LENGTH = 8;
|
|
2
|
+
const FULL_ASYNC_RUN_ID_LENGTH = 36;
|
|
3
|
+
|
|
4
|
+
/** Full generated ids and longer exact identities never fall back to a root scan. */
|
|
5
|
+
export function canScanAsyncRunPrefix(id: string): boolean {
|
|
6
|
+
return id.length >= MIN_SAFE_ASYNC_RUN_PREFIX_LENGTH && id.length < FULL_ASYNC_RUN_ID_LENGTH;
|
|
7
|
+
}
|
|
@@ -3,7 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import { DIRS, type SubagentState } from "../../shared/types.ts";
|
|
4
4
|
import { readStatus } from "../../shared/utils.ts";
|
|
5
5
|
import { findAsyncRunPrefixMatches, type AsyncRunLocation } from "./async-resume.ts";
|
|
6
|
-
import { resultFilePath,
|
|
6
|
+
import { resultCandidateFilesForToolCall, resultFilePath, resultPayloadPathForIndexedRun } from "./result-files.ts";
|
|
7
7
|
import { readActiveRunToolCallIndex } from "./active-run-index.ts";
|
|
8
8
|
import { assertSafeNestedId, findNestedRunMatchesById, type NestedRoute, type NestedRunMatch, type NestedRunResolutionScope } from "../shared/nested-events.ts";
|
|
9
9
|
|
|
@@ -21,11 +21,12 @@ export interface ResolveSubagentRunIdDeps {
|
|
|
21
21
|
|
|
22
22
|
function exactAsyncLocation(id: string, asyncDirRoot: string, resultsDir: string): AsyncRunLocation | undefined {
|
|
23
23
|
const asyncDir = path.join(asyncDirRoot, id);
|
|
24
|
-
const
|
|
25
|
-
|
|
24
|
+
const asyncDirExists = fs.existsSync(asyncDir);
|
|
25
|
+
const resultPath = resultPathFor(resultsDir, id);
|
|
26
|
+
if (!asyncDirExists && !resultPath) return undefined;
|
|
26
27
|
return {
|
|
27
|
-
asyncDir:
|
|
28
|
-
resultPath
|
|
28
|
+
asyncDir: asyncDirExists ? asyncDir : null,
|
|
29
|
+
resultPath,
|
|
29
30
|
resolvedId: id,
|
|
30
31
|
};
|
|
31
32
|
}
|
|
@@ -55,7 +56,7 @@ function readWorkflowResultIdentity(resultPath: string): WorkflowResultIdentity
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
function resultPathFor(resultsDir: string, runId: string): string | null {
|
|
58
|
-
const resultPath = resultFilePath(resultsDir, runId);
|
|
59
|
+
const resultPath = resultPayloadPathForIndexedRun(resultsDir, runId) ?? resultFilePath(resultsDir, runId);
|
|
59
60
|
return fs.existsSync(resultPath) ? resultPath : null;
|
|
60
61
|
}
|
|
61
62
|
|
|
@@ -72,11 +73,12 @@ function indexedToolCallIdAsyncLocations(toolCallId: string, asyncDirRoot: strin
|
|
|
72
73
|
const runId = status.runId || entry;
|
|
73
74
|
byId.set(runId, { asyncDir, resultPath: resultPathFor(resultsDir, runId), resolvedId: runId });
|
|
74
75
|
}
|
|
75
|
-
for (const entry of
|
|
76
|
-
const
|
|
76
|
+
for (const entry of resultCandidateFilesForToolCall(resultsDir, toolCallId)) {
|
|
77
|
+
const runIdFromFile = entry.slice(0, -".json".length);
|
|
78
|
+
const resultPath = resultPayloadPathForIndexedRun(resultsDir, runIdFromFile) ?? path.join(resultsDir, entry);
|
|
77
79
|
const identity = readWorkflowResultIdentity(resultPath);
|
|
78
80
|
if (!identity || !toolCallIdMatches(identity.toolCallId, toolCallId)) continue;
|
|
79
|
-
const runId = identity.runId ?? identity.id ??
|
|
81
|
+
const runId = identity.runId ?? identity.id ?? runIdFromFile;
|
|
80
82
|
const asyncDir = path.join(asyncDirRoot, runId);
|
|
81
83
|
byId.set(runId, { asyncDir: fs.existsSync(asyncDir) ? asyncDir : null, resultPath, resolvedId: runId });
|
|
82
84
|
}
|
|
@@ -108,11 +108,6 @@ function hasExistingSessionFile(value: unknown): value is string {
|
|
|
108
108
|
return typeof value === "string" && fs.existsSync(value);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
function formatCheckpointGuidance(runId: string | undefined, checkpoint: AsyncStatus["checkpoint"] | undefined): string | undefined {
|
|
112
|
-
if (!runId || !checkpoint || checkpoint.status !== "pending") return undefined;
|
|
113
|
-
return `Checkpoint: ${checkpoint.name}${checkpoint.message ? ` — ${checkpoint.message}` : ""}\nApprove: subagent({ action: "approve-checkpoint", id: "${runId}" })\nReject: subagent({ action: "reject-checkpoint", id: "${runId}" })`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
111
|
function formatResumeGuidance(runId: string | undefined, children: Array<{ agent?: unknown; sessionFile?: unknown; runId?: unknown; workflowKey?: unknown; status?: unknown; activityState?: unknown }>, fallbackSessionFile?: unknown, options: { stopped?: boolean } = {}): string {
|
|
117
112
|
if (options.stopped) return "Resume: unavailable; stopped runs are not resumable. Start a new run instead.";
|
|
118
113
|
const knownChildren = children
|
|
@@ -210,12 +205,6 @@ function formatRememberedForegroundStatus(run: ForegroundResumeRun): string {
|
|
|
210
205
|
else lines.push(`Transcript: subagent({ action: "status", id: "${run.runId}", index: 0, view: "transcript" })`);
|
|
211
206
|
const detached = run.children.some((child) => child.status === "detached");
|
|
212
207
|
const resumable = run.children.find((child) => hasExistingSessionFile(child.sessionFile));
|
|
213
|
-
if (run.checkpoint?.status === "pending") {
|
|
214
|
-
lines.push(`Checkpoint: ${run.checkpoint.name}${run.checkpoint.message ? ` — ${run.checkpoint.message}` : ""}`);
|
|
215
|
-
lines.push(`Approve: subagent({ action: "approve-checkpoint", id: "${run.runId}" })`);
|
|
216
|
-
lines.push(`Reject: subagent({ action: "reject-checkpoint", id: "${run.runId}" })`);
|
|
217
|
-
return lines.join("\n");
|
|
218
|
-
}
|
|
219
208
|
if (detached) {
|
|
220
209
|
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.`);
|
|
221
210
|
} else if (resumable) {
|
|
@@ -518,6 +507,12 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
518
507
|
reconciliation.message ? `Diagnosis: ${reconciliation.message}` : undefined,
|
|
519
508
|
reconciliation.resultPath && fs.existsSync(reconciliation.resultPath) ? `Result: ${reconciliation.resultPath}` : undefined,
|
|
520
509
|
].filter((line): line is string => Boolean(line));
|
|
510
|
+
const liveWorkflowControls = status.mode === "workflow" && deps.state?.currentSessionId === status.sessionId && deps.state?.workflowControllers?.has(status.runId)
|
|
511
|
+
? [...(deps.state?.foregroundControls.values() ?? [])].filter((control) => control.parentWorkflowRunId === status.runId
|
|
512
|
+
&& control.sessionId === status.sessionId
|
|
513
|
+
&& Boolean(control.workflowSteeringDir && fs.existsSync(control.workflowSteeringDir))
|
|
514
|
+
&& (control.activeChildren?.size ?? 0) > 0)
|
|
515
|
+
: [];
|
|
521
516
|
for (const [index, step] of (status.steps ?? []).entries()) {
|
|
522
517
|
const stepActivityText = step.status === "running" ? formatActivityLabel(step.lastActivityAt, step.activityState) : undefined;
|
|
523
518
|
const modelThinking = formatModelThinking(step.model, step.thinking);
|
|
@@ -534,28 +529,42 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
534
529
|
lines.push(` Runner: external-cli (${step.runner.command}${step.runner.args.length ? ` ${step.runner.args.join(" ")}` : ""})`);
|
|
535
530
|
if (step.externalProcess?.pid !== undefined) lines.push(` Process: ${step.externalProcess.pid}`);
|
|
536
531
|
if (step.externalProcess) lines.push(` Stdout: ${step.externalProcess.stdoutPath}`, ` Stderr: ${step.externalProcess.stderrPath}`);
|
|
532
|
+
} else if (step.runner?.type === "external-job") {
|
|
533
|
+
lines.push(` Runner: external-job (${step.runner.provider})`);
|
|
534
|
+
if (step.externalJob?.providerJobId) lines.push(` Provider job: ${step.externalJob.providerJobId}`);
|
|
535
|
+
if (step.externalJob?.state) lines.push(` Provider state: ${step.externalJob.state}`);
|
|
536
|
+
if (step.externalJob?.conversationUrl) lines.push(` Conversation: ${step.externalJob.conversationUrl}`);
|
|
537
|
+
if (step.externalJob?.resultArtifactPath) lines.push(` Result artifact: ${step.externalJob.resultArtifactPath}`);
|
|
537
538
|
}
|
|
538
539
|
lines.push(...formatNestedRunStatusLines(step.children, { indent: " ", commandHints: true, maxLines: 20 }));
|
|
539
540
|
const stepOutputPath = path.join(asyncDir, `output-${index}.log`);
|
|
540
541
|
if (stepOutputPath !== outputPath && fs.existsSync(stepOutputPath)) lines.push(` Output: ${stepOutputPath}`);
|
|
541
|
-
if (step.status === "running" && step.runner?.type !== "external-cli") {
|
|
542
|
+
if (step.status === "running" && step.runner?.type !== "external-cli" && step.runner?.type !== "external-job" && status.mode !== "workflow") {
|
|
542
543
|
lines.push(` Intercom target: ${resolveSubagentIntercomTarget(status.runId, step.agent, index)} (if registered)`);
|
|
543
544
|
lines.push(` Steer: subagent({ action: "steer", id: "${status.runId}", index: ${index}, message: "..." })`);
|
|
544
|
-
} else if (step.status === "running" && step.runner?.type === "external-cli") {
|
|
545
|
-
lines.push(" Steer: unavailable;
|
|
545
|
+
} else if (step.status === "running" && (step.runner?.type === "external-cli" || step.runner?.type === "external-job")) {
|
|
546
|
+
lines.push(" Steer: unavailable; external runners do not accept live messages.");
|
|
546
547
|
}
|
|
547
548
|
}
|
|
548
549
|
const attached = new Set((status.steps ?? []).flatMap((step) => step.children?.map((child) => child.id) ?? []));
|
|
549
550
|
const unattached = nestedChildren.filter((child) => !attached.has(child.id));
|
|
550
551
|
lines.push(...formatNestedRunStatusLines(unattached, { indent: "", commandHints: true, maxLines: 20 }));
|
|
552
|
+
if (status.mode === "workflow" && status.state === "running") {
|
|
553
|
+
if (liveWorkflowControls.length === 0) lines.push("Steer: unavailable; no live foreground route is registered in the active session.");
|
|
554
|
+
else for (const control of liveWorkflowControls) {
|
|
555
|
+
for (const index of [...control.activeChildren!.keys()].sort((left, right) => left - right)) {
|
|
556
|
+
lines.push(`Steer live foreground child: subagent({ action: "steer", id: "${control.runId}", index: ${index}, message: "..." })`);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
551
560
|
if (nestedWarning) lines.push(`Warning: ${nestedWarning}`);
|
|
552
561
|
if (status.sessionFile) lines.push(`Session: ${status.sessionFile}`);
|
|
553
|
-
const allExternal = (status.steps?.length ?? 0) > 0 && status.steps!.every((step) => step.runner?.type === "external-cli");
|
|
554
|
-
if (status.state === "running" && !allExternal) lines.push(`Steer running child: subagent({ action: "steer", id: "${status.runId}", message: "..." })`);
|
|
562
|
+
const allExternal = (status.steps?.length ?? 0) > 0 && status.steps!.every((step) => step.runner?.type === "external-cli" || step.runner?.type === "external-job");
|
|
563
|
+
if (status.state === "running" && !allExternal && status.mode !== "workflow") lines.push(`Steer running child: subagent({ action: "steer", id: "${status.runId}", message: "..." })`);
|
|
555
564
|
if (status.state !== "running") {
|
|
556
565
|
lines.push(allExternal
|
|
557
|
-
? "Resume: unavailable;
|
|
558
|
-
:
|
|
566
|
+
? "Resume: unavailable; external runners do not persist Pi sessions."
|
|
567
|
+
: formatResumeGuidance(status.runId, status.steps ?? [], status.sessionFile, { stopped: status.state === "stopped" || status.stopped === true }));
|
|
559
568
|
}
|
|
560
569
|
if (fs.existsSync(logPath)) lines.push(`Log: ${logPath}`);
|
|
561
570
|
if (fs.existsSync(eventsPath)) lines.push(`Events: ${eventsPath}`);
|
|
@@ -152,7 +152,13 @@ function assertScheduleRoot(root: string, projectCwd: string | undefined, create
|
|
|
152
152
|
if (create) fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
153
153
|
return;
|
|
154
154
|
}
|
|
155
|
-
|
|
155
|
+
let projectPath: string;
|
|
156
|
+
try {
|
|
157
|
+
projectPath = fs.realpathSync(projectCwd);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (!create && (error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
156
162
|
let existing = root;
|
|
157
163
|
while (!fs.existsSync(existing)) {
|
|
158
164
|
const parent = path.dirname(existing);
|
|
@@ -235,7 +241,7 @@ class ScheduleStore {
|
|
|
235
241
|
}
|
|
236
242
|
|
|
237
243
|
list(): ScheduleRecord[] {
|
|
238
|
-
return this.ids().map((id) => this.
|
|
244
|
+
return this.ids().map((id) => this.find(id)).filter((record): record is ScheduleRecord => record !== undefined);
|
|
239
245
|
}
|
|
240
246
|
|
|
241
247
|
get(id: string): ScheduleRecord {
|
|
@@ -340,6 +346,8 @@ function executionParams(schedule: ScheduleRecord): SubagentParamsLike {
|
|
|
340
346
|
context: "fresh",
|
|
341
347
|
cwd: schedule.cwd,
|
|
342
348
|
mission: false,
|
|
349
|
+
// Scheduled fires have no operator watching, so completions must name the origin.
|
|
350
|
+
scheduleOrigin: { id: schedule.id, ...(schedule.name ? { name: schedule.name } : {}) },
|
|
343
351
|
...(schedule.timeoutMs === undefined ? {} : { timeoutMs: schedule.timeoutMs }),
|
|
344
352
|
};
|
|
345
353
|
}
|
|
@@ -419,6 +427,18 @@ export class ScheduledRunManager {
|
|
|
419
427
|
return new Set(this.observedAsyncIds);
|
|
420
428
|
}
|
|
421
429
|
|
|
430
|
+
referencedAsyncRunIds(): Set<string> {
|
|
431
|
+
const runIds = new Set(this.observedAsyncIds);
|
|
432
|
+
for (const store of this.stores.values()) {
|
|
433
|
+
for (const scheduleId of store.ids()) {
|
|
434
|
+
for (const run of store.history(scheduleId)) {
|
|
435
|
+
if (run.asyncId) runIds.add(run.asyncId);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return runIds;
|
|
440
|
+
}
|
|
441
|
+
|
|
422
442
|
handleAsyncCompletion(payload: unknown): void {
|
|
423
443
|
if (!payload || typeof payload !== "object") return;
|
|
424
444
|
const data = payload as { id?: unknown; runId?: unknown; success?: unknown; state?: unknown; summary?: unknown };
|
|
@@ -461,7 +481,7 @@ export class ScheduledRunManager {
|
|
|
461
481
|
if (this.deps.resolveCapabilityCeiling?.(sessionId)) return textResult("Cannot persist a schedule while a capability ceiling is active.", undefined, undefined, true);
|
|
462
482
|
if (store.list().length >= resolveMaxPending(this.deps.config)) return textResult(`Schedule limit reached (${resolveMaxPending(this.deps.config)}).`, undefined, undefined, true);
|
|
463
483
|
const id = validateScheduleId((params.id?.trim() || this.randomId()));
|
|
464
|
-
if (store.
|
|
484
|
+
if (store.ids().includes(id)) return textResult(`Schedule '${id}' already exists.`, undefined, undefined, true);
|
|
465
485
|
const now = this.now();
|
|
466
486
|
let trigger: ScheduleTrigger;
|
|
467
487
|
if (at) {
|
|
@@ -474,7 +494,7 @@ export class ScheduledRunManager {
|
|
|
474
494
|
const schedule: ScheduleRecord = {
|
|
475
495
|
schemaVersion: 1,
|
|
476
496
|
id,
|
|
477
|
-
name: params.name?.trim() ||
|
|
497
|
+
name: params.name?.trim() || targetLabel(target.target!),
|
|
478
498
|
cwd: path.resolve(params.cwd ?? ctx.cwd),
|
|
479
499
|
trigger,
|
|
480
500
|
target: target.target!,
|
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
4
4
|
import { resultFilePath, resultPayloadPathForSessionRun, writeAsyncResultFile } from "./result-files.ts";
|
|
5
|
-
import {
|
|
5
|
+
import { updateActiveRunIndex } from "./active-run-index.ts";
|
|
6
6
|
import { readStatus } from "../../shared/utils.ts";
|
|
7
7
|
import { DIRS, type AsyncParallelGroupStatus, type AsyncStatus, type NestedRunSummary, type SubagentRunMode } from "../../shared/types.ts";
|
|
8
8
|
import { resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
@@ -17,6 +17,7 @@ interface StartedRunMetadata {
|
|
|
17
17
|
runId: string;
|
|
18
18
|
pid?: number;
|
|
19
19
|
sessionId?: string;
|
|
20
|
+
completionOwnerId?: string;
|
|
20
21
|
mode?: SubagentRunMode;
|
|
21
22
|
agents?: string[];
|
|
22
23
|
chainStepCount?: number;
|
|
@@ -190,6 +191,7 @@ function buildStartedStatus(asyncDir: string, startedRun: StartedRunMetadata, no
|
|
|
190
191
|
return {
|
|
191
192
|
runId: startedRun.runId || path.basename(asyncDir),
|
|
192
193
|
...(startedRun.sessionId ? { sessionId: startedRun.sessionId } : {}),
|
|
194
|
+
...(startedRun.completionOwnerId ? { completionOwnerId: startedRun.completionOwnerId } : {}),
|
|
193
195
|
mode: startedRun.mode ?? "single",
|
|
194
196
|
state: "running",
|
|
195
197
|
pid: startedRun.pid,
|
|
@@ -244,6 +246,7 @@ function buildFailedRepair(status: AsyncStatus, asyncDir: string, now: number, r
|
|
|
244
246
|
id: runId,
|
|
245
247
|
agent: resultAgent,
|
|
246
248
|
mode: status.mode,
|
|
249
|
+
...(status.completionOwnerId ? { completionOwnerId: status.completionOwnerId } : {}),
|
|
247
250
|
success: false,
|
|
248
251
|
state: "failed",
|
|
249
252
|
summary: message,
|
|
@@ -271,7 +274,7 @@ function writeFailedRepair(asyncDir: string, status: AsyncStatus, resultPath: st
|
|
|
271
274
|
const repair = buildFailedRepair(status, asyncDir, now, reason);
|
|
272
275
|
if (repair.result.sessionId) writeAsyncResultFile(resultPath, repair.result);
|
|
273
276
|
writeAtomicJson(path.join(asyncDir, "status.json"), repair.status);
|
|
274
|
-
|
|
277
|
+
updateActiveRunIndex(asyncDir, repair.status.state, repair.status.toolCallId);
|
|
275
278
|
appendJsonlBestEffort(path.join(asyncDir, "events.jsonl"), {
|
|
276
279
|
type: "subagent.run.repaired_stale",
|
|
277
280
|
ts: now,
|
|
@@ -365,7 +368,7 @@ export function reconcileAsyncRun(asyncDir: string, options: ReconcileAsyncRunOp
|
|
|
365
368
|
: undefined;
|
|
366
369
|
if (terminalStatus) {
|
|
367
370
|
writeAtomicJson(path.join(asyncDir, "status.json"), terminalStatus);
|
|
368
|
-
|
|
371
|
+
updateActiveRunIndex(asyncDir, terminalStatus.state, terminalStatus.toolCallId);
|
|
369
372
|
return { status: terminalStatus, repaired: true, resultPath: existingResultPath, message: "Existing async result file was used to repair stale running status." };
|
|
370
373
|
}
|
|
371
374
|
if (effectiveStatus.displayDismissedAt === undefined) return { status: effectiveStatus, repaired: false, resultPath: existingResultPath };
|