@ferris1225/pi-subagents 4.3.18 → 4.3.19
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 +9 -0
- package/README.md +14 -7
- package/index.ts +2 -2
- package/package.json +1 -1
- package/src/isolation/recovery.ts +22 -9
- package/src/lifecycle/durable.ts +2 -2
- package/src/lifecycle/thread-restore.ts +16 -6
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
Release notes for `@ferris1225/pi-subagents`. Only the most recent releases
|
|
4
4
|
are kept here; every published version is preserved as a GitHub Release.
|
|
5
5
|
|
|
6
|
+
## 4.3.19
|
|
7
|
+
|
|
8
|
+
- Scope session-start thread restore and recovery notices to the current
|
|
9
|
+
checkout. A second pi window in another project no longer announces the first
|
|
10
|
+
project's retained worktree, retries its leftover cleanup, or restores and
|
|
11
|
+
stops its interrupted children.
|
|
12
|
+
- Cover sibling-window isolation with recovery announcement, leftover-cleanup,
|
|
13
|
+
and parked-thread restore checks across two checkouts that share one agent dir.
|
|
14
|
+
|
|
6
15
|
## 4.3.18
|
|
7
16
|
|
|
8
17
|
- Keep known-context local changes in main and delegate only substantial,
|
package/README.md
CHANGED
|
@@ -12,6 +12,10 @@ default and delegates when an independent child has a concrete advantage.
|
|
|
12
12
|
|
|
13
13
|
## What's new
|
|
14
14
|
|
|
15
|
+
**4.3.19** — session-start restore and recovery notices stay inside the current
|
|
16
|
+
project. A second pi window no longer surfaces another checkout's retained
|
|
17
|
+
worktree or restores (and would otherwise stop) that project's interrupted runs.
|
|
18
|
+
|
|
15
19
|
**4.3.18** — main-first delegation: keep known-context local changes in main,
|
|
16
20
|
use cleanup and review roles only where they add value, and configure process
|
|
17
21
|
capacity with `maxConcurrentAgents`. See [Evaluate delegation](#evaluate-delegation)
|
|
@@ -298,9 +302,10 @@ Third-party Pi packages execute as trusted code and must be reviewed accordingly
|
|
|
298
302
|
as a lane wait, not as slot queueing, and its process slot is already released.
|
|
299
303
|
- Setup and integration failures keep the useful patch and worktree, and record
|
|
300
304
|
where they are in `~/.pi/agent/ferris-pi-subagents/pi-subagents-recovery.json`.
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
305
|
+
Later session starts in that same project repeat the notice until you remove
|
|
306
|
+
the artifacts. A different project's pi window does not show or retry them.
|
|
307
|
+
When the changes had already been applied and only the cleanup failed, the next
|
|
308
|
+
session start in that project removes the retained copy itself and clears the notice.
|
|
304
309
|
|
|
305
310
|
## Runs: status and stop
|
|
306
311
|
|
|
@@ -346,9 +351,10 @@ attempt. Worktree integration failures keep their recovery artifacts.
|
|
|
346
351
|
|
|
347
352
|
Interrupted work retains a durable record and any session/worktree artifacts for
|
|
348
353
|
manual recovery after reload or crash. Missing session files no longer discard
|
|
349
|
-
isolated edits. Restore runs at session start; lookup tools,
|
|
350
|
-
fresh dispatch wait for that pass so an existing id cannot
|
|
351
|
-
reused.
|
|
354
|
+
isolated edits. Restore for this checkout runs at session start; lookup tools,
|
|
355
|
+
prompt injection, and fresh dispatch wait for that pass so an existing id cannot
|
|
356
|
+
be reported missing or reused. A sibling window in another project leaves those
|
|
357
|
+
records untouched. Missing recorded worktrees surface as failures without discarding the
|
|
352
358
|
remaining recovery evidence.
|
|
353
359
|
|
|
354
360
|
Canonical managed-path and repository validation remains in place. Invalid records
|
|
@@ -612,7 +618,8 @@ Cleanup runs at session start and is deliberately conservative. A directory goes
|
|
|
612
618
|
away only when the process that created it is gone and no valid manifest record still
|
|
613
619
|
claims it, so a live sibling pi instance never loses state and interrupted or recovery-owned
|
|
614
620
|
work outlives its own process by design. Thread and recovery references always beat an
|
|
615
|
-
age rule.
|
|
621
|
+
age rule. Restore and recovery notices themselves are scoped to the current checkout:
|
|
622
|
+
opening pi in another project does not restore, announce, or stop the first project's runs.
|
|
616
623
|
|
|
617
624
|
## Development
|
|
618
625
|
|
package/index.ts
CHANGED
|
@@ -83,8 +83,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
83
83
|
},
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
pi.on("session_start", async () => {
|
|
87
|
-
await bootstrapDurableState(runtime);
|
|
86
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
87
|
+
await bootstrapDurableState(runtime, ctx.cwd);
|
|
88
88
|
});
|
|
89
89
|
registerAnnouncements(pi, runtime);
|
|
90
90
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ferris1225/pi-subagents",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.19",
|
|
4
4
|
"description": "A managed sub-agent team for pi: scout, artisan, steward, and sentinel roles, one-shot runs, read-only status, and Git worktree isolation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -5,8 +5,8 @@ import { existsSync } from "node:fs";
|
|
|
5
5
|
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import { stripVTControlCharacters } from "node:util";
|
|
8
|
-
import { getSubagentsRoot } from "../execution/spawn.ts";
|
|
9
|
-
import { managedRecoveryGroup } from "./managed-paths.ts";
|
|
8
|
+
import { getProjectRoot, getSubagentsRoot } from "../execution/spawn.ts";
|
|
9
|
+
import { managedRecoveryGroup, samePath } from "./managed-paths.ts";
|
|
10
10
|
import { removeWorktreeGroup, type WorktreeFinalization } from "./worktree.ts";
|
|
11
11
|
|
|
12
12
|
export const RECOVERY_MANIFEST_FILE_NAME = "pi-subagents-recovery.json";
|
|
@@ -170,25 +170,37 @@ export function recoveryRecordFromFinalization(
|
|
|
170
170
|
};
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
173
|
+
function recoveryBelongsToSession(
|
|
174
|
+
configPath: string,
|
|
175
|
+
cwd: string,
|
|
176
|
+
groupDir: string,
|
|
177
|
+
): boolean {
|
|
178
|
+
return samePath(dirname(dirname(groupDir)), getProjectRoot(configPath, cwd));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Show retained recovery paths on later session starts in the same project
|
|
182
|
+
* until the user removes the artifacts. A sibling pi window in another checkout
|
|
183
|
+
* must not retry cleanup or surface another project's worktree. Records whose
|
|
184
|
+
* changes already landed only need the worktree group deleted — the step whose
|
|
185
|
+
* failure retained them — so that project's next session retries that removal
|
|
186
|
+
* first and forgets records it completes. Stale records are pruned automatically. */
|
|
178
187
|
export async function announceRecoveryRecords(
|
|
179
188
|
configPath: string,
|
|
180
189
|
ctx: {
|
|
181
190
|
hasUI?: boolean;
|
|
191
|
+
cwd: string;
|
|
182
192
|
ui: { notify(message: string, kind: "info" | "warning" | "error"): void };
|
|
183
193
|
},
|
|
184
194
|
): Promise<void> {
|
|
185
195
|
if (ctx.hasUI === false) return;
|
|
186
196
|
const records = await readRecoveryRecords(configPath);
|
|
187
197
|
if (records.length === 0) return;
|
|
198
|
+
const local = new Set<RecoveryRecord>();
|
|
188
199
|
for (const record of records) {
|
|
189
|
-
if (!record.integrated || !record.worktreePath) continue;
|
|
190
200
|
const groupDir = await managedRecoveryGroup(configPath, record);
|
|
191
|
-
if (!groupDir) continue;
|
|
201
|
+
if (!groupDir || !recoveryBelongsToSession(configPath, ctx.cwd, groupDir)) continue;
|
|
202
|
+
local.add(record);
|
|
203
|
+
if (!record.integrated || !record.worktreePath) continue;
|
|
192
204
|
if (!existsSync(record.worktreePath) && !(record.patchPath ? existsSync(record.patchPath) : false)) continue;
|
|
193
205
|
await removeWorktreeGroup({
|
|
194
206
|
worktreePath: record.worktreePath,
|
|
@@ -204,6 +216,7 @@ export async function announceRecoveryRecords(
|
|
|
204
216
|
await withFileMutationQueue(path, () => writeManifest(path, live)).catch(() => undefined);
|
|
205
217
|
}
|
|
206
218
|
for (const record of live) {
|
|
219
|
+
if (!local.has(record)) continue;
|
|
207
220
|
const paths = [
|
|
208
221
|
record.worktreePath ? `worktree ${stripVTControlCharacters(record.worktreePath)}` : undefined,
|
|
209
222
|
record.patchPath ? `patch ${stripVTControlCharacters(record.patchPath)}` : undefined,
|
package/src/lifecycle/durable.ts
CHANGED
|
@@ -293,8 +293,8 @@ function projectManifestPaths(durableRoot: string): string[] {
|
|
|
293
293
|
}
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
-
/** Every parked record across all projects
|
|
297
|
-
* sweeps
|
|
296
|
+
/** Every parked record across all projects. Session restore filters to the
|
|
297
|
+
* current checkout; state-root sweeps still need references from anywhere. */
|
|
298
298
|
export async function readThreadRecords(configPath: string): Promise<ThreadRecord[]> {
|
|
299
299
|
const manifests = await Promise.all(
|
|
300
300
|
projectManifestPaths(getSubagentsRoot(configPath))
|
|
@@ -16,12 +16,14 @@ import { monitor } from "../presentation/monitor.ts";
|
|
|
16
16
|
import { emptyUsage } from "../execution/rpc-control.ts";
|
|
17
17
|
import type { SubagentRuntime, SubagentThread, ThreadState } from "./runtime.ts";
|
|
18
18
|
import {
|
|
19
|
+
getProjectRoot,
|
|
19
20
|
getSubagentsRoot,
|
|
20
21
|
RpcRunControl,
|
|
21
22
|
sessionExists,
|
|
22
23
|
sweepProjectResultArtifacts,
|
|
23
24
|
type SingleResult,
|
|
24
25
|
} from "../execution/spawn.ts";
|
|
26
|
+
import { samePath } from "../isolation/managed-paths.ts";
|
|
25
27
|
import { isProcessAlive, killProcessTree, sweepProjectDurableDirs, sweepProjectTempDirs } from "../isolation/temp-hygiene.ts";
|
|
26
28
|
import { readRecoveryRecords, referencedRecoveryPaths } from "../isolation/recovery.ts";
|
|
27
29
|
import {
|
|
@@ -78,13 +80,21 @@ function createRestoredThread(
|
|
|
78
80
|
return thread;
|
|
79
81
|
}
|
|
80
82
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
function belongsToSessionProject(configPath: string, cwd: string, record: ThreadRecord): boolean {
|
|
84
|
+
return samePath(getProjectRoot(configPath, record.cwd), getProjectRoot(configPath, cwd));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Rebuild interrupted records for this session's checkout after reload.
|
|
88
|
+
* Other projects' parked threads stay on disk for their own window; this process
|
|
89
|
+
* must not restore them or kill their children. Orphaned children of *this*
|
|
90
|
+
* checkout are stopped first; missing session files do not discard isolated
|
|
91
|
+
* edits. Already-settled records from older versions are removed with their
|
|
92
|
+
* managed artifacts. */
|
|
93
|
+
export async function restoreDurableThreads(runtime: SubagentRuntime, cwd: string): Promise<number[]> {
|
|
85
94
|
const records = await readThreadRecords(runtime.configPath);
|
|
86
95
|
const restoredIds: number[] = [];
|
|
87
96
|
for (const record of records) {
|
|
97
|
+
if (!belongsToSessionProject(runtime.configPath, cwd, record)) continue;
|
|
88
98
|
if (runtime.threads.has(record.runId) || monitor.findRun(record.runId)) continue;
|
|
89
99
|
if (record.state !== "parked") {
|
|
90
100
|
await discardRestoredRecord(runtime, record);
|
|
@@ -182,10 +192,10 @@ export async function restoreDurableThreads(runtime: SubagentRuntime): Promise<n
|
|
|
182
192
|
* so callers that must see restored threads await that pass alone and never the
|
|
183
193
|
* hygiene sweeps behind it. Hygiene still runs after restore: pruning decides
|
|
184
194
|
* what to delete from the records restore has already claimed. */
|
|
185
|
-
export function bootstrapDurableState(runtime: SubagentRuntime): Promise<void> {
|
|
195
|
+
export function bootstrapDurableState(runtime: SubagentRuntime, cwd: string): Promise<void> {
|
|
186
196
|
const restore = (async () => {
|
|
187
197
|
try {
|
|
188
|
-
runtime.restoredRunIds = await restoreDurableThreads(runtime);
|
|
198
|
+
runtime.restoredRunIds = await restoreDurableThreads(runtime, cwd);
|
|
189
199
|
} catch {
|
|
190
200
|
/* restore is best-effort */
|
|
191
201
|
}
|