pi-cohort 7.2.1 → 7.2.3
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 +12 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/extension/fanout-child.ts +1 -0
- package/src/extension/index.ts +1 -0
- package/src/runs/background/result-watcher.ts +13 -0
- package/src/runs/foreground/subagent-executor.ts +3 -2
- package/src/runs/shared/pi-spawn.ts +38 -33
- package/src/shared/types.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [7.2.3] - 2026-10-18
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Subagents start from SDK-hosted sessions (Pi Web, standalone SDK scripts) whose `process.argv[1]` is not Pi's CLI. `capturePiRuntime` still identifies the parent from `argv[1]` first; when that entry is not Pi's declared CLI, the executor supplies the installation the extension was loaded from (`getPackageDir()` from `@earendil-works/pi-coding-agent`), validated against that package's `bin.pi` through the same ownership check, so a sibling Pi earlier on `PATH` is never selected. Sessions launched through Pi's CLI capture exactly as before. (#22)
|
|
8
|
+
|
|
9
|
+
## [7.2.2] - 2026-10-16
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- 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)
|
|
14
|
+
|
|
3
15
|
## [7.2.1] - 2026-10-16
|
|
4
16
|
|
|
5
17
|
### Fixed
|
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ pi install npm:pi-cohort
|
|
|
39
39
|
|
|
40
40
|
That is the only required step.
|
|
41
41
|
|
|
42
|
-
Children and detached coordinators use the originating parent's resolved Node executable and Pi entry path directly, bypassing `PATH`. Revival keeps that original runtime even when requested from a different parent installation.
|
|
42
|
+
Children and detached coordinators use the originating parent's resolved Node executable and Pi entry path directly, bypassing `PATH`. Revival keeps that original runtime even when requested from a different parent installation. When the parent is not launched through Pi's CLI (an SDK-hosted session such as Pi Web, where `process.argv[1]` is the host's entry), children use the Pi installation this extension was loaded from: the package directory Pi reports for itself, accepted only when it is `@earendil-works/pi-coding-agent` and its declared `bin.pi` resolves inside that package. Single-file Pi binaries and hosts whose `process.execPath` is not a Node binary remain unsupported. These paths are not a version lock: replacing Node or Pi in place changes what runs. If the original installation is removed, restore it at the recorded paths or start a new run; there is no fallback to the current parent's installation. Legacy records without an original runtime identity remain inspectable but cannot be revived - start a new run.
|
|
43
43
|
|
|
44
44
|
## Execution backend extension API
|
|
45
45
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cohort",
|
|
3
|
-
"version": "7.2.
|
|
3
|
+
"version": "7.2.3",
|
|
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
|
@@ -271,6 +271,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
271
271
|
completionSeen: new Map(),
|
|
272
272
|
watcher: null,
|
|
273
273
|
watcherRestartTimer: null,
|
|
274
|
+
safetyRescanTimer: null,
|
|
274
275
|
resultFileCoalescer: {
|
|
275
276
|
schedule: () => false,
|
|
276
277
|
clear: () => {},
|
|
@@ -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);
|
|
@@ -3,7 +3,7 @@ import { capturePiRuntime, parsePiRuntime, type PiRuntimeIdentity } from "../sha
|
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
6
|
-
import type
|
|
6
|
+
import { getPackageDir, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import type { AgentConfig, AgentScope } from "../../agents/agents.ts";
|
|
8
8
|
import { getArtifactsDir } from "../../shared/artifacts.ts";
|
|
9
9
|
import { ChainClarifyComponent, type ChainClarifyResult } from "./chain-clarify.ts";
|
|
@@ -2120,7 +2120,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
2120
2120
|
let runtime: PiRuntimeIdentity | undefined;
|
|
2121
2121
|
let runtimeError: unknown;
|
|
2122
2122
|
try {
|
|
2123
|
-
|
|
2123
|
+
// SDK hosts are not launched through Pi's CLI; the installation this extension was loaded from is the fallback.
|
|
2124
|
+
runtime = (deps.captureRuntime ?? (() => capturePiRuntime({ installRoot: getPackageDir() })))();
|
|
2124
2125
|
} catch (error) {
|
|
2125
2126
|
runtimeError = error;
|
|
2126
2127
|
}
|
|
@@ -1,36 +1,8 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
3
|
|
|
5
4
|
export const PI_CODING_AGENT_PACKAGE = "@earendil-works/pi-coding-agent";
|
|
6
5
|
|
|
7
|
-
export function findPiPackageRootFromEntry(entryPoint: string): string | undefined {
|
|
8
|
-
let dir = path.dirname(entryPoint);
|
|
9
|
-
while (true) {
|
|
10
|
-
const packageJsonPath = path.join(dir, "package.json");
|
|
11
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
12
|
-
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as { name?: unknown };
|
|
13
|
-
if (pkg.name === PI_CODING_AGENT_PACKAGE) return dir;
|
|
14
|
-
}
|
|
15
|
-
if (dir === path.dirname(dir)) return undefined;
|
|
16
|
-
dir = path.dirname(dir);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function resolveInstalledPiPackageRoot(): string | undefined {
|
|
21
|
-
return findPiPackageRootFromEntry(fileURLToPath(import.meta.resolve(PI_CODING_AGENT_PACKAGE)));
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function resolvePiPackageRoot(): string | undefined {
|
|
25
|
-
try {
|
|
26
|
-
const entry = process.argv[1];
|
|
27
|
-
return entry ? findPiPackageRootFromEntry(fs.realpathSync(entry)) : undefined;
|
|
28
|
-
} catch {
|
|
29
|
-
// Non-launch package inspection is best-effort.
|
|
30
|
-
return undefined;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
6
|
export interface PiRuntimeIdentity {
|
|
35
7
|
readonly nodeExecPath: string;
|
|
36
8
|
readonly piEntryPath: string;
|
|
@@ -90,13 +62,46 @@ function checkAvailability(file: string, executable: boolean): void {
|
|
|
90
62
|
}
|
|
91
63
|
}
|
|
92
64
|
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
const piEntryPath = canonicalPath(deps.argv1 ?? process.argv[1], "Pi entry");
|
|
96
|
-
checkAvailability(nodeExecPath, true);
|
|
65
|
+
function identifyFromEntry(entry: string | undefined): { piEntryPath: string; piPackageRoot: string } {
|
|
66
|
+
const piEntryPath = canonicalPath(entry, "Pi entry");
|
|
97
67
|
checkAvailability(piEntryPath, false);
|
|
98
68
|
const piPackageRoot = canonicalPath(owningPackageRoot(piEntryPath), "Pi package root");
|
|
99
|
-
return {
|
|
69
|
+
return { piEntryPath, piPackageRoot };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function identifyFromInstallRoot(installRoot: string): { piEntryPath: string; piPackageRoot: string } {
|
|
73
|
+
const piPackageRoot = canonicalPath(installRoot, "Pi installation root");
|
|
74
|
+
const manifest = path.join(piPackageRoot, "package.json");
|
|
75
|
+
let bin: unknown;
|
|
76
|
+
try {
|
|
77
|
+
const pkg = JSON.parse(fs.readFileSync(manifest, "utf-8"));
|
|
78
|
+
if (pkg?.name !== PI_CODING_AGENT_PACKAGE) {
|
|
79
|
+
throw new PiRuntimeLaunchError(`${manifest}: installation is not ${PI_CODING_AGENT_PACKAGE}`);
|
|
80
|
+
}
|
|
81
|
+
bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.pi;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (error instanceof PiRuntimeLaunchError) throw error;
|
|
84
|
+
throw new PiRuntimeLaunchError(`${manifest}: ${String(error)}`);
|
|
85
|
+
}
|
|
86
|
+
if (typeof bin !== "string" || !bin) throw new PiRuntimeLaunchError(`${manifest}: missing declared Pi bin`);
|
|
87
|
+
const piEntryPath = canonicalPath(path.resolve(piPackageRoot, bin), "Declared Pi bin");
|
|
88
|
+
checkAvailability(piEntryPath, false);
|
|
89
|
+
const owner = canonicalPath(owningPackageRoot(piEntryPath), "Pi package root");
|
|
90
|
+
if (owner !== piPackageRoot) {
|
|
91
|
+
throw new PiRuntimeLaunchError(`${piEntryPath}: owning package ${owner} does not match installation ${piPackageRoot}`);
|
|
92
|
+
}
|
|
93
|
+
return { piEntryPath, piPackageRoot };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function capturePiRuntime(deps: { execPath?: string; argv1?: string; installRoot?: string } = {}): PiRuntimeIdentity {
|
|
97
|
+
const nodeExecPath = canonicalPath(deps.execPath ?? process.execPath, "Node executable");
|
|
98
|
+
checkAvailability(nodeExecPath, true);
|
|
99
|
+
try {
|
|
100
|
+
return { nodeExecPath, ...identifyFromEntry(deps.argv1 ?? process.argv[1]) };
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (!(error instanceof PiRuntimeLaunchError) || deps.installRoot === undefined) throw error;
|
|
103
|
+
return { nodeExecPath, ...identifyFromInstallRoot(deps.installRoot) };
|
|
104
|
+
}
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
export function parsePiRuntime(value: unknown, source: string): PiRuntimeIdentity {
|
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;
|