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 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. Custom hosts whose entry cannot be identified as the installed Pi package's declared CLI are 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.
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.1",
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",
@@ -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: () => {},
@@ -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 { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
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
- runtime = (deps.captureRuntime ?? capturePiRuntime)();
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
- export function capturePiRuntime(deps: { execPath?: string; argv1?: string } = {}): PiRuntimeIdentity {
94
- const nodeExecPath = canonicalPath(deps.execPath ?? process.execPath, "Node executable");
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 { nodeExecPath, piEntryPath, piPackageRoot };
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 {
@@ -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;