@fitchmultz/pi-subagents 0.52.0 → 0.52.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
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.52.2] - 2026-10-29
6
+
7
+ ### Fixed
8
+
9
+ - Avoid rereading historical shell records on every idle parking check while a live shell still blocks parking. Retain one publication path and revalidate its current record, owner token and process birth. Read the full inventory again when that witness disappears or dies. Stop and cold continuation keep their complete inventory checks. No records are pruned and live jobs are not interrupted.
10
+
11
+ ## [0.52.1] - 2026-10-29
12
+
13
+ ### Fixed
14
+
15
+ - On macOS, use the system `ps` directly for process-birth and shell-group checks instead of searching every PATH entry. This reduces the process events and synchronous spawn work caused by frequent parent-liveness checks without changing PID-reuse proof, cancellation, or non-macOS behavior.
16
+
5
17
  ## [0.52.0] - 2026-10-26
6
18
 
7
19
  ### Added
@@ -4,7 +4,7 @@ import * as path from "node:path";
4
4
  import { parkedSchema } from "./contracts.js";
5
5
  import { errorText, loadSDK } from "./host.js";
6
6
  import { conversation, latest, launchAt, list, read, runDirectory, write } from "./storage.js";
7
- import { alive, shells, wakeActor } from "./process.js";
7
+ import { alive, shell, shells, wakeActor } from "./process.js";
8
8
  import { isHistoryRequest, PARK, PARKED_HISTORY, WAKE, } from "../shared/parking.js";
9
9
  import { nativePeerId } from "../pi-intercom/session-targets.js";
