pi-cohort 7.2.0 → 7.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [7.2.2] - 2026-10-16
4
+
5
+ ### Fixed
6
+
7
+ - A background job's result file written while macOS re-arms its FSEvents stream is no longer missed until the next `session_start`. libuv rebuilds one process-wide stream on every new directory `fs.watch` (the watcher's own start, a sibling SDK session's watcher, pi's footer git watchers), and rename events in that gap were lost. The result watcher now rescans the results directory once per second while the native watcher is healthy and consumes any file it never reported; the id-based dedup makes a duplicate hit a no-op and the EMFILE/ENOSPC polling fallback is unchanged. `test/unit/multi-session.test.ts` writes each result once instead of re-publishing it every 250 ms. (#21)
8
+
9
+ ## [7.2.1] - 2026-10-16
10
+
11
+ ### Fixed
12
+
13
+ - A second pi-cohort instance loaded into the same Node process no longer stops the first instance's result watcher or drops its event subscriptions. The extension factory and `registerSubagentNotify` used to call cleanup callbacks a previous instance stored on `globalThis` (`__piSubagentRuntimeCleanup`, `__piSubagentEventUnsubscribes`, `__pi_subagents_notify_unsubscribe__`), which also retired a live session in another SDK runtime; those hooks are gone and each instance now tears down only itself in its own `session_shutdown` handler, which pi emits on reload, new, resume, fork, and quit. (#20)
14
+
3
15
  ## [7.2.0] - 2026-10-11
4
16
 
5
17
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cohort",
3
- "version": "7.2.0",
3
+ "version": "7.2.2",
4
4
  "description": "Delegate Pi work to focused child agents: code review, scouting, implementation, parallel audits, saved chains, and background jobs.",
5
5
  "author": "Jacek Juraszek",
6
6
  "license": "MIT",
@@ -42,6 +42,7 @@ function createChildSafeState(): SubagentState {
42
42
  completionSeen: new Map(),
43
43
  watcher: null,
44
44
  watcherRestartTimer: null,
45
+ safetyRescanTimer: null,
45
46
  resultFileCoalescer: {
46
47
  schedule: () => false,
47
48
  clear: () => {},
@@ -246,15 +246,6 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
246
246
  return;
247
247
  }
248
248
  const globalStore = globalThis as Record<string, unknown>;
249
- const runtimeCleanupStoreKey = "__piSubagentRuntimeCleanup";
250
- const previousRuntimeCleanup = globalStore[runtimeCleanupStoreKey];
251
- if (typeof previousRuntimeCleanup === "function") {
252
- try {
253
- previousRuntimeCleanup();
254
- } catch {
255
- // Best effort cleanup for stale timers from an older reload.
256
- }
257
- }
258
249
 
259
250
  ensureAccessibleDir(RUN_STATE_ROOT_DIR, true);
260
251
  ensureAccessibleDir(RESULTS_DIR);
@@ -280,6 +271,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
280
271
  completionSeen: new Map(),
281
272
  watcher: null,
282
273
  watcherRestartTimer: null,
274
+ safetyRescanTimer: null,
283
275
  resultFileCoalescer: {
284
276
  schedule: () => false,
285
277
  clear: () => {},
@@ -296,16 +288,6 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
296
288
  startResultWatcher();
297
289
  primeExistingResults();
298
290
 
299
- const runtimeCleanup = () => {
300
- stopResultWatcher();
301
- clearPendingForegroundControlNotices(state);
302
- if (state.poller) {
303
- clearInterval(state.poller);
304
- state.poller = null;
305
- }
306
- };
307
- globalStore[runtimeCleanupStoreKey] = runtimeCleanup;
308
-
309
291
  const { ensurePoller, handleStarted, handleComplete, resetJobs } = createAsyncJobTracker(pi, state, ASYNC_DIR);
310
292
  const executor = createSubagentExecutor({
311
293
  pi,
@@ -491,20 +473,8 @@ DIAGNOSTICS:
491
473
  pi.registerTool(tool);
492
474
  registerSlashCommands(pi, state);
493
475
 
494
- const eventUnsubscribeStoreKey = "__piSubagentEventUnsubscribes";
495
476
  const controlNoticeSeenStoreKey = "__piSubagentVisibleControlNotices";
496
- const previousEventUnsubscribes = globalStore[eventUnsubscribeStoreKey];
497
- if (Array.isArray(previousEventUnsubscribes)) {
498
- for (const unsubscribe of previousEventUnsubscribes) {
499
- if (typeof unsubscribe !== "function") continue;
500
- try {
501
- unsubscribe();
502
- } catch {
503
- // Best effort cleanup for stale handlers from an older reload.
504
- }
505
- }
506
- }
507
- registerSubagentNotify(pi);
477
+ const unsubscribeNotify = registerSubagentNotify(pi);
508
478
 
509
479
  const existingVisibleControlNotices = globalStore[controlNoticeSeenStoreKey];
510
480
  const visibleControlNotices = existingVisibleControlNotices instanceof Set ? existingVisibleControlNotices as Set<string> : new Set<string>();
@@ -518,6 +488,7 @@ DIAGNOSTICS:
518
488
  });
519
489
  };
520
490
  const eventUnsubscribes = [
491
+ unsubscribeNotify,
521
492
  pi.events.on(SUBAGENT_ASYNC_STARTED_EVENT, handleStarted),
522
493
  pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, handleComplete),
523
494
  pi.events.on(SUBAGENT_CONTROL_EVENT, controlEventHandler),
@@ -527,7 +498,6 @@ DIAGNOSTICS:
527
498
  }
528
499
  }),
