@ferris1225/pi-subagents 0.31.0 → 0.32.2
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/README.md +122 -71
- package/package.json +1 -1
- package/src/agents.ts +8 -13
- package/src/background.ts +59 -6
- package/src/config.ts +14 -2
- package/src/dispatch.ts +1429 -396
- package/src/format.ts +29 -4
- package/src/index.ts +7 -4
- package/src/inspector-panel.ts +363 -0
- package/src/inspector.ts +369 -0
- package/src/models.ts +192 -50
- package/src/monitor.ts +114 -7
- package/src/prompt.ts +3 -2
- package/src/recovery.ts +145 -0
- package/src/rpc-run.ts +1016 -0
- package/src/runtime.ts +167 -27
- package/src/session-fork.ts +84 -0
- package/src/setup.ts +271 -184
- package/src/spawn.ts +562 -977
- package/src/tools.ts +404 -65
- package/src/trajectory.ts +503 -0
- package/src/ui.ts +32 -16
- package/src/widget.ts +17 -4
- package/src/worktree.ts +687 -0
package/src/monitor.ts
CHANGED
|
@@ -14,12 +14,17 @@ import { stripVTControlCharacters } from "node:util";
|
|
|
14
14
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
16
16
|
import type { UsageStats } from "./spawn.ts";
|
|
17
|
+
import type { IsolationMode, WorktreeFinalizationStatus } from "./worktree.ts";
|
|
17
18
|
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
19
20
|
// Types
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
21
22
|
|
|
22
|
-
export type RunStatus = "queued" | "running" | "done" | "failed";
|
|
23
|
+
export type RunStatus = "queued" | "running" | "steering" | "interrupting" | "parked" | "done" | "failed";
|
|
24
|
+
|
|
25
|
+
export function isRunActiveStatus(status: RunStatus): boolean {
|
|
26
|
+
return status === "queued" || status === "running" || status === "steering" || status === "interrupting";
|
|
27
|
+
}
|
|
23
28
|
|
|
24
29
|
/** Soft state-awareness signals, complementary to the hard idle-kill: a run may
|
|
25
30
|
* be alive (stdout streaming) yet "stuck thinking" (no tool running for a while),
|
|
@@ -44,8 +49,14 @@ export interface RunView {
|
|
|
44
49
|
* are doing, not just their run id. */
|
|
45
50
|
label?: string;
|
|
46
51
|
model?: string;
|
|
52
|
+
/** Primary model ref when the run advanced to another candidate in its pool. */
|
|
53
|
+
modelFallbackFrom?: string;
|
|
47
54
|
/** Effective thinking strength this run was launched with (frontmatter/config/global). */
|
|
48
55
|
thinking?: string;
|
|
56
|
+
isolation?: IsolationMode;
|
|
57
|
+
integrationStatus?: "pending" | WorktreeFinalizationStatus;
|
|
58
|
+
forkedFromRunId?: number;
|
|
59
|
+
forkChildRunIds?: number[];
|
|
49
60
|
status: RunStatus;
|
|
50
61
|
usage: UsageStats;
|
|
51
62
|
/** Concise current activity ("thinking", "read src/index.ts"); last writer wins. */
|
|
@@ -83,6 +94,8 @@ export interface RunView {
|
|
|
83
94
|
export interface RunChainMeta {
|
|
84
95
|
groupId?: string;
|
|
85
96
|
relationLabel?: string;
|
|
97
|
+
isolation?: IsolationMode;
|
|
98
|
+
forkedFromRunId?: number;
|
|
86
99
|
}
|
|
87
100
|
|
|
88
101
|
// ---------------------------------------------------------------------------
|
|
@@ -311,7 +324,7 @@ export function activityStateLabel(state: ActivityState): string {
|
|
|
311
324
|
* active_long_running (total elapsed past its threshold). Both are suppressed
|
|
312
325
|
* for non-running runs. */
|
|
313
326
|
export function deriveActivityState(run: RunView, now: number = Date.now()): ActivityState | undefined {
|
|
314
|
-
if (run.status !== "running") return undefined;
|
|
327
|
+
if (run.status !== "running" && run.status !== "steering") return undefined;
|
|
315
328
|
if (!run.currentTool) {
|
|
316
329
|
const since = run.lastActivityAt ?? run.startedAt ?? now;
|
|
317
330
|
if (now - since >= NEEDS_ATTENTION_AFTER_MS) return "needs_attention";
|
|
@@ -416,7 +429,7 @@ export class MonitorStore {
|
|
|
416
429
|
// running) are also preserved — their status is "done" but they must
|
|
417
430
|
// stay visible until the chain resolves.
|
|
418
431
|
this.runs = this.runs.filter(
|
|
419
|
-
(r) => r.status
|
|
432
|
+
(r) => isRunActiveStatus(r.status) || r.status === "parked" || r.retained,
|
|
420
433
|
);
|
|
421
434
|
this.notify();
|
|
422
435
|
}
|
|
@@ -434,6 +447,8 @@ export class MonitorStore {
|
|
|
434
447
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
435
448
|
...(meta?.groupId ? { groupId: meta.groupId } : {}),
|
|
436
449
|
...(meta?.relationLabel ? { relationLabel: meta.relationLabel } : {}),
|
|
450
|
+
...(meta?.isolation ? { isolation: meta.isolation, integrationStatus: meta.isolation === "worktree" ? "pending" : undefined } : {}),
|
|
451
|
+
...(meta?.forkedFromRunId !== undefined ? { forkedFromRunId: meta.forkedFromRunId } : {}),
|
|
437
452
|
});
|
|
438
453
|
this.notify();
|
|
439
454
|
return id;
|
|
@@ -443,13 +458,13 @@ export class MonitorStore {
|
|
|
443
458
|
const run = this.find(id);
|
|
444
459
|
if (!run) return;
|
|
445
460
|
run.status = status;
|
|
446
|
-
if (status === "running") {
|
|
461
|
+
if (status === "running" || status === "steering" || status === "interrupting") {
|
|
447
462
|
if (run.startedAt === undefined) run.startedAt = Date.now();
|
|
448
|
-
// A model-fallback retry
|
|
463
|
+
// A model-fallback retry or resumed generation restarts the clock; a
|
|
449
464
|
// stale endedAt would freeze the elapsed display at the first attempt.
|
|
450
465
|
if (run.endedAt !== undefined) run.endedAt = undefined;
|
|
451
466
|
run.lastActivityAt = Date.now();
|
|
452
|
-
} else if ((status === "done" || status === "failed") && run.endedAt === undefined) {
|
|
467
|
+
} else if ((status === "parked" || status === "done" || status === "failed") && run.endedAt === undefined) {
|
|
453
468
|
run.endedAt = Date.now();
|
|
454
469
|
}
|
|
455
470
|
this.notify();
|
|
@@ -463,6 +478,15 @@ export class MonitorStore {
|
|
|
463
478
|
this.notify();
|
|
464
479
|
}
|
|
465
480
|
|
|
481
|
+
/** Record the final actual model and primary-to-backup transition. */
|
|
482
|
+
setModel(id: number, model?: string, fallbackFrom?: string): void {
|
|
483
|
+
const run = this.find(id);
|
|
484
|
+
if (!run) return;
|
|
485
|
+
if (model) run.model = model;
|
|
486
|
+
run.modelFallbackFrom = fallbackFrom;
|
|
487
|
+
this.notify();
|
|
488
|
+
}
|
|
489
|
+
|
|
466
490
|
/** Update the run's current one-line activity (what it is doing now). */
|
|
467
491
|
setActivity(id: number, text: string): void {
|
|
468
492
|
const run = this.find(id);
|
|
@@ -520,6 +544,73 @@ export class MonitorStore {
|
|
|
520
544
|
this.notify();
|
|
521
545
|
}
|
|
522
546
|
|
|
547
|
+
setIsolation(id: number, isolation: IsolationMode, integrationStatus?: "pending" | WorktreeFinalizationStatus): void {
|
|
548
|
+
const run = this.find(id);
|
|
549
|
+
if (!run) return;
|
|
550
|
+
run.isolation = isolation;
|
|
551
|
+
run.integrationStatus = integrationStatus;
|
|
552
|
+
this.notify();
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
setForkRelation(sourceRunId: number, childRunId: number): void {
|
|
556
|
+
const source = this.find(sourceRunId);
|
|
557
|
+
if (source) {
|
|
558
|
+
source.forkChildRunIds ??= [];
|
|
559
|
+
if (!source.forkChildRunIds.includes(childRunId)) source.forkChildRunIds.push(childRunId);
|
|
560
|
+
}
|
|
561
|
+
const child = this.find(childRunId);
|
|
562
|
+
if (child) child.forkedFromRunId = sourceRunId;
|
|
563
|
+
this.notify();
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** Update the objective shown for a queued retarget or resumed generation. */
|
|
567
|
+
setTask(id: number, task: string): void {
|
|
568
|
+
const run = this.find(id);
|
|
569
|
+
if (!run) return;
|
|
570
|
+
run.task = task;
|
|
571
|
+
run.label = runLabel(task);
|
|
572
|
+
this.notify();
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** Reuse a stable logical run id for a resumed generation. */
|
|
576
|
+
restartRun(id: number, agent: string, task: string, model?: string, thinking?: string, isolation?: IsolationMode): void {
|
|
577
|
+
const run = this.find(id);
|
|
578
|
+
if (!run) {
|
|
579
|
+
this.runs.push({
|
|
580
|
+
id,
|
|
581
|
+
agent,
|
|
582
|
+
task,
|
|
583
|
+
label: runLabel(task),
|
|
584
|
+
model,
|
|
585
|
+
thinking,
|
|
586
|
+
...(isolation ? { isolation, integrationStatus: isolation === "worktree" ? "pending" as const : undefined } : {}),
|
|
587
|
+
status: "queued",
|
|
588
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
589
|
+
});
|
|
590
|
+
this.notify();
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
run.agent = agent;
|
|
594
|
+
run.task = task;
|
|
595
|
+
run.label = runLabel(task);
|
|
596
|
+
run.model = model;
|
|
597
|
+
run.thinking = thinking;
|
|
598
|
+
if (isolation) run.isolation = isolation;
|
|
599
|
+
run.integrationStatus = isolation === "worktree" ? "pending" : undefined;
|
|
600
|
+
run.status = "queued";
|
|
601
|
+
run.usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 };
|
|
602
|
+
run.activity = undefined;
|
|
603
|
+
run.toolCount = undefined;
|
|
604
|
+
run.currentTool = undefined;
|
|
605
|
+
run.lastActivityAt = undefined;
|
|
606
|
+
run.startedAt = undefined;
|
|
607
|
+
run.endedAt = undefined;
|
|
608
|
+
run.annotation = undefined;
|
|
609
|
+
run.summary = undefined;
|
|
610
|
+
run.retained = undefined;
|
|
611
|
+
this.notify();
|
|
612
|
+
}
|
|
613
|
+
|
|
523
614
|
/** Look up a run by id without removing it. */
|
|
524
615
|
findRun(id: number): RunView | undefined {
|
|
525
616
|
return this.find(id);
|
|
@@ -560,6 +651,7 @@ export class MonitorStore {
|
|
|
560
651
|
if (run.summary) parts.push(run.summary);
|
|
561
652
|
if (run.model) parts.push(run.model);
|
|
562
653
|
if (run.thinking) parts.push(`thinking ${run.thinking}`);
|
|
654
|
+
if (run.isolation === "worktree") parts.push(`worktree ${run.integrationStatus ?? "active"}`);
|
|
563
655
|
if (usage) parts.push(usage);
|
|
564
656
|
const elapsed = formatElapsed(run);
|
|
565
657
|
if (elapsed) parts.push(elapsed);
|
|
@@ -591,6 +683,12 @@ export function statusIcon(status: RunStatus, theme: Theme): string {
|
|
|
591
683
|
switch (status) {
|
|
592
684
|
case "running":
|
|
593
685
|
return theme.fg("accent", "●");
|
|
686
|
+
case "steering":
|
|
687
|
+
return theme.fg("accent", "◆");
|
|
688
|
+
case "interrupting":
|
|
689
|
+
return theme.fg("warning", "◐");
|
|
690
|
+
case "parked":
|
|
691
|
+
return theme.fg("dim", "■");
|
|
594
692
|
case "done":
|
|
595
693
|
return theme.fg("success", "✓");
|
|
596
694
|
case "failed":
|
|
@@ -607,6 +705,12 @@ export function statusLabel(status: RunStatus): string {
|
|
|
607
705
|
return "ready";
|
|
608
706
|
case "running":
|
|
609
707
|
return "running";
|
|
708
|
+
case "steering":
|
|
709
|
+
return "steering";
|
|
710
|
+
case "interrupting":
|
|
711
|
+
return "interrupting";
|
|
712
|
+
case "parked":
|
|
713
|
+
return "parked";
|
|
610
714
|
case "done":
|
|
611
715
|
return "done";
|
|
612
716
|
case "failed":
|
|
@@ -615,10 +719,13 @@ export function statusLabel(status: RunStatus): string {
|
|
|
615
719
|
}
|
|
616
720
|
|
|
617
721
|
/** Theme color matching the status label. */
|
|
618
|
-
export function statusColor(status: RunStatus): "accent" | "success" | "error" | "dim" {
|
|
722
|
+
export function statusColor(status: RunStatus): "accent" | "success" | "error" | "warning" | "dim" {
|
|
619
723
|
switch (status) {
|
|
620
724
|
case "running":
|
|
725
|
+
case "steering":
|
|
621
726
|
return "accent";
|
|
727
|
+
case "interrupting":
|
|
728
|
+
return "warning";
|
|
622
729
|
case "done":
|
|
623
730
|
return "success";
|
|
624
731
|
case "failed":
|
package/src/prompt.ts
CHANGED
|
@@ -58,8 +58,9 @@ ${routing ? `Routing:\n${routing}\n` : ""}Dispatch discipline:
|
|
|
58
58
|
- Delegate only when isolation genuinely pays: a self-contained implementation/fix with its own validation (worker), or a fresh-context review gate (reviewer).
|
|
59
59
|
- When in doubt, start with a direct tool call in the main context; escalate to \`explore\` as soon as the search turns broad or crosses multiple files.
|
|
60
60
|
- For an already-known or trivial target, use a direct search/read tool (e.g. grep/find/read) — do not over-delegate a one-line lookup.
|
|
61
|
-
${hasMultiple ? "- Run INDEPENDENT tasks in parallel: one subagent call with a `tasks` array, and track them with your todo list. Let the automatically resumed main agent launch dependent work only after its prerequisite result arrives (e.g. explore, then worker, then reviewer).\n" : ""}-
|
|
62
|
-
-
|
|
61
|
+
${hasMultiple ? "- Run INDEPENDENT tasks in parallel: one subagent call with a `tasks` array, and track them with your todo list. Parallel worker items default to detached Git worktree isolation; pass `isolation: \"shared\"` only when a worker intentionally needs the caller's live uncommitted tree. Let the automatically resumed main agent launch dependent work only after its prerequisite result arrives (e.g. explore, then worker, then reviewer).\n" : ""}- Single dispatch stays in the shared working tree by default. Use \`isolation: "worktree"\` only for worker/write-capable agents in a Git repository; never request it for explore/reviewer, and never silently retry shared after setup fails.
|
|
62
|
+
- Brief each sub-agent as self-contained: goal, exact paths, constraints, expected output. It has NO memory of this conversation.
|
|
63
|
+
- Treat delegated agents as leaf workers: do not ask a sub-agent to dispatch another sub-agent; child processes do not have this tool. Use \`subagent_control fork\` on a parked/settled retained thread when you need an independent continuation with preserved context and a new run id.
|
|
63
64
|
- Trust but verify: a sub-agent's summary describes intent, not outcome. Check the actual changes/results before reporting work done.
|
|
64
65
|
|
|
65
66
|
Vision tasks:
|
package/src/recovery.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/** Durable handoff for worktree integration/cleanup failures across sessions. */
|
|
2
|
+
|
|
3
|
+
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { stripVTControlCharacters } from "node:util";
|
|
8
|
+
import type { WorktreeFinalization } from "./worktree.ts";
|
|
9
|
+
|
|
10
|
+
export const RECOVERY_MANIFEST_FILE_NAME = "pi-subagents-recovery.json";
|
|
11
|
+
const RECOVERY_MANIFEST_VERSION = 1;
|
|
12
|
+
|
|
13
|
+
export interface RecoveryRecord {
|
|
14
|
+
runId: number;
|
|
15
|
+
createdAt: number;
|
|
16
|
+
integrated: boolean;
|
|
17
|
+
worktreePath?: string;
|
|
18
|
+
patchPath?: string;
|
|
19
|
+
error?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface RecoveryManifest {
|
|
23
|
+
version: number;
|
|
24
|
+
records: RecoveryRecord[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getRecoveryManifestPath(configPath: string): string {
|
|
28
|
+
return join(dirname(configPath), RECOVERY_MANIFEST_FILE_NAME);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeRecord(value: unknown): RecoveryRecord | undefined {
|
|
32
|
+
if (!value || typeof value !== "object") return undefined;
|
|
33
|
+
const raw = value as Record<string, unknown>;
|
|
34
|
+
if (typeof raw.runId !== "number" || !Number.isInteger(raw.runId) || raw.runId < 1) return undefined;
|
|
35
|
+
if (typeof raw.createdAt !== "number" || !Number.isFinite(raw.createdAt)) return undefined;
|
|
36
|
+
return {
|
|
37
|
+
runId: raw.runId,
|
|
38
|
+
createdAt: raw.createdAt,
|
|
39
|
+
integrated: raw.integrated === true,
|
|
40
|
+
...(typeof raw.worktreePath === "string" && raw.worktreePath ? { worktreePath: raw.worktreePath } : {}),
|
|
41
|
+
...(typeof raw.patchPath === "string" && raw.patchPath ? { patchPath: raw.patchPath } : {}),
|
|
42
|
+
...(typeof raw.error === "string" && raw.error ? { error: raw.error } : {}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function readRecoveryRecords(configPath: string): Promise<RecoveryRecord[]> {
|
|
47
|
+
try {
|
|
48
|
+
const parsed = JSON.parse(await readFile(getRecoveryManifestPath(configPath), "utf8")) as {
|
|
49
|
+
records?: unknown;
|
|
50
|
+
};
|
|
51
|
+
if (!Array.isArray(parsed.records)) return [];
|
|
52
|
+
return parsed.records.flatMap((record) => {
|
|
53
|
+
const normalized = normalizeRecord(record);
|
|
54
|
+
return normalized ? [normalized] : [];
|
|
55
|
+
});
|
|
56
|
+
} catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function recoveryKey(record: RecoveryRecord): string {
|
|
62
|
+
return `${record.runId}\0${record.worktreePath ?? ""}\0${record.patchPath ?? ""}\0${record.error ?? ""}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function writeManifest(path: string, records: readonly RecoveryRecord[]): Promise<void> {
|
|
66
|
+
if (records.length === 0) {
|
|
67
|
+
await rm(path, { force: true });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
await mkdir(dirname(path), { recursive: true });
|
|
71
|
+
const temporaryPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
72
|
+
try {
|
|
73
|
+
const manifest: RecoveryManifest = {
|
|
74
|
+
version: RECOVERY_MANIFEST_VERSION,
|
|
75
|
+
records: [...records],
|
|
76
|
+
};
|
|
77
|
+
await writeFile(temporaryPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
78
|
+
await rename(temporaryPath, path);
|
|
79
|
+
} finally {
|
|
80
|
+
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Merge retained artifacts into the durable manifest. */
|
|
85
|
+
export async function persistRecoveryRecords(
|
|
86
|
+
configPath: string,
|
|
87
|
+
records: readonly RecoveryRecord[],
|
|
88
|
+
): Promise<void> {
|
|
89
|
+
if (records.length === 0) return;
|
|
90
|
+
const path = getRecoveryManifestPath(configPath);
|
|
91
|
+
await withFileMutationQueue(path, async () => {
|
|
92
|
+
const merged = new Map<string, RecoveryRecord>();
|
|
93
|
+
for (const record of await readRecoveryRecords(configPath)) merged.set(recoveryKey(record), record);
|
|
94
|
+
for (const record of records) merged.set(recoveryKey(record), record);
|
|
95
|
+
await writeManifest(path, [...merged.values()]);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function recoveryRecordFromFinalization(
|
|
100
|
+
runId: number,
|
|
101
|
+
finalization: WorktreeFinalization,
|
|
102
|
+
now = Date.now(),
|
|
103
|
+
): RecoveryRecord {
|
|
104
|
+
return {
|
|
105
|
+
runId,
|
|
106
|
+
createdAt: now,
|
|
107
|
+
integrated: finalization.integrated,
|
|
108
|
+
...(finalization.worktreePath ? { worktreePath: finalization.worktreePath } : {}),
|
|
109
|
+
...(finalization.patchPath ? { patchPath: finalization.patchPath } : {}),
|
|
110
|
+
...(finalization.error ? { error: finalization.error } : {}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Show retained recovery paths on every later session start until the user
|
|
115
|
+
* removes the artifacts. Stale records are pruned automatically. */
|
|
116
|
+
export async function announceRecoveryRecords(
|
|
117
|
+
configPath: string,
|
|
118
|
+
ctx: {
|
|
119
|
+
hasUI?: boolean;
|
|
120
|
+
ui: { notify(message: string, kind: "info" | "warning" | "error"): void };
|
|
121
|
+
},
|
|
122
|
+
): Promise<void> {
|
|
123
|
+
if (ctx.hasUI === false) return;
|
|
124
|
+
const records = await readRecoveryRecords(configPath);
|
|
125
|
+
if (records.length === 0) return;
|
|
126
|
+
const live = records.filter((record) =>
|
|
127
|
+
(record.worktreePath ? existsSync(record.worktreePath) : false) ||
|
|
128
|
+
(record.patchPath ? existsSync(record.patchPath) : false),
|
|
129
|
+
);
|
|
130
|
+
if (live.length !== records.length) {
|
|
131
|
+
const path = getRecoveryManifestPath(configPath);
|
|
132
|
+
await withFileMutationQueue(path, () => writeManifest(path, live)).catch(() => undefined);
|
|
133
|
+
}
|
|
134
|
+
for (const record of live) {
|
|
135
|
+
const paths = [
|
|
136
|
+
record.worktreePath ? `worktree ${stripVTControlCharacters(record.worktreePath)}` : undefined,
|
|
137
|
+
record.patchPath ? `patch ${stripVTControlCharacters(record.patchPath)}` : undefined,
|
|
138
|
+
].filter(Boolean).join(" · ");
|
|
139
|
+
const reason = record.error ? ` · ${stripVTControlCharacters(record.error)}` : "";
|
|
140
|
+
ctx.ui.notify(
|
|
141
|
+
`pi-subagents recovery for run #${record.runId}: ${record.integrated ? "changes were applied but cleanup failed" : "integration failed"}${paths ? ` · retained ${paths}` : ""}${reason}`,
|
|
142
|
+
"error",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|