10
10
  function parkable(session) {
@@ -74,6 +74,7 @@ export class Parking {
74
74
  restored;
75
75
  since = 0;
76
76
  leaf;
77
+ shell;
77
78
  base;
78
79
  launch;
79
80
  token;
@@ -111,9 +112,7 @@ export class Parking {
111
112
  }
112
113
  /** True once this generation has exited its runtime with or without parked authority. */
113
114
  async park(runtime, actor) {
114
- if (!actor.pi ||
115
- !this.due(runtime.session, actor.busy) ||
116
- shells(this.base, this.launch, this.token).some(alive)) {
115
+ if (!actor.pi || !this.due(runtime.session, actor.busy) || this.hasShell()) {
117
116
  return false;
118
117
  }
119
118
  const hold = await this.prepare(runtime, actor.pi, actor);
@@ -122,6 +121,13 @@ export class Parking {
122
121
  }
123
122
  return hold !== undefined;
124
123
  }
124
+ hasShell() {
125
+ const witness = this.shell === undefined ? undefined : shell(this.shell, this.token);
126
+ if (!witness || !alive(witness)) {
127
+ this.shell = shells(this.base, this.launch, this.token).find(alive)?.file;
128
+ }
129
+ return this.shell !== undefined;
130
+ }
125
131
  /** Final idle: no run, turn, journal change or viewer since the period began. */
126
132
  due(session, busy) {
127
133
  const leaf = session.sessionManager.getLeafId();
@@ -346,6 +346,13 @@ async function waitForExit(targets, timeout) {
346
346
  await delay(25);
347
347
  }
348
348
  }
349
+ export function shell(file, token) {
350
+ const identity = published(file, read(file, processSchema));
351
+ if (identity && identity.token !== token) {
352
+ throw new Error("Foreign or missing native shell identity.");
353
+ }
354
+ return identity;
355
+ }
349
356
  export function shells(base, launch, token) {
350
357
  const dir = path.join(conversation(base, launch), "shells", token);
351
358
  if (!fs.existsSync(dir)) {
@@ -356,11 +363,11 @@ export function shells(base, launch, token) {
356
363
  .filter((name) => name.endsWith(".json"))
357
364
  .map((name) => {
358
365
  const file = path.join(dir, name);
359
- const identity = published(file, read(file, processSchema));
360
- if (!identity || identity.token !== token) {
366
+ const identity = shell(file, token);
367
+ if (!identity) {
361
368
  throw new Error("Foreign or missing native shell identity.");
362
369
  }
363
- return identity;
370
+ return Object.assign(identity, { file });
364
371
  });
365
372
  }
366
373
  function quote(value) {
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { execFileSync } from "node:child_process";
3
3
  import * as path from "node:path";
4
4
  import * as fs from "node:fs";
5
- import { readChildProcessIdentity, readProcessClock } from "../shared/post-exit-stdio-guard.js";
5
+ import { PS_COMMAND, readChildProcessIdentity, readProcessClock, } from "../shared/post-exit-stdio-guard.js";
6
6
  import { read, write } from "./storage.js";
7
7
  import { processSchema } from "./contracts.js";
8
8
  import { verifyProcess } from "./process.js";
@@ -16,7 +16,7 @@ if (directory === undefined ||
16
16
  process.ppid !== pid) {
17
17
  throw new Error("Native shell registration requires its actual parent shell.");
18
18
  }
19
- const group = Number(execFileSync("ps", ["-p", String(pid), "-o", "pgid="], {
19
+ const group = Number(execFileSync(PS_COMMAND, ["-p", String(pid), "-o", "pgid="], {
20
20
  encoding: "utf8",
21
21
  timeout: 1000,
22
22
  stdio: ["ignore", "pipe", "ignore"],
@@ -1 +1 @@
1
- export const EXTENSION_BUILD = Object.freeze({"version":"0.52.0","sha256":"a9faf29451051826907d9343b432c9232529ccaa68c75d681dbcb464702fc25f"});
1
+ export const EXTENSION_BUILD = Object.freeze({"version":"0.52.2","sha256":"cb74b5f47c1e4f8a3a5f27aa2ce24dcb7c240b43aba9472b276a9a815ec30b2a"});
@@ -3,6 +3,8 @@ import { readFileSync, readlinkSync } from "node:fs";
3
3
  import { isLinuxProcessReplaced, readLinuxProcess } from "../pi-intercom/broker/pid.js";
4
4
  import { hasErrorCode } from "./unknown.js";
5
5
  const BIRTH_FIELDS = ["lstart=", "pgid="];
6
+ // macOS ships /bin/ps; resolving "ps" through PATH can spawn once per missing entry.
7
+ export const PS_COMMAND = process.platform === "darwin" ? "/bin/ps" : "ps";
6
8
  /** Capture only a real, cross-process clock context; unavailable context proves nothing. */
7
9
  export function readProcessClock() {
8
10
  if (process.platform !== "android") {
@@ -45,7 +47,7 @@ function psOptions(zone) {
45
47
  }
46
48
  function readPsFields(pid, fields, zone) {
47
49
  try {
48
- return execFileSync("ps", psArguments(pid, fields), {
50
+ return execFileSync(PS_COMMAND, psArguments(pid, fields), {
49
51
  ...psOptions(zone),
50
52
  stdio: ["ignore", "pipe", "ignore"],
51
53
  })
@@ -59,7 +61,7 @@ function readPsFields(pid, fields, zone) {
59
61
  /** Nonblocking `readPsFields`; a failed run likewise observes nothing. */
60
62
  function observePsFields(pid, fields, zone) {
61
63
  return new Promise((resolve) => {
62
- execFile("ps", psArguments(pid, fields), psOptions(zone), (error, stdout) => {
64
+ execFile(PS_COMMAND, psArguments(pid, fields), psOptions(zone), (error, stdout) => {
63
65
  resolve(error ? undefined : stdout.trim().replace(/\s+/g, " "));
64
66
  });
65
67
  });
@@ -165,6 +165,27 @@ is not an indefinite-idle bound. The
165
165
  [issue 238 evidence](evidence/issue-238-idle/README.md) has the samples, host
166
166
  hashes, native causal chain and limits.
167
167
 
168
+ ### Idle parking shell records
169
+
170
+ An idle child with a live shell now retains one shell publication path. Each
171
+ parking check rereads that record and uses the existing token and process-birth
172
+ validation. A missing or dead witness causes a full inventory read. Stop and cold
173
+ continuation still read the complete inventory.
174
+
175
+ On macOS arm64 with Node 24.22.1, five alternating before/after pairs exercised
176
+ `Parking.park` against one real actor's 615 shell records. Both versions refused
177
+ to park on every check. After one untimed initial inventory, each recurring check
178
+ opened 615 records before and one record after. Median wall time changed from
179
+ 80.96 ms to 4.30 ms. Observed ranges were 73.40 to 84.01 ms before and 4.19 to
180
+ 4.43 ms after. The baseline was release 0.52.1 at `3650e11`.
181
+
182
+ This isolated helper measurement advanced the idle clock and supplied an idle
183
+ session stub. It read the real records without changing them or the active actor.
184
+ The initial scan and witness-loss scan still read the full inventory. This is not
185
+ an end-to-end model, TUI or fleet CPU speedup claim. The native `park-shells` test
186
+ separately exercises repeated checks, witness death, missing and replaced
187
+ publications, and continuation on official Pi and the maintained fork.
188
+
168
189
  ## Necessary costs
169
190
 
170
191
  - Native Pi owns context loading and model execution. The extension cannot remove
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitchmultz/pi-subagents",
3
- "version": "0.52.0",
3
+ "version": "0.52.2",
4
4
  "description": "Pi extension for subagent orchestration and local session coordination",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -5,7 +5,7 @@ import type { AgentSession, ExtensionAPI } from "@earendil-works/pi-coding-agent
5
5
  import { parkedSchema, type Launch, type Parked } from "./contracts.ts";
6
6
  import { errorText, loadSDK, type WorkingState } from "./host.ts";
7
7
  import { conversation, latest, launchAt, list, read, runDirectory, write } from "./storage.ts";
8
- import { alive, shells, wakeActor } from "./process.ts";
8
+ import { alive, shell, shells, wakeActor } from "./process.ts";
9
9
  import {
10
10
  isHistoryRequest,
11
11
  PARK,
@@ -138,6 +138,7 @@ export class Parking {
138
138
  private restored?: string;
139
139
  private since = 0;
140
140
  private leaf?: string | null;
141
+ private shell?: string;
141
142
  private readonly base: string;
142
143
  private readonly launch: Launch;
143
144
  private readonly token: string;
@@ -175,11 +176,7 @@ export class Parking {
175
176
  }
176
177
  /** True once this generation has exited its runtime with or without parked authority. */
177
178
  async park(runtime: Runtime, actor: Resident): Promise<boolean> {
178
- if (
179
- !actor.pi ||
180
- !this.due(runtime.session, actor.busy) ||
181
- shells(this.base, this.launch, this.token).some(alive)
182
- ) {
179
+ if (!actor.pi || !this.due(runtime.session, actor.busy) || this.hasShell()) {
183
180
  return false;
184
181
  }
185
182
  const hold = await this.prepare(runtime, actor.pi, actor);
@@ -188,6 +185,13 @@ export class Parking {
188
185
  }
189
186
  return hold !== undefined;
190
187
  }
188
+ private hasShell(): boolean {
189
+ const witness = this.shell === undefined ? undefined : shell(this.shell, this.token);
190
+ if (!witness || !alive(witness)) {
191
+ this.shell = shells(this.base, this.launch, this.token).find(alive)?.file;
192
+ }
193
+ return this.shell !== undefined;
194
+ }
191
195
  /** Final idle: no run, turn, journal change or viewer since the period began. */
192
196
  private due(session: AgentSession, busy: boolean): boolean {
193
197
  const leaf = session.sessionManager.getLeafId();
@@ -393,7 +393,18 @@ async function waitForExit(targets: readonly OwnedProcess[], timeout: number): P
393
393
  await delay(25);
394
394
  }
395
395
  }
396
- export function shells(base: string, launch: Launch, token: string): OwnedProcess[] {
396
+ export function shell(file: string, token: string): OwnedProcess | undefined {
397
+ const identity = published(file, read(file, processSchema));
398
+ if (identity && identity.token !== token) {
399
+ throw new Error("Foreign or missing native shell identity.");
400
+ }
401
+ return identity;
402
+ }
403
+ export function shells(
404
+ base: string,
405
+ launch: Launch,
406
+ token: string,
407
+ ): (OwnedProcess & { readonly file: string })[] {
397
408
  const dir = path.join(conversation(base, launch), "shells", token);
398
409
  if (!fs.existsSync(dir)) {
399
410
  return [];
@@ -403,11 +414,11 @@ export function shells(base: string, launch: Launch, token: string): OwnedProces
403
414
  .filter((name) => name.endsWith(".json"))
404
415
  .map((name) => {
405
416
  const file = path.join(dir, name);
406
- const identity = published(file, read(file, processSchema));
407
- if (!identity || identity.token !== token) {
417
+ const identity = shell(file, token);
418
+ if (!identity) {
408
419
  throw new Error("Foreign or missing native shell identity.");
409
420
  }
410
- return identity;
421
+ return Object.assign(identity, { file });
411
422
  });
412
423
  }
413
424
  function quote(value: string): string {
@@ -2,7 +2,11 @@ import { randomUUID } from "node:crypto";
2
2
  import { execFileSync } from "node:child_process";
3
3
  import * as path from "node:path";
4
4
  import * as fs from "node:fs";
5
- import { readChildProcessIdentity, readProcessClock } from "../shared/post-exit-stdio-guard.ts";
5
+ import {
6
+ PS_COMMAND,
7
+ readChildProcessIdentity,
8
+ readProcessClock,
9
+ } from "../shared/post-exit-stdio-guard.ts";
6
10
  import { read, write } from "./storage.ts";
7
11
  import { processSchema } from "./contracts.ts";
8
12
  import { verifyProcess } from "./process.ts";
@@ -22,7 +26,7 @@ if (
22
26
  throw new Error("Native shell registration requires its actual parent shell.");
23
27
  }
24
28
  const group = Number(
25
- execFileSync("ps", ["-p", String(pid), "-o", "pgid="], {
29
+ execFileSync(PS_COMMAND, ["-p", String(pid), "-o", "pgid="], {
26
30
  encoding: "utf8",
27
31
  timeout: 1000,
28
32
  stdio: ["ignore", "pipe", "ignore"],
@@ -5,6 +5,9 @@ import { hasErrorCode } from "./unknown.ts";
5
5
 
6
6
  const BIRTH_FIELDS = ["lstart=", "pgid="];
7
7
 
8
+ // macOS ships /bin/ps; resolving "ps" through PATH can spawn once per missing entry.
9
+ export const PS_COMMAND = process.platform === "darwin" ? "/bin/ps" : "ps";
10
+
8
11
  interface AgentProcessExit {
9
12
  readonly pid?: number;
10
13
  readonly code: number | null;
@@ -75,7 +78,7 @@ function psOptions(zone: string | undefined) {
75
78
 
76
79
  function readPsFields(pid: number, fields: readonly string[], zone?: string): string | undefined {
77
80
  try {
78
- return execFileSync("ps", psArguments(pid, fields), {
81
+ return execFileSync(PS_COMMAND, psArguments(pid, fields), {
79
82
  ...psOptions(zone),
80
83
  stdio: ["ignore", "pipe", "ignore"],
81
84
  })
@@ -93,7 +96,7 @@ function observePsFields(
93
96
  zone?: string,
94
97
  ): Promise<string | undefined> {
95
98
  return new Promise((resolve) => {
96
- execFile("ps", psArguments(pid, fields), psOptions(zone), (error, stdout) => {
99
+ execFile(PS_COMMAND, psArguments(pid, fields), psOptions(zone), (error, stdout) => {
97
100
  resolve(error ? undefined : stdout.trim().replace(/\s+/g, " "));
98
101
  });
99
102
  });