@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 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
- start repeats that notice until you remove the artifacts. When the changes had
302
- already been applied and only the cleanup failed, the next session start
303
- removes the retained copy itself and clears the notice.
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, prompt injection, and
350
- fresh dispatch wait for that pass so an existing id cannot be reported missing or
351
- reused. Missing recorded worktrees surface as failures without discarding the
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.18",
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
- /** Show retained recovery paths on every later session start until the user
174
- * removes the artifacts. Records whose changes already landed only need the
175
- * worktree group deleted — the step whose failure retained them — so each
176
- * session start retries that removal first and forgets records it completes.
177
- * Stale records are pruned automatically. */
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,
@@ -293,8 +293,8 @@ function projectManifestPaths(durableRoot: string): string[] {
293
293
  }
294
294
  }
295
295
 
296
- /** Every parked record across all projects, for restore and the state-root
297
- * sweeps that must see references from anywhere. */
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
- /** Rebuild interrupted records for manual recovery after reload. Orphaned children
82
- * are stopped first; missing session files do not discard isolated edits. Already-
83
- * settled records from older versions are removed with their managed artifacts. */
84
- export async function restoreDurableThreads(runtime: SubagentRuntime): Promise<number[]> {
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
  }