pi-subagents 0.49.0 → 0.50.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 +35 -0
- package/agents/oracle.md +7 -5
- package/agents/researcher.md +1 -1
- package/agents/reviewer.md +2 -2
- package/agents/scout.md +1 -1
- package/agents/worker.md +1 -1
- package/docs/agents.md +2 -0
- package/docs/configuration.md +47 -3
- package/docs/extension-api.md +36 -0
- package/docs/missions.md +1 -1
- package/docs/observability.md +16 -0
- package/docs/tool-reference.md +19 -2
- package/docs/workflows.md +2 -2
- package/package.json +1 -1
- package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
- package/skills/pi-subagents/references/execution-controls.md +5 -4
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/skills/pi-subagents/references/prompting-and-roles.md +31 -15
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +10 -0
- package/src/api/external-runs.ts +174 -84
- package/src/api/preflight.ts +12 -6
- package/src/extension/config.ts +33 -0
- package/src/extension/index.ts +48 -5
- package/src/extension/public-execution.ts +3 -1
- package/src/extension/rpc.ts +5 -1
- package/src/extension/schemas.ts +2 -1
- package/src/extension/tool-description.ts +4 -2
- package/src/inspectors/herdr/actions.ts +3 -7
- package/src/inspectors/herdr/project-panes.ts +2 -6
- package/src/inspectors/herdr/shell-command.ts +16 -0
- package/src/intercom/intercom-bridge.ts +2 -3
- package/src/intercom/native-supervisor-channel.ts +19 -42
- package/src/missions/goal-driver.ts +3 -1
- package/src/runs/background/active-run-index.ts +71 -1
- package/src/runs/background/async-execution.ts +49 -7
- package/src/runs/background/async-job-tracker.ts +1 -0
- package/src/runs/background/async-resume.ts +3 -5
- package/src/runs/background/async-status-snapshot.ts +277 -0
- package/src/runs/background/async-status.ts +8 -3
- package/src/runs/background/chain-root-attachment.ts +2 -2
- package/src/runs/background/completion-replay.ts +11 -1
- package/src/runs/background/fleet-view.ts +3 -1
- package/src/runs/background/result-files.ts +437 -0
- package/src/runs/background/result-watcher.ts +188 -41
- package/src/runs/background/retained-children.ts +69 -20
- package/src/runs/background/run-id-resolver.ts +30 -24
- package/src/runs/background/run-status.ts +7 -2
- package/src/runs/background/scheduled-runs.ts +54 -28
- package/src/runs/background/stale-run-reconciler.ts +27 -13
- package/src/runs/background/subagent-runner.ts +294 -32
- package/src/runs/background/subagent-wait.ts +2 -0
- package/src/runs/background/wait-completions.ts +5 -2
- package/src/runs/foreground/async-dismiss-action.ts +2 -1
- package/src/runs/foreground/chain-execution.ts +16 -0
- package/src/runs/foreground/execution.ts +219 -15
- package/src/runs/foreground/subagent-executor.ts +114 -28
- package/src/runs/shared/completion-guard.ts +96 -6
- package/src/runs/shared/external-cli-runner.ts +4 -0
- package/src/runs/shared/model-fallback.ts +16 -2
- package/src/runs/shared/nested-events.ts +66 -62
- package/src/runs/shared/orca-progress-tabs.ts +375 -0
- package/src/runs/shared/parallel-utils.ts +2 -0
- package/src/runs/shared/subagent-control.ts +15 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +1 -9
- package/src/runs/shared/subagent-startup-retry.ts +12 -0
- package/src/runs/shared/tool-timeout.ts +93 -0
- package/src/shared/types.ts +29 -2
- package/src/slash/slash-commands.ts +34 -25
- package/src/slash/slash-live-state.ts +3 -0
- package/src/tui/fleet-status.ts +160 -45
- package/src/tui/fleet.ts +125 -15
- package/src/tui/render.ts +41 -10
- package/src/workflows/chat-progress.ts +6 -4
- package/src/workflows/scripted-workflow.ts +227 -70
|
@@ -239,8 +239,15 @@ class ScheduleStore {
|
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
get(id: string): ScheduleRecord {
|
|
242
|
+
const record = this.find(id);
|
|
243
|
+
if (!record) throw new Error(`Schedule '${id}' not found.`);
|
|
244
|
+
return record;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Like {@link get}, but returns undefined when the schedule no longer exists. */
|
|
248
|
+
find(id: string): ScheduleRecord | undefined {
|
|
242
249
|
const file = path.join(scheduleDir(this.root, id, false, this.projectCwd), "schedule.json");
|
|
243
|
-
if (!fs.existsSync(file))
|
|
250
|
+
if (!fs.existsSync(file)) return undefined;
|
|
244
251
|
return parseSchedule(readJson(file, "schedule record"), file);
|
|
245
252
|
}
|
|
246
253
|
|
|
@@ -361,6 +368,7 @@ export class ScheduledRunManager {
|
|
|
361
368
|
private readonly stores = new Map<string, ScheduleStore>();
|
|
362
369
|
private readonly contexts = new Map<string, ExtensionContext>();
|
|
363
370
|
private readonly timers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
371
|
+
private readonly observedAsyncIds = new Set<string>();
|
|
364
372
|
private readonly now: () => number;
|
|
365
373
|
private readonly randomId: () => string;
|
|
366
374
|
private readonly timersApi: ScheduledRunTimers;
|
|
@@ -383,6 +391,7 @@ export class ScheduledRunManager {
|
|
|
383
391
|
this.store = undefined;
|
|
384
392
|
this.stores.clear();
|
|
385
393
|
this.contexts.clear();
|
|
394
|
+
this.observedAsyncIds.clear();
|
|
386
395
|
}
|
|
387
396
|
|
|
388
397
|
async handleToolCall(params: SubagentParamsLike, ctx: ExtensionContext): Promise<AgentToolResult<Details>> {
|
|
@@ -407,26 +416,7 @@ export class ScheduledRunManager {
|
|
|
407
416
|
}
|
|
408
417
|
|
|
409
418
|
observedCompletionRunIds(): Set<string> {
|
|
410
|
-
|
|
411
|
-
for (const store of this.stores.values()) {
|
|
412
|
-
let ids: string[];
|
|
413
|
-
try {
|
|
414
|
-
ids = store.ids();
|
|
415
|
-
} catch (error) {
|
|
416
|
-
console.error(`Failed to inspect schedule store '${store.root}' during async completion discovery:`, error);
|
|
417
|
-
continue;
|
|
418
|
-
}
|
|
419
|
-
for (const id of ids) {
|
|
420
|
-
try {
|
|
421
|
-
for (const run of store.history(id)) {
|
|
422
|
-
if (run.state === "running" && run.asyncId) runIds.add(run.asyncId);
|
|
423
|
-
}
|
|
424
|
-
} catch (error) {
|
|
425
|
-
console.error(`Failed to inspect schedule '${id}' in '${store.root}' during async completion discovery:`, error);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
return runIds;
|
|
419
|
+
return new Set(this.observedAsyncIds);
|
|
430
420
|
}
|
|
431
421
|
|
|
432
422
|
handleAsyncCompletion(payload: unknown): void {
|
|
@@ -563,9 +553,10 @@ export class ScheduledRunManager {
|
|
|
563
553
|
for (const schedule of store.list()) this.restoreOne(store, schedule);
|
|
564
554
|
}
|
|
565
555
|
|
|
566
|
-
private restoreOne(store: ScheduleStore, schedule: ScheduleRecord): void {
|
|
556
|
+
private restoreOne(store: ScheduleStore, schedule: ScheduleRecord, notBefore?: number, rearm = true): void {
|
|
567
557
|
if (schedule.activeRunId) {
|
|
568
558
|
const run = store.history(schedule.id).find((item) => item.id === schedule.activeRunId);
|
|
559
|
+
if (run?.state === "running" && run.asyncId) this.observedAsyncIds.add(run.asyncId);
|
|
569
560
|
const startedAt = run?.startedAt ? Date.parse(run.startedAt) : Number.NaN;
|
|
570
561
|
if (run?.state === "running" && run.asyncDir) {
|
|
571
562
|
try {
|
|
@@ -588,26 +579,59 @@ export class ScheduledRunManager {
|
|
|
588
579
|
fs.rmSync(path.join(store.directory(schedule.id), "active.lock"), { force: true });
|
|
589
580
|
}
|
|
590
581
|
}
|
|
591
|
-
if (schedule.paused) return;
|
|
582
|
+
if (!rearm || schedule.paused) return;
|
|
592
583
|
const next = nextRunAt(schedule);
|
|
593
584
|
if (next === undefined) return;
|
|
594
|
-
if (!schedule.activeRunId && next < this.now() && schedule.catchUp === "none")
|
|
595
|
-
|
|
585
|
+
if (!schedule.activeRunId && next < this.now() && schedule.catchUp === "none") {
|
|
586
|
+
try {
|
|
587
|
+
this.recordMissed(store, schedule, next, "timer");
|
|
588
|
+
} catch (error) {
|
|
589
|
+
if (notBefore !== undefined) this.arm(schedule, store, notBefore);
|
|
590
|
+
throw error;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
this.arm(schedule, store, notBefore);
|
|
596
594
|
}
|
|
597
595
|
|
|
598
|
-
private arm(schedule: ScheduleRecord, store: ScheduleStore): void {
|
|
596
|
+
private arm(schedule: ScheduleRecord, store: ScheduleStore, notBefore?: number): void {
|
|
599
597
|
this.clearTimer(store, schedule.id);
|
|
600
598
|
if (schedule.paused) return;
|
|
601
599
|
const next = nextRunAt(schedule);
|
|
602
600
|
if (next === undefined) return;
|
|
603
|
-
const timer = this.timersApi.setTimeout(() =>
|
|
601
|
+
const timer = this.timersApi.setTimeout(() => {
|
|
602
|
+
// A timer callback is outside every caller's try/catch, so an escaping
|
|
603
|
+
// rejection here reaches the process as an uncaught exception and exits
|
|
604
|
+
// Pi. Contain every failure to this one schedule.
|
|
605
|
+
void this.fire(store, schedule.id).catch((error) => {
|
|
606
|
+
console.warn(`[pi-subagents] Scheduled run '${schedule.id}' failed to fire: ${error instanceof Error ? error.message : String(error)}`);
|
|
607
|
+
this.restoreAfterFireError(store, schedule.id);
|
|
608
|
+
});
|
|
609
|
+
}, Math.min(Math.max(0, next - this.now(), (notBefore ?? 0) - this.now()), MAX_TIMER_DELAY_MS));
|
|
604
610
|
timer.unref?.();
|
|
605
611
|
this.timers.set(this.timerKey(store, schedule.id), timer);
|
|
606
612
|
}
|
|
607
613
|
|
|
614
|
+
private restoreAfterFireError(store: ScheduleStore, id: string): void {
|
|
615
|
+
try {
|
|
616
|
+
const schedule = store.find(id);
|
|
617
|
+
if (!schedule) return;
|
|
618
|
+
const now = this.now();
|
|
619
|
+
const planned = schedule.trigger.kind === "interval" ? duePlannedAt(schedule, now) : undefined;
|
|
620
|
+
const notBefore = planned !== undefined && planned <= now ? Date.parse(nextAfter(schedule.trigger, planned, now)!) : undefined;
|
|
621
|
+
this.restoreOne(store, schedule, notBefore, schedule.trigger.kind === "interval");
|
|
622
|
+
} catch (error) {
|
|
623
|
+
console.warn(`[pi-subagents] Scheduled run '${id}' could not be restored after fire failure: ${error instanceof Error ? error.message : String(error)}`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
608
627
|
private async fire(store: ScheduleStore, id: string): Promise<void> {
|
|
609
628
|
this.clearTimer(store, id);
|
|
610
|
-
|
|
629
|
+
// Schedules are project-scoped and shared, and `delete` only clears the
|
|
630
|
+
// timer inside the deleting process. Another session can therefore remove
|
|
631
|
+
// a schedule while this process still holds an armed timer for it; there
|
|
632
|
+
// is then nothing to run and nothing to re-arm.
|
|
633
|
+
const schedule = store.find(id);
|
|
634
|
+
if (!schedule) return;
|
|
611
635
|
const planned = duePlannedAt(schedule, this.now());
|
|
612
636
|
if (planned === undefined || schedule.paused) return;
|
|
613
637
|
if (planned > this.now()) return this.arm(schedule, store);
|
|
@@ -661,6 +685,7 @@ export class ScheduledRunManager {
|
|
|
661
685
|
if (result.isError || !asyncId) throw new Error(result.content.find((item) => item.type === "text")?.text ?? "Scheduled launch failed.");
|
|
662
686
|
run.asyncId = asyncId;
|
|
663
687
|
run.asyncDir = result.details?.asyncDir;
|
|
688
|
+
this.observedAsyncIds.add(asyncId);
|
|
664
689
|
store.writeRun(schedule, run, "schedule.run.attached_async");
|
|
665
690
|
this.arm(schedule, store);
|
|
666
691
|
return run;
|
|
@@ -695,6 +720,7 @@ export class ScheduledRunManager {
|
|
|
695
720
|
schedule.trigger.nextRunAt = nextAfter(schedule.trigger, planned, now);
|
|
696
721
|
store.writeRun(schedule, skipped, "schedule.skipped_overlap");
|
|
697
722
|
}
|
|
723
|
+
if (run.asyncId) this.observedAsyncIds.delete(run.asyncId);
|
|
698
724
|
run.state = success ? "completed" : "failed_run";
|
|
699
725
|
run.completedAt = timestamp(now);
|
|
700
726
|
if (!success && error) run.error = error;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
4
|
+
import { resultFilePath, resultPayloadPathForSessionRun, writeAsyncResultFile } from "./result-files.ts";
|
|
4
5
|
import { releaseActiveRunIndex, updateActiveRunIndex } from "./active-run-index.ts";
|
|
5
6
|
import { readStatus } from "../../shared/utils.ts";
|
|
6
7
|
import { DIRS, type AsyncParallelGroupStatus, type AsyncStatus, type NestedRunSummary, type SubagentRunMode } from "../../shared/types.ts";
|
|
@@ -98,14 +99,23 @@ interface ResultChildOutcome {
|
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
interface ResultRepairData {
|
|
101
|
-
state: "complete" | "failed" | "paused" | "stopped";
|
|
102
|
+
state: "complete" | "failed" | "paused" | "stopped" | "rejected";
|
|
102
103
|
results?: ResultChildOutcome[];
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
function regularFileExists(filePath: string): boolean {
|
|
107
|
+
try {
|
|
108
|
+
return fs.statSync(filePath).isFile();
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (isNotFoundError(error)) return false;
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
105
115
|
function readResultRepairData(resultPath: string): ResultRepairData | undefined {
|
|
106
116
|
try {
|
|
107
117
|
const data = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as { success?: boolean; state?: string; exitCode?: number; results?: unknown };
|
|
108
|
-
const state = data.success ? "complete" : data.state === "stopped" ? "stopped" : data.state === "paused" || data.exitCode === 0 ? "paused" : "failed";
|
|
118
|
+
const state = data.success ? "complete" : data.state === "stopped" ? "stopped" : data.state === "rejected" ? "rejected" : data.state === "paused" || data.exitCode === 0 ? "paused" : "failed";
|
|
109
119
|
const results = Array.isArray(data.results)
|
|
110
120
|
? data.results.map((entry, index) => {
|
|
111
121
|
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return {};
|
|
@@ -124,7 +134,7 @@ function readResultRepairData(resultPath: string): ResultRepairData | undefined
|
|
|
124
134
|
}
|
|
125
135
|
}
|
|
126
136
|
|
|
127
|
-
function childState(overallState: ResultRepairData["state"], child: ResultChildOutcome | undefined): "
|
|
137
|
+
function childState(overallState: ResultRepairData["state"], child: ResultChildOutcome | undefined): ResultRepairData["state"] {
|
|
128
138
|
if (child?.success === true) return "complete";
|
|
129
139
|
if (child?.success === false) return "failed";
|
|
130
140
|
return overallState;
|
|
@@ -197,7 +207,7 @@ function buildStartedStatus(asyncDir: string, startedRun: StartedRunMetadata, no
|
|
|
197
207
|
};
|
|
198
208
|
}
|
|
199
209
|
|
|
200
|
-
function buildFailedRepair(status: AsyncStatus, asyncDir: string, now: number, reason?: string): { status: AsyncStatus; result:
|
|
210
|
+
function buildFailedRepair(status: AsyncStatus, asyncDir: string, now: number, reason?: string): { status: AsyncStatus; result: Record<string, unknown>; message: string } {
|
|
201
211
|
const runId = status.runId || path.basename(asyncDir);
|
|
202
212
|
const pid = typeof status.pid === "number" ? status.pid : "unknown";
|
|
203
213
|
const baseMessage = reason ?? `Async runner process ${pid} exited or disappeared before writing a result. Marked run failed by stale-run reconciliation.`;
|
|
@@ -259,7 +269,7 @@ function buildFailedRepair(status: AsyncStatus, asyncDir: string, now: number, r
|
|
|
259
269
|
|
|
260
270
|
function writeFailedRepair(asyncDir: string, status: AsyncStatus, resultPath: string, now: number, reason?: string): ReconcileAsyncRunResult {
|
|
261
271
|
const repair = buildFailedRepair(status, asyncDir, now, reason);
|
|
262
|
-
|
|
272
|
+
if (repair.result.sessionId) writeAsyncResultFile(resultPath, repair.result);
|
|
263
273
|
writeAtomicJson(path.join(asyncDir, "status.json"), repair.status);
|
|
264
274
|
if (repair.status.processTerminal?.state === "observed") releaseActiveRunIndex(asyncDir);
|
|
265
275
|
appendJsonlBestEffort(path.join(asyncDir, "events.jsonl"), {
|
|
@@ -267,14 +277,14 @@ function writeFailedRepair(asyncDir: string, status: AsyncStatus, resultPath: st
|
|
|
267
277
|
ts: now,
|
|
268
278
|
runId: repair.status.runId,
|
|
269
279
|
pid: status.pid,
|
|
270
|
-
resultPath,
|
|
280
|
+
...(repair.result.sessionId ? { resultPath } : {}),
|
|
271
281
|
message: repair.message,
|
|
272
282
|
});
|
|
273
|
-
return { status: repair.status, repaired: true, resultPath, message: repair.message };
|
|
283
|
+
return { status: repair.status, repaired: true, ...(repair.result.sessionId ? { resultPath } : {}), message: repair.message };
|
|
274
284
|
}
|
|
275
285
|
|
|
276
286
|
function terminal(state: AsyncStatus["state"]): boolean {
|
|
277
|
-
return state === "complete" || state === "failed" || state === "paused" || state === "stopped";
|
|
287
|
+
return state === "complete" || state === "failed" || state === "paused" || state === "stopped" || state === "rejected";
|
|
278
288
|
}
|
|
279
289
|
|
|
280
290
|
function* nestedRuns(children: NestedRunSummary[] | undefined): Generator<NestedRunSummary> {
|
|
@@ -344,17 +354,21 @@ export function reconcileAsyncRun(asyncDir: string, options: ReconcileAsyncRunOp
|
|
|
344
354
|
if (stepRecord.thinking !== undefined && typeof stepRecord.thinking !== "string") throw new Error(`Invalid async status file '${statusPath}': steps[${index}].thinking must be a string.`);
|
|
345
355
|
}
|
|
346
356
|
const runId = effectiveStatus.runId || path.basename(asyncDir);
|
|
347
|
-
const
|
|
348
|
-
|
|
357
|
+
const resultsDir = options.resultsDir ?? DIRS.results;
|
|
358
|
+
const resultPath = resultFilePath(resultsDir, runId);
|
|
359
|
+
const existingResultPath = effectiveStatus.sessionId
|
|
360
|
+
? resultPayloadPathForSessionRun(resultsDir, effectiveStatus.sessionId, runId) ?? (regularFileExists(resultPath) ? resultPath : undefined)
|
|
361
|
+
: regularFileExists(resultPath) ? resultPath : undefined;
|
|
362
|
+
if (existingResultPath) {
|
|
349
363
|
const terminalStatus = effectiveStatus.state === "running" || effectiveStatus.state === "queued"
|
|
350
|
-
? terminalStatusFromResult(effectiveStatus,
|
|
364
|
+
? terminalStatusFromResult(effectiveStatus, existingResultPath, now)
|
|
351
365
|
: undefined;
|
|
352
366
|
if (terminalStatus) {
|
|
353
367
|
writeAtomicJson(path.join(asyncDir, "status.json"), terminalStatus);
|
|
354
368
|
if (terminalStatus.processTerminal?.state === "observed") releaseActiveRunIndex(asyncDir);
|
|
355
|
-
return { status: terminalStatus, repaired: true, resultPath, message: "Existing async result file was used to repair stale running status." };
|
|
369
|
+
return { status: terminalStatus, repaired: true, resultPath: existingResultPath, message: "Existing async result file was used to repair stale running status." };
|
|
356
370
|
}
|
|
357
|
-
if (effectiveStatus.displayDismissedAt === undefined) return { status: effectiveStatus, repaired: false, resultPath };
|
|
371
|
+
if (effectiveStatus.displayDismissedAt === undefined) return { status: effectiveStatus, repaired: false, resultPath: existingResultPath };
|
|
358
372
|
}
|
|
359
373
|
if (effectiveStatus.displayDismissedAt !== undefined) {
|
|
360
374
|
return { status: null, repaired: false, resultPath };
|