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.
|
|
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",
|
package/src/extension/index.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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);
|
package/src/shared/types.ts
CHANGED
|
@@ -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;
|