529
500
  ];
530
- globalStore[eventUnsubscribeStoreKey] = eventUnsubscribes;
531
501
 
532
502
  pi.on("message_end", (event, ctx) => {
533
503
  if (event.message.role !== "assistant") return;
@@ -591,9 +561,6 @@ DIAGNOSTICS:
591
561
  // Best effort cleanup during shutdown.
592
562
  }
593
563
  }
594
- if (globalStore[eventUnsubscribeStoreKey] === eventUnsubscribes) {
595
- delete globalStore[eventUnsubscribeStoreKey];
596
- }
597
564
  stopResultWatcher();
598
565
  if (state.poller) clearInterval(state.poller);
599
566
  state.poller = null;
@@ -608,9 +575,6 @@ DIAGNOSTICS:
608
575
  slashBridge.dispose();
609
576
  promptTemplateBridge.cancelAll();
610
577
  promptTemplateBridge.dispose();
611
- if (globalStore[runtimeCleanupStoreKey] === runtimeCleanup) {
612
- delete globalStore[runtimeCleanupStoreKey];
613
- }
614
578
  try {
615
579
  if (state.lastUiContext?.hasUI) {
616
580
  state.lastUiContext.ui.setWidget(WIDGET_KEY, undefined);
@@ -40,18 +40,7 @@ interface SubagentResult {
40
40
  totalTasks?: number;
41
41
  }
42
42
 
43
- export default function registerSubagentNotify(pi: ExtensionAPI): void {
44
- const unsubscribeStoreKey = "__pi_subagents_notify_unsubscribe__";
45
- const globalStore = globalThis as Record<string, unknown>;
46
- const previousUnsubscribe = globalStore[unsubscribeStoreKey];
47
- if (typeof previousUnsubscribe === "function") {
48
- try {
49
- previousUnsubscribe();
50
- } catch {
51
- // Best effort cleanup for stale handlers from an older reload.
52
- }
53
- }
54
-
43
+ export default function registerSubagentNotify(pi: ExtensionAPI): () => void {
55
44
  const seen = getGlobalSeenMap("__pi_subagents_notify_seen__");
56
45
  const ttlMs = 10 * 60 * 1000;
57
46
 
@@ -104,5 +93,5 @@ export default function registerSubagentNotify(pi: ExtensionAPI): void {
104
93
  );
105
94
  };
106
95
 
107
- globalStore[unsubscribeStoreKey] = pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, handleComplete);
96
+ return pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, handleComplete);
108
97
  }
@@ -14,6 +14,7 @@ import { projectNestedRegistryForRoot, sanitizeSummary } from "../shared/nested-
14
14
 
15
15
  const WATCHER_RESTART_DELAY_MS = 3000;
16
16
  const POLL_INTERVAL_MS = 3000;
17
+ const SAFETY_RESCAN_INTERVAL_MS = 1000;
17
18
 
18
19
  type ResultWatcherFs = Pick<typeof fs, "existsSync" | "readFileSync" | "unlinkSync" | "readdirSync" | "mkdirSync" | "watch">;
19
20
 
@@ -196,9 +197,15 @@ export function createResultWatcher(
196
197
  }
197
198
  };
198
199
 
200
+ const clearSafetyRescan = () => {
201
+ if (state.safetyRescanTimer) timers.clearInterval(state.safetyRescanTimer);
202
+ state.safetyRescanTimer = null;
203
+ };
204
+
199
205
  const startPollingFallback = (reason: unknown) => {
200
206
  state.watcher?.close();
201
207
  state.watcher = null;
208
+ clearSafetyRescan();
202
209
  if (state.watcherRestartTimer) return;
203
210
 
204
211
  console.error(
@@ -250,9 +257,14 @@ export function createResultWatcher(
250
257
  console.error(`Subagent result watcher failed for '${resultsDir}':`, error);
251
258
  state.watcher?.close();
252
259
  state.watcher = null;
260
+ clearSafetyRescan();
253
261
  scheduleRestart();
254
262
  });
255
263
  state.watcher.unref?.();
264
+ if (!state.safetyRescanTimer) {
265
+ state.safetyRescanTimer = timers.setInterval(primeExistingResults, SAFETY_RESCAN_INTERVAL_MS);
266
+ state.safetyRescanTimer.unref?.();
267
+ }
256
268
  } catch (error) {
257
269
  if (shouldFallBackToPolling(error)) {
258
270
  startPollingFallback(error);
@@ -267,6 +279,7 @@ export function createResultWatcher(
267
279
  const stopResultWatcher = () => {
268
280
  state.watcher?.close();
269
281
  state.watcher = null;
282
+ clearSafetyRescan();
270
283
  if (state.watcherRestartTimer) {
271
284
  timers.clearTimeout(state.watcherRestartTimer);
272
285
  timers.clearInterval(state.watcherRestartTimer);
@@ -718,6 +718,7 @@ export interface SubagentState {
718
718
  completionSeen: Map<string, number>;
719
719
  watcher: FSWatcher | null;
720
720
  watcherRestartTimer: ReturnType<typeof setTimeout> | null;
721
+ safetyRescanTimer: ReturnType<typeof setInterval> | null;
721
722
  resultFileCoalescer: {
722
723
  schedule(file: string, delayMs?: number): boolean;
723
724
  clear(): void;